乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core中间件,掌控请求处理过程中的关键
什么是中间件
中间件是一种计算机软件,为软件应用程序提供操作系统以外的服务。它可以被描述为"软件胶水"。
中间件使软件开发者更容易实现通信和输入/输出,因此他们可以专注于其应用程序的具体目的。它在20世纪80年代得到了普及,作为解决如何将较新的应用程序与较旧的遗留系统联系起来的问题,尽管这个术语从1968年就开始使用了。
中间件的类型
关于中间件的分类,存在许多定义,这些定义要么是关于它的使用领域,要么是关于它所服务的应用模块。在最近的书目中,中间件的主要分类如下:。
- 事务性的(Transactional)。处理多个同步/异步交易,作为分布式系统(如银行交易或信用卡支付)的相关请求集群。
- 面向消息的(Message-oriented)。消息队列和消息传递架构,支持同步/异步通信。
- 程序性的(Procedural)。远程和本地架构,连接、传递和检索异步系统通信的软件响应,如调用操作。
- 面向对象(Object-oriented)。与程序性中间件相似,然而,这种类型的中间件包含了面向对象的编程设计原则。从分析上看,其软件组件包含了对象引用、异常和通过分布式对象请求的属性继承。
什么是ASP.NET Core中间件
中间件(Middleware)是一种装配到应用管道以处理请求和响应的软件。
每个组件
- 选择是否将请求传递到管道中的下一个组件
- 可在管道中的下一个组件前后执行工作
前世今生
Asp.Net Core
其实就是仍然基于.Net Full Framework
(最低要求Framework 4.6.2)的项目, 但同时保留了.Net Core一些新的设置理念,比如Asp.Net Core
默认使用Kestrel
作为Http请求的监听器,而不是使用原来庞大的Https.sys
。Kestrel不仅仅是微软下一代的跨平台Http请求监听器,同时还提供了比Https.sys更轻量级以及更快速的Http请求处理。
另除此之外,Asp.Net Core与原来的Web设计另一个最大的区别在于Asp.Net Core(及.Net Core)完全抛弃了原来的使用管道模式来接收以及处理HttpRequest。在Asp.Net Core中允许处理中间件(Middleware)来对所有的HttpRequest来进行请求,当请求被接收到时,Asp.Net Core会调用注册的中间件(按照注册的顺序)对HttpRequest进行处理。这样做相比与原来使用HttpApplication的管道方式而言,其优势在于完全由开发人员决定HttpRequest需要执行怎么样的处理,没有多余的其他步骤。而原来的方式既使开发人员不希望对一个HttpRequest进行任何处理,但HttpApplication仍然会按照管道的设置依次创建HttpModel -> 激活HttpHandler -> 处理Session等。
据.Net Core团队给出来的性能测试数据来看,Asp.Net Core(.Net Core)相比与原来的Web(.Net framework 4.6)程序性能提升了2300%.
而.Net Core其实就是保留了上面所说的优势的同时支持跨平台运行。.Net Core的系统是可以真正运行在除Windows以外的其他平台的。轻量级、跨平台、模块化是.Net Core整体的设计理念,同时也是微软产品理念转变的一个体现。.Net Core虽然有千般好,但是我们当前仍然没有直接使用它,因为它现在有一个致使的“缺陷”那就是生态环境,由于.Net Core的API已经完全重写,虽然当前已经提供了.Net framework 90%以上的API,但是仍然会造成一些开发上的不便,当然这还不是最大的问题,最大的问题在于一些第三方Nuget包仍然不支持.Net Core。这样就会造成一些项目无法直接迁移或是迁移成本太高的问题。
工作原理
ASP.NET Core请求管道包含一系列请求委托,依次调用。
每个委托均可在下一个委托前后执行操作。应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。
尽可能简单的ASP.NET Core应用设置了处理所有请求的单个请求委托。这种情况不包括实际请求管道。调用单个匿名函数以响应每个HTTP请求。
下图演示了这一概念,沿黑色箭头执行。
如果把上诉图顺时针90度旋转,我们会发现,它像一个俄罗斯套娃。
其中最早的中间件Middleware1看起来是最大的套娃,然后往下去看,每一个套娃一个个套进去越来越小,每个套娃的处理过程我们看到,有logic和next,next就表示后面的所有的套娃的一个委托,每一层每一层套下去的话,我们可以在任意的中间件来决定,在后面的中间件之前执行什么,或者说在所有中间件执行完执行什么
从图中可以看到,RequestDelegate
携带着HttpContext
一路经过各种Server
、Hosting
等,最终到达了由IApplicationBuilder
构建出来的Applicationpipeline
这一管道区域,然后再经过各种中间件处理,最终构建出来了我们的Response
,而我们的工具箱也正是在这个过程中变得“饱满”起来。
有一个需要知道的知识点就是,中间件是怎么样添加或者叫注册到管道中的呢?又是如何被应用起来的呢?
上面的图可以看到,橙色区域的Applicationpipeline
是由IApplicationBuilder
构建起来的。也就是说我们可以在IApplicationBuilder
做点什么东西来添加我们的中间件。是的IApplicationBuilder
暴露出来了一个IApplicationBuilderUse(Func<RequestDelegate,RequestDelegate>middleware);
方法来让我们注册中间件,也就是说位于Startup.cs
文件中的Configure
方法。
那么又是怎么样应用起来的呢?IApplicationBuilder
在Hosting
中有一个IApplicationBuilderFactory
的对象,Hosting
通过这个Factory
创建之后就会传递到了HostingApplication
对象中,最后由IWebHost
对象调用IServer
对象的Start
同时把HostingApplication
传递进去来最终启动服务端。
核心对象
- IApplicationBuilder
- RequestDelegate,处理整个请求的委托
- HttpContext
- HttpRequest
- HttpResponse
- IFeatureCollection
先来看IApplicationBuilder
的定义
//
// 摘要:
// Defines a class that provides the mechanisms to configure an application's request
// pipeline.
public interface IApplicationBuilder
{
//
// 摘要:
// Gets or sets the System.IServiceProvider that provides access to the application's
// service container.
IServiceProvider ApplicationServices { get; set; }
//
// 摘要:
// Gets a key/value collection that can be used to share data between middleware.
IDictionary<string, object> Properties { get; }
//
// 摘要:
// Gets the set of HTTP features the application's server provides.
IFeatureCollection ServerFeatures { get; }
//
// 摘要:
// Builds the delegate used by this application to process HTTP requests.
//
// 返回结果:
// The request handling delegate.
RequestDelegate Build();
//
// 摘要:
// Creates a new Microsoft.AspNetCore.Builder.IApplicationBuilder that shares the
// Microsoft.AspNetCore.Builder.IApplicationBuilder.Properties of this Microsoft.AspNetCore.Builder.IApplicationBuilder.
//
// 返回结果:
// The new Microsoft.AspNetCore.Builder.IApplicationBuilder.
IApplicationBuilder New();
//
// 摘要:
// Adds a middleware delegate to the application's request pipeline.
//
// 参数:
// middleware:
// The middleware delegate.
//
// 返回结果:
// The Microsoft.AspNetCore.Builder.IApplicationBuilder.
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}
它有一个Use
方法,可以让我们去注册中间件。每一个委托的入参也是一个委托。
它还有一个Build
方法,可以把所有的中间件串起来之后合并成一个委托方法。
再来看RequestDelegate
的定义
//
// 摘要:
// A function that can process an HTTP request.
//
// 参数:
// context:
// The Microsoft.AspNetCore.Http.HttpContext for the request.
//
// 返回结果:
// A task that represents the completion of request processing.
public delegate Task RequestDelegate(HttpContext context);
这个委托的入参就是一个HttpContext
,所有的委托实际上都是对HttpContext
的处理。
中间件顺序
ASP.NET Core MVC和Razor Pages应用的完整请求处理管道
内置中间件
中间件 | 描述 | 顺序 |
---|---|---|
身份验证(Authentication) | 提供身份验证支持。 | 在需要HttpContext.User之前。OAuth回叫的终端。 |
授权(Authorization) | 提供身份验证支持。 | 紧接在身份验证中间件之后。 |
Cookie策略(Cookie Policy) | 跟踪用户是否同意存储个人信息,并强制实施cookie字段(如secure和SameSite)的最低标准。 | 在发出cookie的中间件之前。示例:身份验证、会话、MVC(TempData)。 |
CORS | 配置跨域资源共享。 | 在使用CORS的组件之前。由于此错误,UseCors当前必须在UseResponseCaching之前运行。 |
DeveloperExceptionPage | 生成一个页面,其中包含的错误信息仅适用于开发环境。 | 在生成错误的组件之前。对于开发环境,项目模板会自动将此中间件注册为管道中的第一个中间件。 |
诊断(Diagnostics) | 提供新应用的开发人员异常页、异常处理、状态代码页和默认网页的几个单独的中间件。 | 在生成错误的组件之前。异常终端或为新应用提供默认网页的终端。 |
转接头(Forwarded Headers) | 将代理标头转发到当前请求。 | 在使用已更新字段的组件之前。示例:方案、主机、客户端IP、方法。 |
运行状况检查(Health Check) | 检查ASP.NETCore应用及其依赖项的运行状况,如检查数据库可用性。 | 如果请求与运行状况检查终结点匹配,则为终端。 |
标头传播(Header Propagation) | 将HTTP标头从传入的请求传播到传出的HTTP客户端请求中。 | |
HTTP日志记录(HTTP Logging) | 记录HTTP请求和响应。 | 中间件管道的开头。 |
HTTP方法重写(HTTP Method Override) | 允许传入POST请求重写方法。 | 在使用已更新方法的组件之前。 |
HTTPS重定向(HTTPS Redirection) | 将所有HTTP请求重定向到HTTPS。 | 在使用URL的组件之前。 |
HTTP严格传输安全性(HSTS,HTTP Strict Transport Security) | 添加特殊响应标头的安全增强中间件。 | 在发送响应之前,修改请求的组件之后。示例:转接头、URL重写。 |
MVC | 用MVC/RazorPages处理请求。 | 如果请求与路由匹配,则为终端。 |
OWIN | 与基于OWIN的应用、服务器和中间件进行互操作。 | 如果OWIN中间件处理完请求,则为终端。 |
请求解压缩(Request Decompression) | 提供对解压缩请求的支持。 | 在读取请求正文的组件之前。 |
响应缓存(Response Caching) | 提供对缓存响应的支持。 | 在需要缓存的组件之前。UseCORS必须在UseResponseCaching之前。 |
响应压缩(Response Compression) | 提供对压缩响应的支持。 | 在需要压缩的组件之前。 |
请求本地化(Request Localization) | 提供本地化支持。 | 在对本地化敏感的组件之前。使用RouteDataRequestCultureProvider时,必须在路由中间件之后显示。 |
终结点路由(Endpoint Routing) | 定义和约束请求路由。 | 用于匹配路由的终端。 |
SPA | 通过返回单页应用程序(SPA)的默认页面,在中间件链中处理来自这个点的所有请求 | 在链中处于靠后位置,因此其他服务于静态文件、MVC操作等内容的中间件占据优先位置。 |
会话(Session) | 提供对管理用户会话的支持。 | 在需要会话的组件之前。 |
静态文件(Static Files) | 为提供静态文件和目录浏览提供支持。 | 如果请求与文件匹配,则为终端。 |
URL重写(URL Rewrite) | 提供对重写URL和重定向请求的支持。 | 在使用URL的组件之前。 |
W3CLogging | 以W3C扩展日志文件格式生成服务器访问日志。 | 中间件管道的开头。 |
WebSockets | 启用WebSockets协议。 | 在接受WebSocket请求所需的组件之前。 |
自定义中间件
中间件是一种装配到应用管道以处理请求和响应的软件。 ASP.NET Core提供了一组丰富的内置中间件组件,但在某些情况下,你可能需要写入自定义中间件。
使用中间件
常用用法
- Run
- Use
- Map
- MapWhen
基本使用(Use)
在Startup.cs
的Configure
方法中来注册中间件。
这里先介绍Use的方式,看下定义
//
// 摘要:
// Extension methods for adding middleware.
public static class UseExtensions
{
//
// 摘要:
// Adds a middleware delegate defined in-line to the application's request pipeline.
//
// 参数:
// app:
// The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
//
// middleware:
// A function that handles the request or calls the given next function.
//
// 返回结果:
// The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware);
}
自带中间件注册示例
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
从上诉讲到的图来说,中间件注册的顺序是和执行顺序是存在关系的,最早注册的中间件权力最大,它可以越早的发生作用。
除了上诉自带的内置中间件,我们还可以通过Use
来注册我们自定义的中间件逻辑。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Use(async (httpcontext, next) =>
{
await httpcontext.Response.WriteAsync("Hello Middleware");
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
运行输出
我们看到它只是输出了我们定义的值,但是没有执行后续的动作了,因为我们在这个中间里面没有让它继续执行next
的操作。
如果要让它执行后续的中间件,那么这里我们需要增加逻辑。
app.Use(async (httpcontext, next) =>
{
await httpcontext.Response.WriteAsync("Hello Middleware");
await next();
});
再次运行,虽然往后执行了,但是会报错,因为后面的中间件修改了Header的值。
System.InvalidOperationException: Headers are read-only, response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_ContentType(String value)
at Microsoft.AspNetCore.Mvc.Formatters.OutputFormatter.WriteResponseHeaders(OutputFormatterWriteContext context)
at Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter.WriteAsync(OutputFormatterWriteContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, Object value)
我们可以调整下顺序,先让后面的执行,再来执行我们这个逻辑。
app.Use(async (httpcontext, next) =>
{
await next();
await httpcontext.Response.WriteAsync("Hello Middleware");
});
这时候就顺利执行了,而且会发现,Hello Middleware
跟在正常输出结果的后面了。
基本使用(Map)
Map
的用法是可以针对特定路径来注册中间件逻辑的。
先看下Map扩展的定义
//
// 摘要:
// Extension methods for the Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.
public static class MapExtensions
{
//
// 摘要:
// Branches the request pipeline based on matches of the given request path. If
// the request path starts with the given path, the branch is executed.
//
// 参数:
// app:
// The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
//
// pathMatch:
// The request path to match.
//
// configuration:
// The branch to take for positive path matches.
//
// 返回结果:
// The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration);
}
具体使用
app.Map("/abc", applicationBuilder =>
{
applicationBuilder.Use(async (httpcontext, next) =>
{
await next();
await httpcontext.Response.WriteAsync("Hello Middleware");
});
});
执行结果
可以看到在地址/abc
的时候成功执行了我们定义的中间件逻辑。
基本使用(MapWhen)
当我们的判断逻辑不只是简单的路径的时候,我们可以使用MapWhen,它支持判断逻辑是一个委托。
app.MapWhen(httpContext =>
{
return httpContext.Request.Query.Keys.Contains("abc");
}, applicationBuilder =>
{
applicationBuilder.Run(async httpcontext =>
{
await httpcontext.Response.WriteAsync("Hello Middleware");
});
});
这里当请求字符串里面包括abc
的Key的时候,通过IApplicationBuilder
的Run
方法直接执行输出。和之前的Use
相比,Run
就是中间件执行的末端。
自定义中间件
先定义一个带有Invoke
或者InvokeAsync
方法的中间件类。
internal class TeslaMiddleware
{
readonly RequestDelegate _next;
readonly ILogger _logger;
public TeslaMiddleware(RequestDelegate next, ILogger<TeslaMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext httpContext)
{
using (_logger.BeginScope("TraceIdentifier: {TraceIdentifier}", httpContext.TraceIdentifier))
{
_logger.LogDebug("Start Action");
await _next(httpContext);
_logger.LogDebug("End Action");
}
}
}
这里定义了一个RequestDelegate
委托来表示衔接在这个中间件之后的中间件,同时,我们通过日志框架在这前后打印了下日志。
这个我们还通过配置文件将这个代码的日志等级降低到Debug,以便可以看到这个日志。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Warning"
},
"Console": {
"LogLevel": {
"demoForMiddleware31.Middlewares.TeslaMiddleware": "Debug"
}
}
},
"AllowedHosts": "*"
}
运行结果如下
我们看到,这时候Controller还是继续成功执行的。
因为这里await _next(httpContext)
成功的执行了后面的中间件及逻辑。
如果我们注释掉它,那么后面的Controller逻辑就不会被执行。
这样就可以实现一个断路器。
注意事项
- 中间件注入的顺序十分重要,某些中间件起到的是断路器的作用,某些中间件会做一些请求内容的处理。
- 应用程序一旦开始向Response.Write时,后续的中间件就不能再去操作它的Header了,否则会抛出异常。
判断是否开始向Response输出内容的方式:
Response.HasStarted
public async Task InvokeAsync(HttpContext httpContext)
{
using (_logger.BeginScope("TraceIdentifier: {TraceIdentifier}", httpContext.TraceIdentifier))
{
_logger.LogDebug("Start Action");
await _next(httpContext);
if (!httpContext.Response.HasStarted)
{
await httpContext.Response.WriteAsync("Hello Middleware");
}
_logger.LogDebug("End Action");
}
}
异常处理中间件(UseExceptionHandler)
常见处理异常的方式
ASP.NET Core给我们提供了四种方式
- 异常处理页
- 异常处理匿名委托方法
- 异常处理过滤器(IExceptionFilter)
- ExceptionFilterAtttribute
开发环境下的异常处理页(UseDeveloperExceptionPage)
默认在Startup.cs
的Configure
方法中,就有一个开发环境使用的UseDeveloperExceptionPage
中间件注册。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
主动报一个错
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
throw new Exception("Has Error");
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
这个错误页面会输出很多信息。
自定义错误页(UseExceptionHandler)
我们可以通过UseExceptionHandler
并且指定一个错误页来展示我们要展示的错误信息。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//if (env.IsDevelopment())
//{
// app.UseDeveloperExceptionPage();
//}
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
这里我们需要配合做一个Error页面出来。
先在ConfigureServices
里面添加下MVC的服务。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMvc();
}
接下来我们新增一个Views
-Error
-Index.cshtml
界面来承载。
@model demoForMiddleware231.Exceptions.IKnowException
@{
ViewData["Title"] = "Index";
}
<h1>错误信息</h1>
<div>Message:<label>@Model.Message</label></div>
<div>ErrorCode:<label>@Model.ErrorCode</label></div>
接下来,看看我们定义的IKnowException
和KnowException
定义。
public interface IKnowException
{
public string Message { get; }
public int ErrorCode { get; }
public object[] ErrorData { get; }
}
public class KnowException : IKnowException
{
public string Message { get; private set; }
public int ErrorCode { get; private set; }
public object[] ErrorData { get; private set; }
public readonly static IKnowException Unknown = new KnowException { Message = "未知错误", ErrorCode = 9999 };
public static IKnowException FromKnowException(IKnowException knowException)
{
return new KnowException { Message = knowException.Message, ErrorCode = knowException.ErrorCode, ErrorData = knowException.ErrorData };
}
}
接下来新建一个ErrorController
来拼装界面和数据。
public class ErrorController : Controller
{
[Route("/error")]
public IActionResult Index()
{
var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
var ex = exceptionHandlerPathFeature?.Error;
IKnowException knowException = ex as IKnowException;
if(knowException == null)
{
var logger = HttpContext.RequestServices.GetService<ILogger<TeslaExceptionFilterAtttribute>>();
logger.LogError(ex, ex.Message);
knowException = KnowException.Unknown;
}
else
{
knowException = KnowException.FromKnowException(knowException);
}
return View(knowException);
}
}
通过HttpContext.Features
方法我们可以得到当前上下文的异常信息,如果这个异常符合我们定义的已知异常类型,那么就把它转换为已知异常类型对象。
如果这个异常不符合我们已知异常类型,那么通过日志记录这个异常,并且把异常类型设置为指定的未知类型异常。
我们来运行看看实际效果如何
很好,由于我们主动报的异常并不是已知格式的异常,所以它会显示未知异常的类型。
自定义错误输出(UseExceptionHandler)
其次,我们还可以使用代理方法的方式处理异常,并且以Json的格式进行输出。
之前我们提到,在中间件这里我们可以使用Run
模式进行断路,借助UseExceptionHandler
,我们也可以实现在发生异常的时候,直接断路返回指定格式的异常信息,这种方式更适合接口类型的服务。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseExceptionHandler(applicationBuilder =>
{
applicationBuilder.Run(async httpContext =>
{
var exceptionHandlerPathFeature = httpContext.Features.Get<IExceptionHandlerPathFeature>();
var ex = exceptionHandlerPathFeature?.Error;
IKnowException knowException = ex as IKnowException;
if (knowException == null)
{
var logger = httpContext.RequestServices.GetService<ILogger<TeslaExceptionFilterAtttribute>>();
logger.LogError(ex, ex.Message);
knowException = KnowException.Unknown;
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
else
{
knowException = KnowException.FromKnowException(knowException);
httpContext.Response.StatusCode = StatusCodes.Status200OK;
}
var jsonOptions = httpContext.RequestServices.GetService<IOptions<JsonOptions>>();
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(knowException, jsonOptions.Value.JsonSerializerOptions));
});
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
这里我们设计了如果是已知异常,我们还是输出200,如果是未知异常,那就输出500;
监控系统会对响应码进行识别,当监控系统发现比较多的500,监控系统会认为服务处于不可用状态。
自定义异常过滤器(ExceptionFilter)
自定义异常过滤器(ExceptionFilter)可以作用于MVC框架体系内的,而不是在中间件的早期去发生作用。
继承IExceptionFilter
自定义异常过滤器TeslaExceptionFilter
public class TeslaExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
IKnowException knowException = context.Exception as IKnowException;
if (knowException == null)
{
var logger = context.HttpContext.RequestServices.GetService<ILogger<TeslaExceptionFilterAtttribute>>();
logger.LogError(context.Exception, context.Exception.Message);
knowException = KnowException.Unknown;
context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
else
{
knowException = KnowException.FromKnowException(knowException);
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
}
context.Result = new JsonResult(knowException)
{
ContentType = "application/json"
};
}
}
和之前的处理逻辑一样,遇到已知错误就直接输出,不能识别的错误就标记为未知错误。
在ConfigureServices
这里把它添加进来。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMvc(mvcOptions =>
{
mvcOptions.Filters.Add<TeslaExceptionFilter>();
}).AddJsonOptions(jsonOptions =>
{
jsonOptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
});
}
运行后产生的效果和之前自定义错误输出一样。
自定义异常属性(ExceptionFilterAttribute)
自定义一个TeslaExceptionFilterAtttribute
,继承自ExceptionFilterAttribute
,让它可以处理异常的逻辑。
public class TeslaExceptionFilterAtttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
IKnowException knowException = context.Exception as IKnowException;
if(knowException == null)
{
var logger = context.HttpContext.RequestServices.GetService<ILogger<TeslaExceptionFilterAtttribute>>();
logger.LogError(context.Exception, context.Exception.Message);
knowException = KnowException.Unknown;
context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
else
{
knowException = KnowException.FromKnowException(knowException);
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
}
context.Result = new JsonResult(knowException)
{
ContentType = "application/json"
};
}
}
在MVC这里我们暂时不需要添加它
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMvc(mvcOptions =>
{
}).AddJsonOptions(jsonOptions =>
{
jsonOptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
});
}
然后我们在需要进行这个异常处理逻辑的Controller上增加它。
[ApiController]
[Route("[controller]")]
[TeslaExceptionFilterAtttribute]
public class WeatherForecastController : ControllerBase
{
}
这样跑出来的结果和自定义异常过滤器的是一样的。
我们看到其实ExceptionFilterAttribute也是继承了IExceptionFilter
,所以它可以注册为全局的。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMvc(mvcOptions =>
{
mvcOptions.Filters.Add<TeslaExceptionFilterAtttribute>();
}).AddJsonOptions(jsonOptions =>
{
jsonOptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
});
}
这样就可以移除之前Controller那个Atttribute了,因为它已经是全局过滤器。
总结异常处理技巧
- 用特定的异常类或者接口来表示业务逻辑异常
- 为业务逻辑异常定义全局错误码
- 为未知异常定义特定的输出信息和错误码
- 对于已知业务逻辑异常响应HTTP 200(监控系统友好)
- 对于未预见的异常响应HTTP 500
- 为所有的异常记录详细的日志
静态文件中间件(UseStaticFiles)
能力
- 支持指定相对路径
- 支持目录浏览
- 支持设置默认文档
- 支持映射多目录
启用静态文件(UseStaticFiles)
创建一个约定名为wwwroot
的文件夹目录,创建之后,它图标会变化成一个网络图标。
在Startup.cs
的Configure
方法中引入静态文件中间件UseStaticFiles
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
这时候,我们就可以访问wwwroot
下面所有静态文件了。
我们弄一点静态文件丢里面
启动后,访问https://localhost:5001/Index.html
启用默认页面(UseDefaultFiles)
上面,我们发现,如果不指定具体HTML文件路径,它不会自动去找Index.html
。
我们可以使用UseDefaultFiles
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
根据它的定义,也可以传值进去。
public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath);
启用目录浏览(UseDirectoryBrowser)
我们可以通过UseDirectoryBrowser
来支持。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseDirectoryBrowser();
app.UseStaticFiles();
同时在ConfigureServices
方法中去注册DirectoryBrowser
相关的服务。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDirectoryBrowser();
}
自定义静态文件目录
我们还可以添加对非wwwroot
目录的静态文件的支持。
我们新建一个名为Assets
的文件夹,并且内置一个List.html
,然后我们在Configure
方法中追加对这个文件夹的物理文件支持。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Assets"))
});
接下来我们去请求这个List.html
,它会先去wwwroot
目录寻找,如果找不到就继续找我们当前追加的这个目录。
并且我们还可以让这个目录,配置单独的访问地址,这里可以通过RequestPath
来进行配置。
app.UseStaticFiles(new StaticFileOptions
{
RequestPath = "/Assets",
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Assets"))
});
实现前端HTML5 History路由模式的支持
要实现一个,除了我们定义的api接口,其他的地址访问都自动导向index.html
去。
首先我们改造下默认的Controller的Route
地址,让它携带一个特征/api
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
接下来我们使用MapWhen
的方法来做个排除法
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.MapWhen(httpContext =>
{
return !httpContext.Request.Path.Value.StartsWith("/api");
}, applicationBuilder =>
{
var rewriteOptions = new RewriteOptions();
rewriteOptions.AddRewrite(".*", "/index.html", true);
applicationBuilder.UseRewriter(rewriteOptions);
applicationBuilder.UseStaticFiles();
});
这里当请求的地址不是以/api
开头的,我们都通过UseRewriter
重写路由的中间件把请求地址给重写了,重写之后,我们再使用静态中间件。
除了使用UseRewriter
中间件,我们还可以采用断路器的方式。
const int BufferSize = 64 * 1024;
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.MapWhen(httpContext =>
{
return !httpContext.Request.Path.Value.StartsWith("/api");
}, applicationBuilder =>
{
applicationBuilder.Run(async httpContext =>
{
var indexFile = env.WebRootFileProvider.GetFileInfo("index.html");
httpContext.Response.ContentType = "text/html";
using (var fileStream = new FileStream(indexFile.PhysicalPath, FileMode.Open, FileAccess.Read))
{
await StreamCopyOperation.CopyToAsync(fileStream, httpContext.Response.Body, null, BufferSize, httpContext.RequestAborted);
}
});
});
也可以达到同样的效果,但是这种和前面的区别是,这种无法利用Http的缓存效果。