网站规则,网站建设与管理需要什么软件有哪些,网页设计培训心得体会,网站导航条怎么做前言在日常使用ASP.NET Core开发的过程中我们多多少少会设计到使用中间件的场景#xff0c;ASP.NET Core默认也为我们内置了许多的中间件#xff0c;甚至有时候我们需要自定义中间件来帮我们处理一些请求管道过程中的处理。接下来#xff0c;我们将围绕着以下几个问题来简单… 前言 在日常使用ASP.NET Core开发的过程中我们多多少少会设计到使用中间件的场景ASP.NET Core默认也为我们内置了许多的中间件甚至有时候我们需要自定义中间件来帮我们处理一些请求管道过程中的处理。接下来我们将围绕着以下几个问题来简单探究一下关于ASP.NET Core中间件是如何初始化的首先使用UseMiddleware注册自定义中间件和直接Use的方式有何不同其次使用基于约定的方式定义中间件和使用实现IMiddleware接口的方式定义中间件有何不同再次使用基于约定的方式自定义中间件的究竟是如何约束我们编写的类和方法格式的最后使用约定的方式定义中间件通过构造注入和通过Invoke方法注入的方式有何不同接下来我们将围绕这几个核心点来逐步探究关于ASP.NET Core关于中间件初始化的神秘面纱来指导我们以后使用它的时候需要有注意点来减少踩坑的次数。自定义的方式使用自定义中间件的方式有好几种咱们简单来演示一下三种比较常用方式。Use方式首先也是最直接最简单的使用Use的方式比如app.Use(async (context, next)
{var endpoint context.Features.GetIEndpointFeature()?.Endpoint;if (endpoint ! null){ResponseCacheAttribute responseCache endpoint.Metadata.GetMetadataResponseCacheAttribute();if (responseCache ! null){//做一些事情}}await next();
});
基于约定的方式然后使用UseMiddleware也是我们比较常用的一种方式这种方式使用起来相对于第一种来说虽然使用起来可能会稍微繁琐一点毕竟需要定义一个类但是更好的符合符合面向对象的封装思想它的使用方式大致如下首先定义一个Middleware的类public class RequestCultureMiddleware
{private readonly RequestDelegate _next;public RequestCultureMiddleware(RequestDelegate next){_next next;}public async Task InvokeAsync(HttpContext context){var cultureQuery context.Request.Query[culture];if (!string.IsNullOrWhiteSpace(cultureQuery)){var culture new CultureInfo(cultureQuery);CultureInfo.CurrentCulture culture;CultureInfo.CurrentUICulture culture;}await _next(context);}
}
编写完成之后需要手动的将类注册到管道中才能生效注册方式如下所示app.UseMiddlewareRequestCultureMiddleware();
实现IMiddleware的方式还有一种方式是实现IMiddleware接口的方式这种方式比如前两种方式常用但是也确确实实的存在于ASP.NET Core中既然存在也就有它存在的理由我们也可以探究一下它的使用方式也是需要自定义一个类去实现IMiddleware接口如下所示public class RequestCultureOtherMiddleware:IMiddleware
{public async Task InvokeAsync(HttpContext context, RequestDelegate next){var cultureQuery context.Request.Query[culture];if (!string.IsNullOrWhiteSpace(cultureQuery)){var culture new CultureInfo(cultureQuery);CultureInfo.CurrentCulture culture;CultureInfo.CurrentUICulture culture;}await next(context);}
}
这种方式和第二种方式略有不同需要手动将中间件注册到容器中,至于声明周期也没做特殊要求可以直接注册为单例模式services.AddSingletonIMiddleware,RequestCultureOtherMiddleware();
完成上步操作之后同样也需要将其注册到管道中去app.UseMiddlewareRequestCultureOtherMiddleware();
这种方式相对于第二种方式的主要区别在于灵活性方面的差异它实现了IMiddleware接口那就要受到IMiddleware接口的约束也就是我们常说的里氏代换原则首先我们可以先来看下IMiddleware接口的定义[点击查看源码????]public interface IMiddleware
{/// summary/// 请求处理方法/// /summary/// param namecontext当前请求上下文/param/// param namenext请求管道中下一个中间件的委托/paramTask InvokeAsync (HttpContext context, RequestDelegate next);
}
通过这个接口也就看出来InvokeAsync只能接受HttpContext和RequestDelegate参数无法定义其他形式的参数也没办法通过注入的方式编写InvokeAsync方法参数说白了就是没有第二种方式灵活受限较大。关于常用的自定义中间件的方式我们就先说到这里我们也知道了如何定义使用中间件。接下来我们就来探讨一下这么多种方式之间到底存在怎样的联系。源码探究上面我们已经演示了关于使用中间件的几种方式那么这么几种使用方式之间有啥联系或区别我们只看到了表面的接下来我们来看一下关于中间件初始化的源码来一探究竟。首先无论那种形式都是基于IApplicationBuilder这个接口扩展而来的所以我们先从这里下手找到源码IApplicationBuilder位置[点击查看源码????]可以看到以下代码/// summary
/// 将中间件委托添加到应用程序的请求管道。
/// /summary
/// param namemiddleware中间件委托/param
/// returnsThe see crefIApplicationBuilder/./returns
IApplicationBuilder Use(FuncRequestDelegate, RequestDelegate middleware);
IApplicationBuilder接口里只有Use的方式可以添加中间件由此我们可以大致猜到两点信息其它添加中间件的方式都是在扩展自IApplicationBuilder并不是IApplicationBuilder本身的方法。其它添加中间件的形式最终都会转换为Use的方式。Use扩展方法上面我们看到了IApplicationBuilder只包含了一个Use方法但是我们日常编程中最常使用到的却并不是这一个而是来自UseExtensions扩展类的Use扩展方法实现如下所示[点击查看源码????]public static IApplicationBuilder Use(this IApplicationBuilder app, FuncHttpContext, FuncTask, Task middleware)
{//将middleware转换为Use(FuncRequestDelegate, RequestDelegate middleware)的形式return app.Use(next {return context {FuncTask simpleNext () next(context);return middleware(context, simpleNext);};});
}
如预料的那样Use的扩展方法最终都会转换为Use(FuncRequestDelegate, RequestDelegate middleware)的形式去执行。Use扩展方法的形式还是比较清晰的毕竟也是基于委托的形式而且参数是固定的。UseMiddleware上面我们看到了Use的扩展方法它最终还是转换为Use(FuncRequestDelegate, RequestDelegate middleware)的形式去执行。接下来我们来看下通过编写类的形式定义中间件会是怎样的转换操作。找到UseMiddleware扩展方法所在的地方也就是UseMiddlewareExtensions扩展类里[点击查看源码????]我们最常用的是UseMiddleware这个方法而且这个方法是UseMiddlewareExtensions扩展类的入口方法[点击查看源码????]说白了就是它是完全调用别的方法没有自己的实现逻辑/// summary
/// 将中间件类型添加到应用程序的请求管道.
/// /summary
/// typeparam nameTMiddleware中间件类型/typeparam
/// param nameargs传递给中间件类型实例的构造函数的参数./param
/// returnsThe see crefIApplicationBuilder/ instance./returns
public static IApplicationBuilder UseMiddleware[DynamicallyAccessedMembers(MiddlewareAccessibility)]TMiddleware(this IApplicationBuilder app, params object[] args)
{ return app.UseMiddleware(typeof(TMiddleware), args);
}
继续向下看找到它调用的扩展方法在展示该方法之前我们先罗列一下该类的常量属性因为类中的方法有用到如下所示internal const string InvokeMethodName Invoke;
internal const string InvokeAsyncMethodName InvokeAsync;
从这里我们可以得到一个信息基于约定的形式自定义的中间件触发方法名可以是Invoke或InvokeAsync继续看执行方法的实现代码public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object[] args)
{//判断自定义的中间件是否是实现了IMiddleware接口if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo())){//Middleware不支持直接传递参数//因为它是注册到容器中的所以不能通过构造函数传递自定义的参数否则抛出异常if (args.Length 0){throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));}//实现IMiddleware接口的中间件走的是这个逻辑咱们待会看return UseMiddlewareInterface(app, middleware);}var applicationServices app.ApplicationServices;return app.Use(next {//获取自定义中间件类的非静态public方法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的方法只能有有一个存在多个话会抛出异常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的方法的返回值类型必须是Task类型否则会抛出异常var methodInfo invokeMethods[0];if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));}//获取Invoke或InvokeAsync方法的参数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];//因为方法数组的首元素是RequestDelegate类型的next//也就是基于约定定义的中间件构造函数的第一个参数是RequestDelegate类型的实例ctorArgs[0] next;Array.Copy(args, 0, ctorArgs, 1, args.Length);//创建基于约定的中间件实例//又看到ActivatorUtilities这个类了关于这个类有兴趣的可以研究一下可以根据容器创建类型实例非常好用var instance ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);//如果Invoke或InvokeAsync方法只有一个参数则直接创建RequestDelegate委托返回if (parameters.Length 1){//RequestDelegate其实就是public delegate Task RequestDelegate(HttpContext context);return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);}//编译Invoke或InvokeAsync方法关于Compile的实现等会咱们再看var factory Compileobject(methodInfo, parameters);//返回这个委托//看着这个委托的格式有点眼熟其实就是RequestDelegate即public delegate Task RequestDelegate(HttpContext context);return context {var serviceProvider context.RequestServices ?? applicationServices;//serviceProvider不能为空,否则没法玩了if (serviceProvider null){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));}//返回委托执行结果return factory(instance, context, serviceProvider);};});
}
这个方法其实是工作的核心方法通过这里可以看出来自定义中间件的大致执行过程。代码中的注释我写的比较详细有兴趣的可以仔细了解一下如果懒得看我们就大致总结一下大致的核心点首先UseMiddleware的本质确实还是执行的Use方法实现IMiddleware接口的中间件走的是独立的处理逻辑而且构造函数传递自定义的参数因为它的数据来自于容器的注入。基于约定定义中间件的情况,即不实现IMiddleware的情况下。①基于约定定义的中间件构造函数的第一个参数需要是RequestDelegate类型②查找方法名可以为Invoke或InvokeAsync且存在而且只能存在一个③Invoke或InvokeAsync方法返回值需为Task且方法的第一个参数必须为HttpContext类型④Invoke或InvokeAsync方法如果只包含HttpContext类型参数则该方法直接转换为RequestDelegate⑤我们之所以可以通过构造注入在中间件中获取服务是因为基于约定的方式是通过ActivatorUtilities类创建的实例通过上面的源码我们了解到了实现IMiddleware接口的方式自定义中间件的方式是单独处理的即在UseMiddlewareInterface方法中[点击查看源码????]接下来我们查看一下该方法的代码private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType)
{return app.Use(next {return async context {var middlewareFactory (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));if (middlewareFactory null){// 没有middlewarefactory直接抛出异常throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)));}//创建middleware实例var middleware middlewareFactory.Create(middlewareType);if (middleware null){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));}try{//执行middleware的InvokeAsync方法await middleware.InvokeAsync(context, next);}finally{//释放middlewaremiddlewareFactory.Release(middleware);}};});
}
通过上面的代码我们可以看到,IMiddleware实例是通过IMiddlewareFactory实例创建而来ASP.NET Core中IMiddlewareFactory默认注册的实现类是MiddlewareFactory接下来我们看下这个类的实现[点击查看源码????]public class MiddlewareFactory : IMiddlewareFactory{ private readonly IServiceProvider _serviceProvider;public MiddlewareFactory(IServiceProvider serviceProvider) { _serviceProvider serviceProvider; }public IMiddleware? Create(Type middlewareType) { //根据类型从容器中获取IMiddleware实例 return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware; }public void Release(IMiddleware middleware) { //因为容器控制了对象的生命周期所以这里啥也没有 }}
好吧其实就是在容器中获取的IMiddleware实例通过这个我们就可以总结出来实现IMiddleware接口的形式创建中间件的操作
需要实现IMiddleware接口来约束中间件的行为方法名只能为InvokeAsync需要手动注册IMiddleware和实现类到容器中生命周期可自行约束如果生命周期为Scope或瞬时那么每次请求都会创建新的中间件实例没办法通过InvokeAsync方法注入服务因为受到了IMiddleware接口的约束上面我们看到了实现IMiddleware接口的方式中间件是如何被初始化的接下来我们继续来看基于约定的方式定义的中间件是如何被初始化的。通过上面我们展示的源码可知实现逻辑在Compile方法中该方法整体实现方式就是基于Expression主要原因个人猜测有两点一个是形式比较灵活能应对的场景较多二是性能稍微比反射好一点。在此之前我们先展示一下Compile方法依赖的操作首先反射是获取UseMiddlewareExtensions类的GetService方法操作private static readonly MethodInfo GetServiceInfo typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!;
其中GetService方法的实现如下所示其实就是在容器ServiceProvider中获取指定类型实例private static object GetService(IServiceProvider sp, Type type, Type middleware)
{var service sp.GetService(type);if (service null){throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware));}return service;
}
好了上面已将Compile外部依赖已经展示出来了接下来我们就可以继续探究Compile方法了[点击查看源码????]private static FuncT, HttpContext, IServiceProvider, Task CompileT(MethodInfo methodInfo, ParameterInfo[] parameters)
{var middleware typeof(T);//构建三个Parameter名为httpContext、serviceProvider、middlewarevar httpContextArg Expression.Parameter(typeof(HttpContext), httpContext);var providerArg Expression.Parameter(typeof(IServiceProvider), serviceProvider);var instanceArg Expression.Parameter(middleware, middleware);//穿件Expression数组且数组第一个参数为httpContextArgvar methodArguments new Expression[parameters.Length];methodArguments[0] httpContextArg;//因为Invoke或InvokeAsync方法第一个参数为HttpContext且methodArguments第一个参数占位所以跳过第一个参数for (int i 1; i parameters.Length; i){//获取方法参数var parameterType parameters[i].ParameterType;//不支持ref类型操作if (parameterType.IsByRef){throw new NotSupportedException(Resources.FormatException_InvokeDoesNotSupportRefOrOutParams(InvokeMethodName));}//构建参数类型表达式即用户构建方法参数的操作var parameterTypeExpression new Expression[]{providerArg,Expression.Constant(parameterType, typeof(Type)),Expression.Constant(methodInfo.DeclaringType, typeof(Type))};//声明调用GetServiceInfo的表达式var getServiceCall Expression.Call(GetServiceInfo, parameterTypeExpression);//将getServiceCall操作转换为parameterTypemethodArguments[i] Expression.Convert(getServiceCall, parameterType);}//获取中间件类型表达式Expression middlewareInstanceArg instanceArg;if (methodInfo.DeclaringType ! null methodInfo.DeclaringType ! typeof(T)){//转换中间件类型表达式类型与声明类型一致middlewareInstanceArg Expression.Convert(middlewareInstanceArg, methodInfo.DeclaringType);}//调用middlewareInstanceArg(即当前中间件)的methodInfo(即获取Invoke或InvokeAsync)方法参数(methodArguments)var body Expression.Call(middlewareInstanceArg, methodInfo, methodArguments);//转换为lambdavar lambda Expression.LambdaFuncT, HttpContext, IServiceProvider, Task(body, instanceArg, httpContextArg, providerArg);return lambda.Compile();
}
上面的代码比较抽象其实主要是因为它是基于表达式树进行各种操作的如果对表达式树比较熟悉的话可能对上面的代码理解起来还好一点如果不熟悉表达式树的话可能理解起来比较困难不过还是建议简单学习一下Expression相关的操作慢慢的发现还是挺有意思的它的性能整体来说比传统的反射性能也会更好一点。其实Compile主要实现的操作转化为我们比较容易理解的代码的话就是下面所示的操作如果我们编写了一个如下的中间件代码public class Middleware
{public Task Invoke(HttpContext context, ILoggerFactory loggerFactory){}
}
那么通过Compile方法将转换为类似以下形式的操作这样说的话可能会好理解一点Task Invoke(Middleware instance, HttpContext httpContext, IServiceProvider provider)
{return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory));
}
通过上面的源码分析我们了解到基于约定的方式定义的中间件实例是通过ActivatorUtilities类创建的而且创建实例是在返回RequestDelegate委托之前IApplicationBuilder的Use方法只会在首次运行的时候执行后续管道串联执行的其实正是它返回的结果RequestDelegate这个委托。但是执行转换Invoke或InvokeAsync方法为执行委托的操作却是在返回的RequestDelegate委托当中也就是我们每次请求管道会处理的逻辑中。这个逻辑可以在IApplicationBuilder默认的实现类ApplicationBuilder类的Build方法中可以得知[点击查看源码????]它的实现逻辑如下所示public RequestDelegate Build()
{//最后的管道处理即请求未能匹配到任何终结点的情况RequestDelegate app context {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;};//_components即我们通过Use添加的中间件foreach (var component in _components.Reverse()){//得到执行结果即RequestDelegateapp component(app);}//返回第一个管道中间件return app;
}
通过上面的代码我们可以清楚的看到管道最终执行的就是执行FuncRequestDelegate, RequestDelegate这个委托的返回结果RequestDelegate。由此得到结论基于约定的中间件形式通构造函数注入的服务实例是和应用程序的生命周期一致的。通过Invoke或InvokeAsync方法注入的服务实例每次请求都会被执行到即生命周期是Scope的。总结 通过本次对源码的研究我们认识到了自定义的ASP.NET Core中间件是如何被初始化的。虽然自定义的中间件的形式有许多种方式但是最终还都是转换为IApplicationBuilder Use(FuncRequestDelegate, RequestDelegate middleware)这种方式。将中间件抽离为独立的类有两种方式即基于约定的方式和实现IMiddleware接口的形式通过分析源码我们也更深刻的了解两种方式的不同之处。基于约定的方式更灵活它的声明周期是单例的但是通过它的Invoke或InvokeAsync方法注入的服务实例生命周期是Scope的。实现IMiddleware接口的方式生命周期取决于自己注册服务实例时候声明的周期而且这种方式没办法通过方法注入服务因为有IMiddleware接口InvokeAsync方法的约束。 当然不仅仅是我们在总结中说的的这些还存在更多的细节这些我们在分析源码的时候都有涉及相信阅读文章比较仔细的同学肯定会注意到这些。阅读源码收获正是这些解决心中的疑问了解更多的细节有助于在实际使用中避免一些不必要的麻烦。本次讲解就到这里愿各位能有所收获。????欢迎扫码关注我的公众号????