1. 程式人生 > >ASP.NET Core 中介軟體(Middleware)(一)

ASP.NET Core 中介軟體(Middleware)(一)

本文主要目標:記錄Middleware的執行原理流程,並繪製流程圖。 目錄結構: >1、執行環境 >2、Demo實踐 >3、原始碼追蹤 >4、AspnetCore內建middleware ![](https://img2020.cnblogs.com/blog/380359/202102/380359-20210214190655198-262907590.png) #一、執行環境 Visual Studio Community 2019 版本 16.8.5 .Net Sdk Version: 5.0.103 #二、Demo實踐 講解或學習一個東西的時候,最方便的方式是先寫一個Demo。基於此,我寫以一箇中間件的記錄請求輸出的實踐Demo來理解Middleware。 實體: ``` c# 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 >https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.concurrent.blockingcollection-1.getconsumingenumerable?view=net-5.0 ``` public static void Consume(Action func) { 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語句: ``` 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; ``` 執行程式效果: ![](https://img2020.cnblogs.com/blog/380359/202102/380359-20210214191110194-2011025163.png) ![](https://img2020.cnblogs.com/blog/380359/202102/380359-20210214191223070-1495071610.png) 可以看到該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. ///
/// The instance. /// The middleware type. /// The arguments to pass to the middleware type instance's constructor. /// The instance. 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 = Compile(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); }; }); } ``` 這裡面用了 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(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); }; 8、Compile演示了Lamuda表示式的編譯方式,以後可作參考 ``` private static Func Compile(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(); } ``` 效果如下: ![](https://img2020.cnblogs.com/blog/380359/202102/380359-20210214191247981-932577050.png) 以上搜查暫時告一段落。 但裡面還有個IApplicationBuilder的use方式尚沒有看到使用方式,還需要繼續探查。 IApplicationBuilder介面: 定義一個用於配置應用程式請求管道的類 ``` public interface IApplicationBuilder { /// /// Gets or sets the that provides access to the application's service container. /// IServiceProvider ApplicationServices { get; set; } /// /// 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. /// IDictionary Properties { 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 builder given a . /// /// An of HTTP features. /// An configured with . public IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures) { return new ApplicationBuilder(_serviceProvider, serverFeatures); } } ``` 關注一下 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 for application services. /// public IServiceProvider ApplicationServices { get { return GetProperty(ApplicationServicesKey)!; } set { SetProperty(ApplicationServicesKey, value); } } /// /// Gets the for server features. /// public IFeatureCollection ServerFeatures { get { return GetProperty(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 after the operation has completed. public IApplicationBuilder Use(Func 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> _components裡面,換句話來講就是將一個RequestDelegate的委託放到一個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 that completes when the request leaves. 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); } } ``` 需要注意的是有兩個: 1、IQueuePolicy,asp.netCore內建了兩種實現QueuePolicy和StackPolicy,這裡就不貼程式碼了,主要是關於堆和棧的實現邏輯。 2、ConcurrencyLimiterOptions QueuePolicyServiceCollectionExtensions ``` public static class QueuePolicyServiceCollectionExtensions { /// /// Tells to use a FIFO queue as its queueing strategy. /// /// The 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 to use a LIFO stack as its queueing strategy. /// /// The 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-q3oOInX0L