AspNetCore7.0源码解读之UseMiddleware

前言

本文编写时源码参考github仓库主分支。

aspnetcore提供了Use方法供开发者自定义中间件,该方法接收一个委托对象,该委托接收一个RequestDelegate对象,并返回一个RequestDelegate对象,方法定义如下:

IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);

委托RequestDelegate的定义

/// <summary>
/// A function that can process an HTTP request.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> for the request.</param>
/// <returns>A task that represents the completion of request processing.</returns>
public delegate Task RequestDelegate(HttpContext context);

如果我们直接使用IApplicationBuilder.Use来写中间件逻辑,可以使用lamda表达式来简化代码,如下:

app.Use((RequestDelegate next) =>
{return (HttpContext ctx) =>{// do your logicreturn next(ctx);};
});

如果写一些简单的逻辑,这种方式最为方便,问题是如果需要写的中间件代码比较多,依然这样去写,会导致我们Program.cs文件代码非常多,如果有多个中间件,那么最后我们的的Program.cs文件包含多个中间件代码,看上去十分混乱。

将中间件逻辑独立出来

为了解决我们上面的代码不优雅,我们希望能将每个中间件业务独立成一个文件,多个中间件代码不混乱的搞到一起。我们需要这样做。

单独的中间件文件

// Middleware1.cs
public class Middleware1
{public static RequestDelegate Logic(RequestDelegate requestDelegate){return (HttpContext ctx) =>{// do your logicreturn requestDelegate(ctx);};}
}

调用中间件

app.Use(Middleware1.Logic);
// 以下是其他中间件示例
app.Use(Middleware2.Logic);
app.Use(Middleware3.Logic);
app.Use(Middleware4.Logic);

这种方式可以很好的将各个中间件逻辑独立出来,Program.cs此时变得十分简洁,然而我们还不满足这样,因为我们的Logic方法中直接返回一个lamada表达式(RequestDelegate对象),代码层级深了一层,每个中间件都多写这一层壳似乎不太优雅,能不能去掉这层lamada表达式呢?

UseMiddlewareExtensions

为了解决上面提到的痛点,UseMiddlewareExtensions扩展类应运而生,它在Aspnetcore底层大量使用,它主要提供一个泛型UseMiddleware<T>方法用来方便我们注册中间件,下面是该方法的定义

public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object?[] args)

如果只看这个方法的声明,估计没人知道如何使用,因为该方法接收的泛型参数TMiddleware没有添加任何限制,而另一个args参数也是object类型,而且是可以不传的,也就是它只需要传任意一个类型都不会在编译时报错。 比如这样,完全不会报错:

d5dbc8edcf78500e0c3c8b71cec5b6b8.png当然,如果你这样就运行程序,一定会收到下面的异常

System.InvalidOperationException:“No public 'Invoke' or 'InvokeAsync' method found for middleware of type 'System.String'.”

提示我们传的类型没有InvokeInvokeAsync公共方法,这里大概能猜到,底层应该是通过反射进行动态调用InvokeInvokeAsync公共方法的。

源码分析

想要知道其本质,唯有查看源码,以下源码来自UseMiddlewareExtensions

如下,该扩展类一共提供两个并且是重载的公共方法UseMiddleware,一般都只会使用第一个UseMiddleware,第一个UseMiddleware方法内部再去调用第二个UseMiddleware方法,源码中对类型前面添加的[DynamicallyAccessedMembers(MiddlewareAccessibility)]属性可以忽略,它的作用是为了告诉编译器我们通过反射访问的范围,以防止对程序集对我们可能调用的方法或属性等进行裁剪。

internal const string InvokeMethodName = "Invoke";
internal const string InvokeAsyncMethodName = "InvokeAsync";/// <summary>
/// Adds a middleware type to the application's request pipeline.
/// </summary>
/// <typeparam name="TMiddleware">The middleware type.</typeparam>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="args">The arguments to pass to the middleware type instance's constructor.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)] TMiddleware>(this IApplicationBuilder app, params object?[] args)
{return app.UseMiddleware(typeof(TMiddleware), args);
}/// <summary>
/// Adds a middleware type to the application's request pipeline.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="middleware">The middleware type.</param>
/// <param name="args">The arguments to pass to the middleware type instance's constructor.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app,[DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware,params object?[] args)
{if (typeof(IMiddleware).IsAssignableFrom(middleware)){// IMiddleware doesn't support passing args directly since it's// activated from the containerif (args.Length > 0){throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));}return UseMiddlewareInterface(app, middleware);}var applicationServices = app.ApplicationServices;var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);MethodInfo? invokeMethod = null;foreach (var method in methods){if (string.Equals(method.Name, InvokeMethodName, StringComparison.Ordinal) || string.Equals(method.Name, InvokeAsyncMethodName, StringComparison.Ordinal)){if (invokeMethod is not null){throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));}invokeMethod = method;}}if (invokeMethod is null){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));}if (!typeof(Task).IsAssignableFrom(invokeMethod.ReturnType)){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));}var parameters = invokeMethod.GetParameters();if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));}var state = new InvokeMiddlewareState(middleware);return app.Use(next =>{var middleware = state.Middleware;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);if (parameters.Length == 1){return (RequestDelegate)invokeMethod.CreateDelegate(typeof(RequestDelegate), instance);}var factory = Compile<object>(invokeMethod, parameters);return context =>{var serviceProvider = context.RequestServices ?? applicationServices;if (serviceProvider == null){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));}return factory(instance, context, serviceProvider);};});
}

第一个UseMiddleware可以直接跳过,看第二个UseMiddleware方法,该方法一上来就先判断我们传的泛型类型是不是IMiddleware接口的派生类,如果是,直接交给UseMiddlewareInterface方法。

if (typeof(IMiddleware).IsAssignableFrom(middleware)){// IMiddleware doesn't support passing args directly since it's// activated from the containerif (args.Length > 0){throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));}return UseMiddlewareInterface(app, middleware);}

这里总算看到应该有的东西了,如果声明UseMiddleware<T>方法时,对泛型T添加IMiddleware限制,我们不看源码就知道如何编写我们的中间件逻辑了,只需要写一个类,继承IMiddleware并实现InvokeAsync方法即可, UseMiddlewareInterface方法的实现比较简单,因为我们继承了接口,逻辑相对会简单点。

private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app,Type middlewareType)
{return app.Use(next =>{return async context =>{var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));if (middlewareFactory == null){// No middleware factorythrow new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)));}var middleware = middlewareFactory.Create(middlewareType);if (middleware == null){// The factory returned null, it's a broken implementationthrow new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));}try{await middleware.InvokeAsync(context, next);}finally{middlewareFactory.Release(middleware);}};});
}
public interface IMiddleware
{/// <summary>/// Request handling method./// </summary>/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>/// <param name="next">The delegate representing the remaining middleware in the request pipeline.</param>/// <returns>A <see cref="Task"/> that represents the execution of this middleware.</returns>Task InvokeAsync(HttpContext context, RequestDelegate next);
}

如果我们的类不满足IMiddleware,继续往下看

通过反射查找泛型类中InvokeInvokeAsync方法

var applicationServices = app.ApplicationServices;
var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
MethodInfo? invokeMethod = null;
foreach (var method in methods)
{if (string.Equals(method.Name, InvokeMethodName, StringComparison.Ordinal) || string.Equals(method.Name, InvokeAsyncMethodName, StringComparison.Ordinal)){// 如果Invoke和InvokeAsync同时存在,则抛出异常,也就是,我们只能二选一if (invokeMethod is not null){throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));}invokeMethod = method;}
}// 如果找不到Invoke和InvokeAsync则抛出异常,上文提到的那个异常。
if (invokeMethod is null)
{throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
}// 如果Invoke和InvokeAsync方法的返回值不是Task或Task的派生类,则抛出异常
if (!typeof(Task).IsAssignableFrom(invokeMethod.ReturnType))
{throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
}
Snippet// 如果Invoke和InvokeAsync方法没有参数,或第一个参数不是HttpContext,抛异常
var parameters = invokeMethod.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
{throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
}

上面一堆逻辑主要就是检查我们的InvokeInvokeAsync方法是否符合要求,即:必须是接收HttpContext参数,返回Task对象,这恰好就是委托RequestDelegate的定义。

构造RequestDelegate这部分源码的解读都注释到相应的位置了,如下

var state = new InvokeMiddlewareState(middleware);
// 调用Use函数,向管道中注册中间件
return app.Use(next =>
{var middleware = state.Middleware;var ctorArgs = new object[args.Length + 1];// next是RequestDelegate对象,作为构造函数的第一个参数传入ctorArgs[0] = next;Array.Copy(args, 0, ctorArgs, 1, args.Length);// 反射实例化我们传入的泛型类,并把next和args作为构造函数的参数传入var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);// 如果我们的Invoke方法只有一个参数,则直接创建该方法的委托if (parameters.Length == 1){return (RequestDelegate)invokeMethod.CreateDelegate(typeof(RequestDelegate), instance);}// 当Invoke方法不止一个参数HttpContext,通过Compile函数创建动态表达式目录树,// 表达式目录树的构造此处略过,其目的是实现将除第一个参数的其他参数通过IOC注入var factory = Compile<object>(invokeMethod, parameters);return context =>{// 获取serviceProvider用于在上面构造的表达式目录树中实现依赖注入var serviceProvider = context.RequestServices ?? applicationServices;if (serviceProvider == null){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));}// 将所需的参数传入构造的表达式目录树工厂return factory(instance, context, serviceProvider);};
});

至此,整个扩展类的源码就解读完了。

通过UseMiddleware注入自定义中间件

通过上面的源码解读,我们知道了其实我们传入的泛型类型是有严格的要求的,主要有两种

通过继承IMiddleware

继承IMiddleware并实现该接口的InvokeAsync函数

public class Middleware1 : IMiddleware
{public async Task InvokeAsync(HttpContext context, RequestDelegate next){// do your logicawait next(context);}
}

通过反射

我们知道,在不继承IMiddleware的情况下,底层会通过反射实例化泛型类型,并通过构造函数传入RequestDelegate,而且要有一个公共函数InvokeInvokeAsync,并且接收的第一个参数是HttpContext,返回Task,根据要求我们将Middleware1.cs改造如下

public class Middleware1
{RequestDelegate next;public Middleware1(RequestDelegate next){this.next = next;}public async Task Invoke(HttpContext httpContext){// do your logicawait this.next(httpContext);}
}

总结

通过源码的学习,我们弄清楚底层注册中间件的来龙去脉,两种方式根据自己习惯进行使用,笔者认为通过接口的方式更加简洁直观简单,并且省去了反射带来的性能损失,推荐使用。既然通过继承接口那么爽,为啥还费那么大劲实现反射的方式呢?由源码可知,如果继承接口的话,就不能进行动态传参了。

if (typeof(IMiddleware).IsAssignableFrom(middleware)){// IMiddleware doesn't support passing args directly since it's// activated from the containerif (args.Length > 0){throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));}return UseMiddlewareInterface(app, middleware);}

所以在需要传参的场景,则必须使用反射的方式,所以两种方式都有其存在的必要。

如果本文对您有帮助,还请点赞转发关注一波支持作者。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/287816.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《零基础看得懂的C语言入门教程 》——(五)C语言的变量、常量及运算

一、学习目标 了解C语言变量的其它创建方式了解C语言常量了解C语言的运算符 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误区 第二篇&#xff1a;&#xff08;二&#xff…

实战使用Axure设计App,使用WebStorm开发(4) – 实现页面UI

系列文章 实战使用Axure设计App,使用WebStorm开发(1) – 用Axure描述需求 实战使用Axure设计App,使用WebStorm开发(2) – 创建 Ionic 项目 实战使用Axure设计App,使用WebStorm开发(3) – 构建页面架构 实战使用Axure设计App,使用WebStorm开发(4) – 实现页面UI 实战使用Axu…

英文词频统计预备,组合数据类型练习

实例: 下载一首英文的歌词或文章&#xff0c;将所有,.&#xff1f;&#xff01;等替换为空格&#xff0c;将所有大写转换为小写&#xff0c;统计某几个单词出现的次数&#xff0c;分隔出一个一个的单词。2.列表实例&#xff1a;由字符串创建一个作业评分列表&#xff0c;做增删…

《零基础看得懂的C语言入门教程 》——(六)轻轻松松了解C语言的逻辑运算

一、学习目标 了解逻辑判断的概念了解if语句的使用方法了解switch语句的使用方法了解逻辑运算符的使用方法 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误区 第二篇&#…

TCP之滑动窗口

一、滑动窗口的基本知识 TCP滑动窗口包含了发送窗口和接收窗口 1)、TCP滑动窗口的最大值 TCP数据包头部里面有个窗口值,默认窗口是一个16bit位字段,表示窗口的字节容量,所以TCP滑动窗口的最大值是2^16-1=65535个字节,TCP里面也有窗口扩大因子可把原来16bit的窗口,扩大为…

《零基础看得懂的C语言入门教程 》——(七)C语言的循环分分钟上手

一、学习目标 了解循环的使用方法 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误区 第二篇&#xff1a;&#xff08;二&#xff09;C语言没那么难简单开发带你了解流程 第…

ArcGIS中数据存放相对路径和绝对路径的区别

配套蓝光视频教程:【ArcGIS风暴】数据相对路径VS绝对路径 问题举例: 菜鸟们在使用ArcGIS时经常会碰到将地图文档(.mxd)拷贝到别的电脑上或改变一个路径时,出现数据丢失的现象,具体表现为图层前面出现一个红色的感叹号,如下图所示。 出现以上问题的根本原因是数据GSS.ti…

TIOBE 5 月编程语言排行榜:C# 最受开发者欢迎,C++ 将冲击 Top 3

技术迭代的速度越来越快&#xff0c;这一点在每月更新一次的编程语言排行榜榜单中体现得尤为明显。今天&#xff0c;最新的 TIOBE 5 月编程语言榜单出炉&#xff0c;不妨一起来看一下又有哪些新的趋势。C# 的使用量增幅最高&#xff0c;C 或将冲击 Top 3和 4 月相比&#xff0c…

SQL Server2016导出数据表数据

SQL Server2016导出数据表数据我们前面已经介绍了很多关于SQL Server的相关文章&#xff0c;今天我们主要介绍的是&#xff0c;如何导出数据库下表中数据。我们所有的操作都是通过SSMS进行操作的。我们右击需要导出数据的数据库----任务----导出数据根据向导提示&#xff0c;下…

Jfinal 显示欢迎页 index.jsp

为什么80%的码农都做不了架构师&#xff1f;>>> IndexController.index()方法&#xff0c;为什么是index()方法&#xff1f;其实这是一个约定 那么它是如何打开index.jsp文件的呢&#xff1f;我们来查看index()方法的代码&#xff1a; public class IndexControlle…

【经典珍藏版】手把手全程教你制作漂亮的720全景地图(附PtGui软件下载地址)

如今,在网络异常发达的信息与智能测绘时代,我们可以在手机、笔记本电脑等多种设备上随时随地看到很漂亮的全景照片,仰以观于天文,俯以察于地理,可以全景图片视频拍摄,可以任意放大缩小、漫游、重力感应、VR眼睛虚拟体验等等,其乐无穷。作为一个GISer,采集地理信息,探索…

一篇文带你从0到1了解建站及完成CMS系统编写

学习目标 了解搭建一般网站的简便方式了解最原始一般站点搭建了解内容管理站点搭建了解权限设计及完成了解使用设计模式减少代码冗余了解前端拖拽页面生成及生成了解自定义数据的创建了解动态生成的前端页如何绑定自定义数据 开发环境 Windows7 *64 SP1php5.6apache/nginxth…

判断输入的整数是否为素数_C语言 | 判断是否素数

“要成为绝世高手&#xff0c;并非一朝一夕&#xff0c;除非是天生武学奇才&#xff0c;但是这种人…万中无一”——包租婆这道理放在C语言学习上也一并受用。在编程方面有着天赋异禀的人毕竟是少数&#xff0c;我们大多数人想要从C语言小白进阶到高手&#xff0c;需要经历的是…

『技术群里聊些啥』Task 不是你想 Cancel,想 Cancel 就能 Cancel

前言在群里看到有人问如何取消这个 Task 的执行&#xff1a;实际上这并不会取消S1eepMode1方法的执行&#xff1a;这是为什么呢&#xff1f;原因首先&#xff0c;让我们看看s_cts.Cancel()都做了啥&#xff1a;public void Cancel() > Cancel(false);public void Cancel(boo…

《零基础看得懂的C语言入门教程 》——(八)了解基本数组还不是那么简单

一、学习目标 了解数组的使用方法 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误区 第二篇&#xff1a;&#xff08;二&#xff09;C语言没那么难简单开发带你了解流程 第…

推导坐标旋转公式(转)

在《Flash actionScript 3.0 动画教程》一书中有一个旋转公式&#xff1a; x1cos(angle)*x-sin(angle)*y; y1cos(angle)*ysin(angle)*x; 其中x&#xff0c;y表示物体相对于旋转点旋转angle的角度之前的坐标&#xff0c;x1&#xff0c;y1表示物体旋转angle后相对于旋转点的坐标 …

任务管理平台_jytask一个任务调度统一管理平台

task介绍和使用https://gitee.com/yuejing/task 下的文档&#xff1a;[doc/task介绍和使用.docx]task是什么&#xff1f;task是一个任务调度统一管理平台。 目前主要是通过http来进行任务的调度&#xff0c;http支持签名算法。一张图能更加懂它是做什么的(一个集中管理任务的平…

设计一个支持百万用户的系统

设计一个支持数百万用户的系统是非常有挑战性的, 这是一个需要不断调整和优化的过程, 接下来的内容中, 我将构建一个系统, 从单个用户开始&#xff0c;到最后支持数百万的用户。从单个服务开始 千里之行&#xff0c;始于足下&#xff0c;让我们从最简单的单个服务开始。所有的…

《零基础看得懂的C语言入门教程 》——(九)C语言二维数组与循环嵌套

一、学习目标 了解二维数组的使用方法了解循环嵌套的使用方法 目录 C语言真的很难吗&#xff1f;那是你没看这张图&#xff0c;化整为零轻松学习C语言。 第一篇&#xff1a;&#xff08;一&#xff09;脱离学习误区 第二篇&#xff1a;&#xff08;二&#xff09;C语言没那么…

Scala-2.13.0 安装及配置

Scala 简介 Scala 是一门多范式&#xff08;multi-paradigm&#xff09;的编程语言&#xff0c;设计初衷是要集成面向对象编程和函数式编程的各种特性。 Scala 运行在Java虚拟机上&#xff0c;并兼容现有的Java程序。 Scala 源代码被编译成Java字节码&#xff0c;所以它可以运…