.Net Core 中间件

中间件的工作原理

中间件的设计模式是责任链模式,下图是微软官方的中间件执行流程图:
微软官方的中间件执行流程图

中间件的执行顺序

  • 当接收到请求时,中间件会按注册顺序依次执行处理逻辑;
  • 响应请求时,中间件会按注册顺序导入执行响应处理逻辑;
  • 中间件可以起到断路器的作用,中断执行后注册的中间件。

两个核心对象

  • IApplicationBuilder :用于注册Middleware。
  • RequestDelegate :处理HTTP请求的函数委托;参数是请求上下文 HttpContext ;可以将其理解为ASP.NET Core中对一切HTTP请求处理的抽象,没有它整个框架就失去了对HTTP请求的处理能力。

中间件的使用

使用 Use 注入委托:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.Use(async (context, next) =>
    {
        await next(); // 交由下一中间件处理
        if (context.Response.HasStarted)
        {
            // 一旦已经开始输出,则不能再修改响应头的内容
        }
        await context.Response.WriteAsync("Hello");
    });
}

使用 Map

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.Map("/abc", abcBuilder =>
    {
        abcBuilder.Use(async (context, next) =>
        {
            await next();
            await context.Response.WriteAsync("Hello");
        });
    });
}

使用 MapWhen

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
     app.MapWhen(context =>
     {
         return context.Request.Query.Keys.Contains("abc");
     }, builder =>
     {
         builder.Run(async context =>
         {
             await context.Response.WriteAsync("new abc");
         });
     });
}

自定义中间件

自定义中间件需要包含 public async Task InvokeAsync(HttpContext context) 方法:

class MyMiddleware
{
    RequestDelegate _next;
    ILogger _logger;
    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        using (_logger.BeginScope("TraceIdentifier:{TraceIdentifier}", context.TraceIdentifier))
        {
            _logger.LogDebug("开始执行");
            
            await _next(context);

            _logger.LogDebug("执行结束");
        }
    }
}

扩展 IApplicationBuilder

public static class MyBuilderExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app)
    {
        return app.UseMiddleware<MyMiddleware>();
    }
}

注册 MyMiddleware :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseMyMiddleware();
}

异常处理中间件

处理异常有以下几种方式:

  • 异常处理页
  • 异常处理匿名委托方法
  • IExceptionFilter
  • ExceptionFilterAttribute

异常处理页

在研发环境下,可以使用 UseDeveloperExceptionPage 中间件,可以将异常详细的展示出来,便于定位问题:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
	    app.UseDeveloperExceptionPage();
	}
}

在实际生成环境下,可以使用 UseExceptionHandler 跳转到自定义异常页面:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	app.UseExceptionHandler("/error");
}

异常处理匿名委托方法

在前后端分离的项目中,可以使用匿名委托的方式返回错误码及错误信息给前端。
首先先定义服务的业务异常:

    public interface IKnownException
    {
        public string Message { get; }

        public int ErrorCode { get; }

        public object[] ErrorData { get; }
    }

    public class KnownException : IKnownException
    {
        public string Message { get; private set; }

        public int ErrorCode { get; private set; }

        public object[] ErrorData { get; private set; }

        public readonly static IKnownException Unknown = new KnownException { Message = "未知错误", ErrorCode = 9999 };

        public static IKnownException FromKnownException(IKnownException exception)
        {
            return new KnownException { Message = exception.Message, ErrorCode = exception.ErrorCode, ErrorData = exception.ErrorData };
        }
    }
    
    public class MyServerException : Exception, IKnownException
    {
        public MyServerException(string message, int errorCode, params object[] errorData) : base(message)
        {
            this.ErrorCode = errorCode;
            this.ErrorData = errorData;
        }

        public int ErrorCode { get; private set; }
        public object[] ErrorData { get; private set; }
    }

然后使用匿名委托注入异常处理逻辑:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseExceptionHandler(errApp =>
    {
        errApp.Run(async context =>
        {
            var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
            IKnownException knownException = exceptionHandlerPathFeature.Error as IKnownException;
            if (knownException == null)
            {
                var logger = context.RequestServices.GetService<ILogger<Startup>>();
                logger.LogError(exceptionHandlerPathFeature.Error, exceptionHandlerPathFeature.Error.Message);
                knownException = KnownException.Unknown;
                context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else
            {
                knownException = KnownException.FromKnownException(knownException);
                context.Response.StatusCode = StatusCodes.Status200OK;
            }
            var jsonOptions = context.RequestServices.GetService<IOptions<JsonOptions>>();
            context.Response.ContentType = "application/json; charset=utf-8";
            await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(knownException, jsonOptions.Value.JsonSerializerOptions));
        });
    });
}

异常抛出:

//throw new Exception("报个错");
throw new MyServerException("服务出错了", 65);

异常处理结果:

{
message: "服务出错了",
errorCode: 65,
errorData: [ ]
}

IExceptionFilter

使用 IExceptionFilter 时,能捕获到在 Controller 里面输出的错误;如果是在 MVC 的中间件之前输出的错误,它是没办法处理的。如果需要对 Controller 进行特殊的异常处理,可以使用 IExceptionFilter 。
使用时,先定义自己的 ExceptionFilter :

public class MyExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        IKnownException knownException = context.Exception as IKnownException;
        if (knownException == null)
        {
            var logger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilter>>();
            logger.LogError(context.Exception, context.Exception.Message);
            knownException = KnownException.Unknown;
            context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        }
        else
        {
            knownException = KnownException.FromKnownException(knownException);
            context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
        }
        context.Result = new JsonResult(knownException)
        {
            ContentType = "application/json; charset=utf-8"
        };
    }
}

在 ConfigureServices 方法中添加自定义异常过滤器:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(mvcOptions =>
    {
        mvcOptions.Filters.Add<MyExceptionFilter>();
    });
}

ExceptionFilterAttribute

ExceptionFilterAttribute 可以更细粒度的控制异常的处理。
先定义自己的 ExceptionFilterAttribute :

public class MyExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        IKnownException knownException = context.Exception as IKnownException;
        if (knownException == null)
        {
            var logger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>();
            logger.LogError(context.Exception, context.Exception.Message);
            knownException = KnownException.Unknown;
            context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        }
        else
        {
            knownException = KnownException.FromKnownException(knownException);
            context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
        }
        context.Result = new JsonResult(knownException)
        {
            ContentType = "application/json; charset=utf-8"
        };
    }
}

然后再在需要处理异常的 Controller 或者 Action 上打上 [MyExceptionFilter] 标签即可。
当然 ExceptionFilterAttribute 也继承了 IExceptionFilter 接口,这使得我们也可以在 ConfigureServices 中对 ExceptionFilterAttribute 进行全局注册对所有 Controller 进行异常处理:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(mvcOptions =>
    {
        mvcOptions.Filters.Add<MyExceptionFilterAttribute>();
    });
}

异常处理技巧

  • 用特定的异常类或接口表示业务逻辑异常
  • 为业务逻辑异常定义全局错误码
  • 为未知异常定义特定的输出信息和错误码
  • 对于已知业务逻辑异常响应 HTTP 200 (监控系统友好)
  • 对于未预见的异常响应 HTTP 500
  • 为所有的异常记录详细的日志

静态文件中间件

静态文件中间件的能力:

  • 支持指定相对路径
  • 支持目录浏览
  • 支持设置默认文档
  • 支持多目录映射

使用 UseStaticFiles 静态文件中间件启用静态文件,静态文件存放于 wwwroot 文件夹下 :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();
}

UseStaticFiles 的重载方法,输入 StaticFileOptions 参数,启用指定目录下的文件为静态文件:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles(new StaticFileOptions
    {
        RequestPath = "/files",
        FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "file"))
    });
}

使用 UseDefaultFiles 中间件开启默认文件,默认文件为 index.html :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseDefaultFiles();
}

UseDefaultFiles 中间件也提供了重载方法,输入 DefaultFilesOptions 参数来自定义默认文件。
实现所有非API请求重定向到指定页面:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();
    app.MapWhen(context =>
    {
        return !context.Request.Path.Value.StartsWith("/api");
    }, appBuilder =>
    {
    	// 重写url
        var option = new RewriteOptions();
        option.AddRewrite(".*", "/index.html", true);
        appBuilder.UseRewriter(option);
        appBuilder.UseStaticFiles();
    });
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值