.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();
});
}