编程博客
002、修改Nuget包位置
001、C#常用的单词
003、收藏的书签
回家准备
004、ASP.NET Core 3.0 gRPC
001、ASP.NET Core 3.0 使用gRPC
002、ASP.NET Core 3.0 gRPC 双向流
003、ASP.NET Core 3.0 gRPC 身份认证和授权
005、飞牛NAS
001、 FnOS飞牛系统实用配置1-应用远程访问(迅雷)
006、DeepSeek
001、DeepSeek 使用指南
007、并发编程
01、并发编程 - 死锁的产生、排查与解决方案
02、并发编程 - 初识线程
03、并发编程 - 线程浅试
04、并发编程 - 线程同步(一)
05、并发编程 - 线程同步(二)
06、并发编程 - 线程同步(三)之原子操作 Interlocked 简介
07、并发编程 - 线程同步(四)之原子操作 Interlocked 详解一
08、并发编程 - 线程同步(五)之原子操作 Interlocked 详解二
09、并发编程 - 线程同步(六)之锁 lock
008、启动项
03、应用--Program 中的 WebApplication
02、主机--Host
01、Program 文件的作用
04、控制反转 IOC 与依赖注入 DI
05、中间件
06、Logger 原理及配置 Log4Net
07、ElasticSearch
本文档使用 MrDoc 发布
-
+
首页
05、中间件
# 管道与中间件 管道由中间件组成,可以想象成管道就是一个产品线的处理流程模板,中间件就是这个流程上需要对一件产品做的处理,而这个产品就是我们的请求,当我们的请求进入管道的时候,会按照中间件的顺序对于请求做处理,然后选择是否传至下一个中间件进行处理,直到到达管道末尾,然后返回结果。 ## 中间件[#](https://www.cnblogs.com/boise/p/18002742#%E4%B8%AD%E9%97%B4%E4%BB%B6) ### 中间件作用:[#](https://www.cnblogs.com/boise/p/18002742#%E4%B8%AD%E9%97%B4%E4%BB%B6%E4%BD%9C%E7%94%A8) 微软官网原话: > 中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件 > > * 选择是否将请求传递到管道中的下一个组件。 > * 可在管道中的下一个组件前后执行工作。 > > 请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。 > > 使用 RunMap 或者 Use 拓展方法来配置请求委托,也可将一个单独的请求委托并行指定为匿名方法(称为并行中间件),或在可重用的类中对其进行定义。 这些可重用的类和并行匿名方法即为中间件,也叫中间件组件。 请求管道中的每个中间件组件负责调用管道中的下一个组件,或使管道短路。 当中间件短路时,它被称为“终端中间件”,因为它阻止中间件进一步处理请求。 简单来说,我们的请求处理管道其实是由一个个中间件组成的,每一个中间件都是一个处理请求和响应的组件(或者说委托,也就是 `RequestDelegate`),每一个中间件处理完成后,会把 `HttpContext上下文`(next)传递到下一个组件,直到到达终端中间件,终端中间件执行完成后,会原路返回。 ### 中间件短路:[#](https://www.cnblogs.com/boise/p/18002742#%E4%B8%AD%E9%97%B4%E4%BB%B6%E7%9F%AD%E8%B7%AF) 中间件短路是指在这个中间件处理中,不会把 `HttpContext上下文` 传递给下一个组件,比如 `app.Run()` 就是一个终端中间件,他不会收到 next,无论你自己定义几个 `app.Run()`,永远在到达第一个终端中间件后,立即短路,不会调用后续的中间件。 ### 常见中间件及执行顺序[#](https://www.cnblogs.com/boise/p/18002742#%E5%B8%B8%E8%A7%81%E4%B8%AD%E9%97%B4%E4%BB%B6%E5%8F%8A%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F) 中间件一般摆放的顺序即完整请求处理管道: 1. `ExceptionHandler`:异常处理 2. `UseHSTS`:https 3. `UseHttpsRedirection`:重定向 4. `UseStaticFiles`:静态文件 5. `UseRouting`:路由,因为 6.0,应用继承了 6. `UseCORS`:跨域 7. `UseAuthentication`:身份认证 8. `UseAuthorization`:鉴权 9. 终端中间件 并非所有中间件都完全按照此顺序出现,但许多中间件都会遵循此顺序。 例如 * `UseCors`、`UseAuthentication` 和 `UseAuthorization` 必须按显示的顺序出现。 * `UseCors` 当前必须在 `UseResponseCaching` 之前出现。 * `UseRequestLocalization` 必须在可能检查请求区域性的任何中间件(例如 `app.UseMvcWithDefaultRoute()`)之前出现。 * 如果显示调用了 `UseRouting()`,则后面必须紧跟 `UseEndpoint()` 中间件 * 应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。 想要了解更多内置中间件,[官网]([ASP.NET Core 中间件 | Microsoft Learn](https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0#built-in-middleware-1)) 中间件的顺序对于安全性、性能和功能至关重要。 在我们的 `Program.cs` 文件中,中间件执行的顺序就是以 `app.UseXXX()` 拓展方法从上至下执行,即 Request 请求,在到达终端中间件后,按原路返回,即从下至上执行则是 Respones 响应。所以每个中间件摆放的位置和,会影响 http 请求执行的顺序。 举个例子,一般我们使用 `app.UseStaticFiles()` 静态文件中间件会放在 `app.UseAuthentication()` 身份认证中间件前面,这样代表是公开访问。所以当请求需要在访问一些系统的一些静态文件的时候不会被拦截,比如系统的 favicon 图标、js 文件等。 ## 中间件运行原理[#](https://www.cnblogs.com/boise/p/18002742#%E4%B8%AD%E9%97%B4%E4%BB%B6%E8%BF%90%E8%A1%8C%E5%8E%9F%E7%90%86) ### Fun<RequestDelegate,RequestDelegate> 委托[#](https://www.cnblogs.com/boise/p/18002742#funrequestdelegaterequestdelegate%E5%A7%94%E6%89%98) 单纯看类型就知道这是一个委托,传入下一个 RequestDelegate 委托、返回当前 RequestDelegate 委托处理的结果。 #### RequestDelegate 委托[#](https://www.cnblogs.com/boise/p/18002742#requestdelegate%E5%A7%94%E6%89%98) 微软的官方解释是 > 一个可以处理 HTTP 请求的函数。 在 UseMiddleware()拓展方法中,我们自定义的中间件会被包装成为这个委托添加到管道中。 *RequestDelegate 源码* ```C# using System.Threading.Tasks; namespace Microsoft.AspNetCore.Http { /// <summary> /// A一个可以处理 HTTP 请求的函数。 /// </summary> /// <param name="context">The <see cref="HttpContext"/> 请求</param> /// <returns>表示请求处理完成的任务</returns> public delegate Task RequestDelegate(HttpContext context); } ``` ### IApplicationBuilder 接口[#](https://www.cnblogs.com/boise/p/18002742#iapplicationbuilder%E6%8E%A5%E5%8F%A3) 通过 IApplicationBuilder 接口,注册中间件,然后会在 app.Run()的时候调用 WebApplication.BuildRequestDelegation()方法(这个方法本质上就是调用的 ApplicationBuilder.Builder()方法)把注册的中间件串联一起来作为请求管道。 这个接口有一个实现类 ApplicationBuilder。 我们在应用的章节就知道了 WebApplication 类继承了这个接口,所以我们可以直接在应用上注册中间件。 **关键的属性和方法** * 有一个存储请求中间件的列表 * 有一个 Use()方法用于把中间件添加到列表中 * 有一个 New()方法用来创造 ApplicationBuilder,也就是管道分支 * 有一个 Builder()方法用于执行添加的中间件,并返回处理好的结果 *ApplicationBuilder 实现类源码* ```c# public class ApplicationBuilder : IApplicationBuilder { // 用于存储委托的列表 private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new(); // 从ApplicationBuilder获取一组属性 public IDictionary<string, object?> Properties { get; } // 添加委托到管道中 public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) { _components.Add(middleware); return this; } // 创造当前的管道,也是管道分支。 public IApplicationBuilder New() { return new ApplicationBuilder(this); } // 用于执行添加的中间件,并返回处理好的结果,WebApplication.BuildRequestDelegation()本质上就是调用的这个方法来创建中间件委托 public RequestDelegate Build() { // 注册一个中间件,默认请求上下文的结果是404 RequestDelegate app = context => { // 如果我们到达管道的尽头,但我们有一个端点,那么就会发生一些意想不到的事情。 // 如果用户代码设置了终结点,但他们忘记添加 UseEndpoint 中间件,则可能会发生这种情况。 var endpoint = context.GetEndpoint(); var endpointRequestDelegate = endpoint?.RequestDelegate; // 判断有无设置终结点,如果没有设置终结点,则报错 if (endpointRequestDelegate != null) { var message = $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " + $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " + $"routing."; throw new InvalidOperationException(message); } // 默认先设置请求的上下文的状态是404 context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; // 这里循环需要注意的是。执行从列表索引的最后一个开始循环的,也就是默认把委托列表倒序执行,所以这里最先开始执行的是我们注册的最后一个中间件 // 第一次的时候,先把最上面的定义的那个404中间件传入进去,获取最后一个中间件的结果。 // 第二次的时候,把最后一个中间件的结果,放到倒数第二个中间件中,并获得执行的结果。 // 第三次的时候,把倒数二个中间件的结果,放到倒数第三个中间件中,并获得结果。 // 直到执行到第一个中间件。最终返回注册的第一个中间件的返回值。 for (var c = _components.Count - 1; c >= 0; c--) { app = _components[c](app); } return app; } } ``` ## 自定义中间件[#](https://www.cnblogs.com/boise/p/18002742#%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%AD%E9%97%B4%E4%BB%B6) 有 2 种方式: 1. 继承 IMiddleware 接口需要满足的条件: 注册实现的中间件类,**必须通过依赖注入到容器中,否则会报错**,原因是,在 UseMiddleware()的时候,这种方式实现的中间件类,会调用内部的 UseMiddlewareInterface()从依赖注入容器中获取服务实例。 2. 自定义中间件需要满足的条件: 必须是 public 修饰,含有一个参数为 RequestDelegate 类型的构造函数, 必须包含一个 public 修饰的,名称为 Invoke 或者 InvokeAsync 的实例异步方法,且返回类型为 Task,包含一个类型为 HttpContext 的参数。 无需注入,会通过反射创造实例. *自定义容错中间件 demo* ```C# // 这个自定义中间件的作用是,当我们的程序在请求过程中抛出了一些内部异常,提示500,则可以捕捉并进行友好捕捉返回。 public class ErrorMiddleware { public readonly RequestDelegate _next; // 构造函数会尝试从依赖注入容器中获取服务 public ErrorMiddleware (RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { try { // 把请求传递给下一个中间件 awite _next.Invoke(context); } catch(Exception ex) { // 这里可以执行记录日志等操作 // 日志记录方法。。。。。 // 友好包装 // 调用ConfigureAwait,可以在异步执行的时候不捕获上下文,意思是如果你的异步方法后不涉及上下文操作,调用这个可以减少性能开销 await WriteExceptionAsync(context, ex).ConfigureAwait(false); } } /// <summary> /// 友好返回处理 /// </summary> /// <param name="context"></param> /// <param name="exception"></param> /// <returns></returns> private async Task WriteExceptionAsync((HttpContext context, Exception exception) { if (exception.IsNull()) return; // 自定义友好返回类 ServiceResult result = new ServiceResult(); // 返回友好的提示 HttpResponse response = context.Response; response.ContentType = context.Request.Headers["Accept"]; response.ContentType = "application/json"; result.IsFailure(exception.Message); // 写入返回信息中 await response.WriteAsync(result.ToJson<ServiceResult>()).ConfigureAwait(false); } } ``` ## 注册自定义中间件[#](https://www.cnblogs.com/boise/p/18002742#%E6%B3%A8%E5%86%8C%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%AD%E9%97%B4%E4%BB%B6) 因为定义的中间件有 2 种方式,一个是通过继承 IMiddlewar 接口实现,一个是按照约定格式实现的。他们都是通过使用的 ApplicationBuilder 的拓展方法 UseMiddleware()方法来把中间件注册到管道中。 不过在 UseMiddleware()方法内部实现其实是 2 种: * 通过接口实现的,会调用私有的 UseMiddlewareInterface()方法。 * 按照约定格式实现的,调用 UseMiddleware()方法。 **他们的区别就在** * UseMiddlewareInterface()方法会从依赖注入容器中获取服务实例(毕竟是通过接口实现的)然后调用 InvokeAsync()方法, * UseMiddleware()则是通过反射创造中间件委托表达式,然后调用执行返回。不过无论是接口实现还是约定实现,都可以调用 UseMiddleware()方法来把中间件注册到管道中。 **注意!!!!!** * 如果是手写中间件,则 Invoke()方法的参数不要带 requestdelegation。否则会产生依赖注入容器报错, * 如果继承 IMiddleware,内部会调用 UseMiddlewareInterface()从依赖注入容器中获取实例,如果使用了 AutoFac 替换掉了依赖注入容器,那么就需要手动注册, ### UseMiddleware[#](https://www.cnblogs.com/boise/p/18002742#usemiddleware) *调用中间件* ```C# app.UseMiddleware<ErrorMiddleware>(); ``` *UseMiddleware 源码* ```c# public static class UseMiddlewareExtensions { // 这里就是为什么即使没有继承IMiddleware接口,方法可以定义为Invoke的原因。 internal const string InvokeMethodName = "Invoke"; internal const string InvokeAsyncMethodName = "InvokeAsync"; private static readonly MethodInfo GetServiceInfo = typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!; // 我们将把所有公共构造函数和公共方法保留在中间件上 private const DynamicallyAccessedMemberTypes MiddlewareAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods; /// <summary> /// 添加中间件到管道里面 /// </summary> /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> /// <param name="middleware">中间件</param> /// <param name="args">要传递给中间件类型实例的构造函数的参数。</param> public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)]TMiddleware>(this IApplicationBuilder app, params object?[] args) { return app.UseMiddleware(typeof(TMiddleware), args); } /// <summary> /// 自定义中间件类,添加中间件到管道里面 /// </summary> /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> /// <param name="middleware">中间件</param> /// <param name="args">要传递给中间件类型实例的构造函数的参数。</param> public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args) { // 如果是继承IMiddleware实现的。 if (typeof(IMiddleware).IsAssignableFrom(middleware)) { // 如果继承的IMiddleware,不允许从构造函数中传递参数 if (args.Length > 0) { throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware))); } // 从下方的UseMiddlewareInterface工厂来生成委托 return UseMiddlewareInterface(app, middleware); } // 否则进入自定义中间件的解析流程 // 获取依赖注入容器 var applicationServices = app.ApplicationServices; // 添加到委托链表中 return app.Use(next => { // 对自定义的中间件进行解析判断。 // 获取定义的中间件里面的方法 var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); // 判断方法列表里面有无定义Invoke或者InvokeAsync的方法 var invokeMethods = methods.Where(m => string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal) || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal) ).ToArray(); // 如果定义Invoke或者InvokeAsync超过1个则抛出异常 if (invokeMethods.Length > 1) { throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName)); } // 如果没有定义Invoke或者InvokeAsync方法则抛出异常 if (invokeMethods.Length == 0) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware)); } // 获取定义好的第一个Invoke或者InvokeAsync方法 var methodInfo = invokeMethods[0]; // 如果这个方法的返回值不是Task则抛出异常 if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task))); } // 获取方法的参数列表 var parameters = methodInfo.GetParameters(); // 如果没有定义参数,或者第一个参数不是HttpContext则抛出异常 if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext))); } // 获取构造函数中传递的参数 var ctorArgs = new object[args.Length + 1]; // 默认第一个参数是 ctorArgs[0] = next; Array.Copy(args, 0, ctorArgs, 1, args.Length); // 创造这个中间件的实例 var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); // 如果参数只有一个,(invoker默认的参数必须有一个HttpContext) if (parameters.Length == 1) { // 则根据该中间件实例创造一个委托 return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance); } // 如果方法参数超过1个,则代表有自己依赖注入的方法。 // 把这个Invoke或者InvokeAsync方法编译成一个可以执行的Lambda表达式委托 var factory = Compile<object>(methodInfo, parameters); return context => { // 获取依赖注入容器 var serviceProvider = context.RequestServices ?? applicationServices; // 如果依赖注入容器为空,抛出异常 if (serviceProvider == null) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider))); } // 调用并返回这个委托结果 return factory(instance, context, serviceProvider); }; }); } /// <summary> /// 如果继承的是IMiddleware的中间件,则调用的此方法添加中间件到管道里面 /// </summary> /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param> /// <param name="middlewareType">中间件类型</param> private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType) { // 往中间件列表中添加一个委托 return app.Use(next => { // 解析 return async context => { // 从依赖注入容器中获取IMiddlewareFactory工厂 // context.RequestServices.GetService,我们之前在依赖注入章节讲过,调用这个方法其实就是向WebApplication.ServiceProvider获取服务。 var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory)); // 如果没有实现这个工厂,则报错 if (middlewareFactory == null) { // No middleware factory throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory))); } // 根据这个工厂创造中间件的实例 var middleware = middlewareFactory.Create(middlewareType); // 创造失败 if (middleware == null) { // 抛出异常 // The factory returned null, it's a broken implementation throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType)); } try { // 调用实现的InvokeAsync方法 await middleware.InvokeAsync(context, next); } finally { // 释放中间件 middlewareFactory.Release(middleware); } }; }); } } ``` ## 管道分支[#](https://www.cnblogs.com/boise/p/18002742#%E7%AE%A1%E9%81%93%E5%88%86%E6%94%AF) 什么是管道分支,按照正常的请求,我们每个请求都是走的同一个管道,同一个模板的处理方式,那么如果我们需要对一个请求,比如有一个关于处理用户信息请求,在到了主管道的特定中间件,我需要对于这个请求做一些特殊处理,就可以用到管道分支这个操作。 ### Map[#](https://www.cnblogs.com/boise/p/18002742#map) 特点:使用拓展方法 `app.Map("匹配的路由",处理方法)`:根据请求路径 `HttpRequest.Path` 中的值匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。分支内还可以嵌套分支。**当请求进入到管道分支后,执行完成不会再回到主管道!!!!,所以必须在处理方法中,调用 App.Run()来终结请求。** ### MapWhen[#](https://www.cnblogs.com/boise/p/18002742#mapwhen) 特点:使用拓展方法 `app.MapWhen(context => 表达式,处理方法)`:根据表达式的值来判断是否需要创建管道分支,**当请求进入到管道分支后,执行完成不会再回到主管道!!!!,所以必须在处理方法中,调用 App.Run()来终结请求。** ### UseWhen[#](https://www.cnblogs.com/boise/p/18002742#usewhen) 特点:**会回到主管道** ## 请求是如何进入管道、中间件什么时候被执行[#](https://www.cnblogs.com/boise/p/18002742#%E8%AF%B7%E6%B1%82%E6%98%AF%E5%A6%82%E4%BD%95%E8%BF%9B%E5%85%A5%E7%AE%A1%E9%81%93%E4%B8%AD%E9%97%B4%E4%BB%B6%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E8%A2%AB%E6%89%A7%E8%A1%8C)
个人天使
2025年2月10日 13:58
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码