ASP.NET Core 中间件(Middleware)(一)
本文主要目标:记录Middleware的运行原理流程,并绘制流程图。
目录结构:
1、运行环境
2、Demo实践
3、源码追踪
4、AspnetCore内置middleware
一、运行环境
Visual Studio Community 2019 版本 16.8.5
.Net Sdk Version: 5.0.103
二、Demo实践
讲解或学习一个东西的时候,最方便的方式是先写一个Demo。基于此,我写以一个中间件的记录请求输出的实践Demo来理解Middleware。
实体:
public class RequestResponseLog { public string Id { get; set; } public DateTime CreateTime { get; set; } public string RequestJson { get; set; } public string ResponseJson { get; set; } } public class Student { public string Id { get; set; } public string Name { get; set; } ////// 学校 /// public string School { get; set; } ////// 班级 /// public string Class { get; set; } ////// 年级 /// public string Grade { get; set; } }
Controller:用于接收请求
[Route("api/[controller]")] [ApiController] public class StudentController : Controller { [HttpGet("GetStudent")] public IActionResult GetStudent() { var student = new Student() { Id = Guid.NewGuid().ToString(), Class = "321", Grade = "23", Name = "Name001", School = "School002" }; return Ok(student); } }
Middleware 中间件(记录Request和Response):
public class RequestResponseLoggingMiddleware { private RequestDelegate _next; public RequestResponseLoggingMiddleware(RequestDelegate next) { this._next = next; } ////// /// /// ///public async Task Invoke(HttpContext context) { //First, get the incoming request var request = await FormatRequest(context.Request); var body = context.Response.Body; //Copy a pointer to the original response body stream var originalBodyStream = context.Response.Body; //Create a new memory stream... using (var responseBody = new MemoryStream()) { //...and use that for the temporary response body context.Response.Body = responseBody; //Continue down the Middleware pipeline, eventually returning to this class await _next(context); //Format the response from the server var response = await FormatResponse(context.Response); //TODO: Save log to chosen datastore,临时使用 DemoQueueBlock .Add(new RequestResponseLog() { Id=Guid.NewGuid().ToString(), CreateTime = DateTime.Now, ResponseJson = response, RequestJson = request }); //Copy the contents of the new memory stream (which contains the response) to the original stream, which is then returned to the client. await responseBody.CopyToAsync(originalBodyStream); } }
为了防止实时存储数据库压力过大,仓储部分用了BlockingCollection实现的简易队列。
blockingcollection-1.getconsumingenumerable
public static void Consume(Actionfunc) { Task.Factory.StartNew(() => { foreach (var item in Colls.GetConsumingEnumerable()) { func(item); Console.WriteLine(string.Format("---------------: {0}", item)); } }); }
消费队列时入库:
public class DemoConsume { private readonly MysqlDbContext _dbContext; public DemoConsume(MysqlDbContext dbContext) { _dbContext = dbContext; } public bool Consume() { DemoQueueBlock.Consume(async (log)=> { await _dbContext.AddAsync(log); await _dbContext.SaveChangesAsync(); }); return true; } }
StartUp文件AddConsume和
app.UseMiddleware
();
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); var connection = Configuration.GetConnectionString("MysqlConnection"); services.AddDbContext(options => options.UseMySQL(connection),ServiceLifetime.Scoped); services.AddConsume(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseMiddleware (); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
Sql语句:
CREATE TABLE `request_response_log` ( `id` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `create_time` datetime(0) NULL DEFAULT NULL, `request_json` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, `response_json` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
运行程序效果:
可以看到该Demo提供了一个记录Http请求和输出日志的功能。
这里面和Middleware有关的功能为:
1、定义了RequestResponseLoggingMiddleware类
RequestDelegate向下转发请求,
Invoke方法
2、StartUp的app.UseMiddleware
()。
这些方法具体怎么流转运行的呢?我们来搜一下源码可以确认下。
三、源码跟踪
所以我们可以看下UseMiddlewareExtensions
public static class UseMiddlewareExtensions { internal const string InvokeMethodName = "Invoke"; internal const string InvokeAsyncMethodName = "InvokeAsync"; ////// Adds a middleware type to the application's request pipeline. /// /// Theinstance. /// The middleware type. /// The arguments to pass to the middleware type instance's constructor. /// The 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 container if (args.Length > 0) { throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware))); } return UseMiddlewareInterface(app, middleware); } var applicationServices = app.ApplicationServices; return app.Use(next => { var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); var invokeMethods = methods.Where(m => string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal) || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal) ).ToArray(); if (invokeMethods.Length > 1) { throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName)); } if (invokeMethods.Length == 0) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware)); } var methodInfo = invokeMethods[0]; if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task))); } var parameters = methodInfo.GetParameters(); 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); if (parameters.Length == 1) { return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance); } var factory = Compileinstance.
这里面用了
UseMiddleware
(),进而调用
UseMiddleware(type TMiddleware)
进行了如下判断:
1、如果TMiddleware是继承了IMiddleware,则执行UseMiddlewareInterface方法。利用IMiddlewareFactory提供中间件的工厂创建方式,Microsoft.AspNetCore.Http提供了IMiddlewareFactory的默认实现MiddlewareFactory。
return app.Use(next => { return async context => { 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 { await middleware.InvokeAsync(context, next); } finally { middlewareFactory.Release(middleware); } }; });
2、如果没有继承Middleware,则执行以下操作:
1、根据Invoke或InvokeAsync查找方法
2、验证只存在一个方法
3、验证返回类型为Task
4、验证第一个参数必须是HttpContext
5、ActivatorUtilities.CreateInstance 创建实例
6、如果只有一个参数,返回一个RequestDelegate类型的方法委托?
7、多个参数继续执行如下操作。Compile方法和参数。
var factory = Compile
return factory(instance, context, serviceProvider); };
8、Compile演示了Lamuda表达式的编译方式,以后可作参考
private static FuncCompile (MethodInfo methodInfo, ParameterInfo[] parameters) { // If we call something like // // public class Middleware // { // public Task Invoke(HttpContext context, ILoggerFactory loggerFactory) // { // // } // } // // We'll end up with something like this: // Generic version: // // Task Invoke(Middleware instance, HttpContext httpContext, IServiceProvider provider) // { // return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); // } // Non generic version: // // Task Invoke(object instance, HttpContext httpContext, IServiceProvider provider) // { // return ((Middleware)instance).Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); // } var middleware = typeof(T); var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext"); var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); var instanceArg = Expression.Parameter(middleware, "middleware"); var methodArguments = new Expression[parameters.Length]; methodArguments[0] = httpContextArg; for (int i = 1; i < parameters.Length; i++) { var parameterType = parameters[i].ParameterType; 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)) }; var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression); methodArguments[i] = Expression.Convert(getServiceCall, parameterType); } Expression middlewareInstanceArg = instanceArg; if (methodInfo.DeclaringType != null && methodInfo.DeclaringType != typeof(T)) { middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodInfo.DeclaringType); } var body = Expression.Call(middlewareInstanceArg, methodInfo, methodArguments); var lambda = Expression.Lambda >(body, instanceArg, httpContextArg, providerArg); return lambda.Compile(); }
从上面我们可以看到这个扩展方法主要做了两件事:
判断是IMiddleware,然后采用不同的处理方式。
文章刚开始我们已经实践了非继承的模式,下面我们来实践下继承IMiddleware的模式。
public class TestMiddleware : IMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { Console.WriteLine("TestMiddleware"); await next(context); // throw new NotImplementedException(); } }
StartUp
(由于
MiddlewareFactory通过_serviceProvider.GetRequiredService(middlewareType) as IMiddleware获取中间件,所以需要在ConfigureServices里面注入TestMiddleware,不然会报错):
public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseMiddleware (); }
效果如下:
以上搜查暂时告一段落。
但里面还有个IApplicationBuilder的use方式尚没有看到使用方式,还需要继续探查。
IApplicationBuilder接口:
定义一个用于配置应用程序请求管道的类
public interface IApplicationBuilder { ////// Gets or sets the IServiceProvider ApplicationServices { get; set; } ///that provides access to the application's service container. /// /// Gets the set of HTTP features the application's server provides. /// IFeatureCollection ServerFeatures { get; } ////// Gets a key/value collection that can be used to share data between middleware. /// IDictionaryProperties { get; } /// /// Adds a middleware delegate to the application's request pipeline. /// /// The middleware delegate. ///The IApplicationBuilder Use(Func. middleware); /// /// Creates a new ///that shares the of this /// . /// The new IApplicationBuilder New(); ///. /// Builds the delegate used by this application to process HTTP requests. /// ///The request handling delegate. RequestDelegate Build(); }
通过查看引用,我们可以看到提供了以下扩展:AspNetCore.Http.Abstractions\Extension
图片
通过翻看源码,可以看出这些扩展都是调用的IApplicationBuilder的use,我们只需要继续关注这个Use就行了。通过继续追溯源码,可以搜到IApplicationBuilderFactory的默认实现ApplicationBuilderFactory,它是一个创建ApplicationBuilder的工厂类。
public class ApplicationBuilderFactory : IApplicationBuilderFactory { private readonly IServiceProvider _serviceProvider; ////// Initialize a new factory instance with an /// The. /// used to resolve dependencies and initialize components. public ApplicationBuilderFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } /// /// Create an /// Anbuilder given a . /// of HTTP features. /// An public IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures) { return new ApplicationBuilder(_serviceProvider, serverFeatures); } }configured with .
关注一下 ApplicationBuilder的重点部分:
public class ApplicationBuilder : IApplicationBuilder { private const string ServerFeaturesKey = "server.Features"; private const string ApplicationServicesKey = "application.Services"; private readonly List> _components = new(); /// /// Initializes a new instance of /// The. /// for application services. public ApplicationBuilder(IServiceProvider serviceProvider) { Properties = new Dictionary (StringComparer.Ordinal); ApplicationServices = serviceProvider; } /// /// Initializes a new instance of /// The. /// for application services. /// The server instance that hosts the application. public ApplicationBuilder(IServiceProvider serviceProvider, object server) : this(serviceProvider) { SetProperty(ServerFeaturesKey, server); } private ApplicationBuilder(ApplicationBuilder builder) { Properties = new CopyOnWriteDictionary (builder.Properties, StringComparer.Ordinal); } /// /// Gets the public IServiceProvider ApplicationServices { get { return GetPropertyfor application services. /// (ApplicationServicesKey)!; } set { SetProperty (ApplicationServicesKey, value); } } /// /// Gets the public IFeatureCollection ServerFeatures { get { return GetPropertyfor server features. /// (ServerFeaturesKey)!; } } /// /// Gets a set of properties for public IDictionary. /// Properties { get; } private T? GetProperty (string key) { return Properties.TryGetValue(key, out var value) ? (T?)value : default(T); } private void SetProperty (string key, T value) { Properties[key] = value; } /// /// Adds the middleware to the application request pipeline. /// /// The middleware. ///An instance of public IApplicationBuilder Use(Funcafter the operation has completed. middleware) { _components.Add(middleware); return this; } /// /// Produces a ///that executes added middlewares. /// The public RequestDelegate Build() { RequestDelegate app = context => { // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened. // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware. 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); } context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; for (var c = _components.Count - 1; c >= 0; c--) { app = _components[c](app); } return app; } }.
从上面源码的实现来看Use的作用仅仅是将一个中间件添加到List
流程图如下:
图片
四、Asp.netCore内置Middleware举例:
以ConcurrencyLimiterMiddleware为例,传入的请求进行排队处理,避免线程池的不足.
public class ConcurrencyLimiterMiddleware { private readonly IQueuePolicy _queuePolicy; private readonly RequestDelegate _next; private readonly RequestDelegate _onRejected; private readonly ILogger _logger; ////// Creates a new /// The. /// representing the next middleware in the pipeline. /// The used for logging. /// The queueing strategy to use for the server. /// The options for the middleware, currently containing the 'OnRejected' callback. public ConcurrencyLimiterMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IQueuePolicy queue, IOptions options) { if (options.Value.OnRejected == null) { throw new ArgumentException("The value of 'options.OnRejected' must not be null.", nameof(options)); } _next = next; _logger = loggerFactory.CreateLogger (); _onRejected = options.Value.OnRejected; _queuePolicy = queue; } /// /// Invokes the logic of the middleware. /// /// The. /// A public async Task Invoke(HttpContext context) { var waitInQueueTask = _queuePolicy.TryEnterAsync(); // Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets. bool result; if (waitInQueueTask.IsCompleted) { ConcurrencyLimiterEventSource.Log.QueueSkipped(); result = waitInQueueTask.Result; } else { using (ConcurrencyLimiterEventSource.Log.QueueTimer()) { result = await waitInQueueTask; } } if (result) { try { await _next(context); } finally { _queuePolicy.OnExit(); } } else { ConcurrencyLimiterEventSource.Log.RequestRejected(); ConcurrencyLimiterLog.RequestRejectedQueueFull(_logger); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; await _onRejected(context); } }that completes when the request leaves.
需要注意的是有两个:
1、IQueuePolicy,asp.netCore内置了两种实现QueuePolicy和StackPolicy,这里就不贴代码了,主要是关于堆和栈的实现逻辑。
2、ConcurrencyLimiterOptions
QueuePolicyServiceCollectionExtensions
public static class QueuePolicyServiceCollectionExtensions { ////// Tells /// Theto use a FIFO queue as its queueing strategy. /// to add services to. /// Set the options used by the queue. /// Mandatory, since must be provided. /// public static IServiceCollection AddQueuePolicy(this IServiceCollection services, Action configure) { services.Configure(configure); services.AddSingleton (); return services; } /// /// Tells /// Theto use a LIFO stack as its queueing strategy. /// to add services to. /// Set the options used by the queue. /// Mandatory, since must be provided. /// public static IServiceCollection AddStackPolicy(this IServiceCollection services, Action configure) { services.Configure(configure); services.AddSingleton (); return services; } } public class QueuePolicyOptions { /// /// Maximum number of concurrent requests. Any extras will be queued on the server. /// This option is highly application dependant, and must be configured by the application. /// public int MaxConcurrentRequests { get; set; } ////// Maximum number of queued requests before the server starts rejecting connections with '503 Service Unavailable'. /// This option is highly application dependant, and must be configured by the application. /// public int RequestQueueLimit { get; set; } }
通过源码可以大概看出使用方式了吧,这里就不做实践了。
今天的分享到此结束,谢谢观看。
由于排版问题,原文请参考: https://mp.weixin.qq.com/s/nm8Pa-q3oOInX0LIw9swNA