前言
本篇繼續之前的思路,不注重用法,如果還不知道有哪些用法的小夥伴,可以點選這裡,微軟文件說的很詳細,在閱讀本篇文章前,還是希望你對中介軟體有大致的瞭解,這樣你讀起來可能更加能夠意會到意思。廢話不多說,我們們進入正題(ps:讀者要注意關注原始碼的註釋哦?)。
Middleware類之間的關係
下圖也是隻列出重要的類和方法,其主要就是就ApplicationBuilder類,如下圖:
原始碼解析
1.在使用中介軟體時,需要在StartUp類的Config方法中來完成(.Net自帶的中介軟體,官方有明確過使用的順序,可以看文件),例如Use,Map,Run等方法,它們都通過IApplicationBuilder內建函式呼叫,所以我們先看ApplicationBuilder類的主體構造,程式碼如下圖:
//這個是所有中介軟體的委託
public delegate Task RequestDelegate(HttpContext context);
public class ApplicationBuilder : IApplicationBuilder
{
//服務特性集合key
private const string ServerFeaturesKey = "server.Features";
//注入的服務集合key
private const string ApplicationServicesKey = "application.Services";
//新增的中介軟體集合
private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new();
public ApplicationBuilder(IServiceProvider serviceProvider)
{
Properties = new Dictionary<string, object?>(StringComparer.Ordinal);
ApplicationServices = serviceProvider;
}
public ApplicationBuilder(IServiceProvider serviceProvider, object server)
: this(serviceProvider)
{
SetProperty(ServerFeaturesKey, server);
}
private ApplicationBuilder(ApplicationBuilder builder)
{
Properties = new CopyOnWriteDictionary<string, object?>(builder.Properties, StringComparer.Ordinal);
}
public IServiceProvider ApplicationServices
{
get
{
return GetProperty<IServiceProvider>(ApplicationServicesKey)!;
}
set
{
SetProperty<IServiceProvider>(ApplicationServicesKey, value);
}
}
public IFeatureCollection ServerFeatures
{
get
{
return GetProperty<IFeatureCollection>(ServerFeaturesKey)!;
}
}
//快取結果,方便讀取
public IDictionary<string, object?> Properties { get; }
private T? GetProperty<T>(string key)
{
return Properties.TryGetValue(key, out var value) ? (T?)value : default(T);
}
private void SetProperty<T>(string key, T value)
{
Properties[key] = value;
}
//新增委託呼叫,將中介軟體新增到集合中
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
_components.Add(middleware);
return this;
}
//建立新的AppBuilder
public IApplicationBuilder New()
{
return new ApplicationBuilder(this);
}
//執行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);
}
context.Response.StatusCode = StatusCodes.Status404NotFound;
return Task.CompletedTask;
};
//後新增的在末端,先新增的先執行
for (var c = _components.Count - 1; c >= 0; c--)
{
app = _components[c](app);
}
return app;
}
}
根據上述程式碼可以看出,向集合中新增項只能呼叫Use方法,然後在Build方法時將委託全部構造成鏈,請求引數是HttpContext,也就是說,每次請求時,直接呼叫這個鏈路頭部的委託就可以把所有方法走一遍。
- 接下來,看一下那些自定義的中介軟體是怎麼加入到管道,並且在.net中是怎麼處理的,原始碼如下:
public static class UseMiddlewareExtensions
{
internal const string InvokeMethodName = "Invoke";
internal const string InvokeAsyncMethodName = "InvokeAsync";
public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)]TMiddleware>(this IApplicationBuilder app, params object?[] args)
{
return app.UseMiddleware(typeof(TMiddleware), args);
}
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args)
{
//判斷如果是以依賴注入的形式加入的中介軟體,需要繼承IMiddleware,則不允許有引數
if (typeof(IMiddleware).IsAssignableFrom(middleware))
{
if (args.Length > 0)
{
throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
}
return UseMiddlewareInterface(app, middleware);
}
var applicationServices = app.ApplicationServices;
return app.Use(next =>
{
//檢查是否有Invoke或者InvokeAsync方法
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];
//返回型別必須是Task
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
}
//獲取Invoke方法引數
var parameters = methodInfo.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
}
//第一個引數是RequestDelegate
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)
{
//如果是隻有一個引數,直接根據例項Invoke方法,建立RequestDelegate委託
return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
}
//說明Invoke有容器注入的其他服務,則這個方法就是獲取那些服務
var factory = Compile<object>(methodInfo, parameters);
return context =>
{
//預設是請求的Scope容器,如果是null,則返回根容器
var serviceProvider = context.RequestServices ?? applicationServices;
if (serviceProvider == null)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
}
//執行Invoke方法
return factory(instance, context, serviceProvider);
};
});
}
private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType)
{
//呼叫Use方法,將委託新增到ApplicationBuilder的記憶體集合裡
return app.Use(next =>
{
return async context =>
{
//獲取中介軟體工廠類,從Scope容器中獲取注入的中介軟體
var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));
if (middlewareFactory == null)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)));
}
//獲取中介軟體注入的物件例項
var middleware = middlewareFactory.Create(middlewareType);
if (middleware == null)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));
}
try
{
//呼叫InvokeAsync方法
await middleware.InvokeAsync(context, next);
}
finally
{
//實際上沒做處理,和容器的生命週期一致
middlewareFactory.Release(middleware);
}
};
});
}
}
根據上面的程式碼可以看出,根據不同方式注入的中介軟體,.Net做了不同的處理,並且對自定義的中介軟體做型別檢查,但是最後必須呼叫app.Use方法,將委託加入到ApplicationBuilder的記憶體集合裡面,到Build階段處理。
- 上面介紹了自定義中介軟體的處理方式,接下里我們依次介紹下Use,Map和Run方法的處理,原始碼如下:
public static class UseExtensions
{
public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
{
//呼叫Use方法,新增到記憶體集合裡
return app.Use(next =>
{
return context =>
{
//next就是下一個處理,也就是RequestDelegate
Func<Task> simpleNext = () => next(context);
return middleware(context, simpleNext);
};
});
}
}
public static class MapExtensions
{
public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration)
{
return Map(app, pathMatch, preserveMatchedPathSegment: false, configuration);
}
public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, bool preserveMatchedPathSegment, Action<IApplicationBuilder> configuration)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
//不能是/結尾,這個!.用法我也是學習到了
if (pathMatch.HasValue && pathMatch.Value!.EndsWith("/", StringComparison.Ordinal))
{
throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch));
}
//構建新的ApplicationBuilder物件,裡面不包含之前新增的中介軟體
var branchBuilder = app.New();
//新分支裡面的中介軟體
configuration(branchBuilder);
//執行Build方法構建分支管道
var branch = branchBuilder.Build();
var options = new MapOptions
{
Branch = branch,
PathMatch = pathMatch,
PreserveMatchedPathSegment = preserveMatchedPathSegment
};
//內部其實是檢查是否匹配,匹配的話執行Branch,不匹配繼續執行next
return app.Use(next => new MapMiddleware(next, options).Invoke);
}
}
public static class RunExtensions
{
public static void Run(this IApplicationBuilder app, RequestDelegate handler)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
//只執行Handle,不做其他處理,也就是管道終端,給短路了
app.Use(_ => handler);
}
}
上面的程式碼分別介紹了Use,Map,Run的方法實現,它們還是在需要將中介軟體加入到記憶體集合裡面,但是對於不同的方法,它們達到的效果也不一樣。
- 總結上面的程式碼可以看出,它執行完Build方法,把委託鏈構造出來之後,然後在每次請求的時候只需要將構造完成的HttpContext當作請求引數傳入之後,即可依次執行中介軟體的內容,那麼應用程式是如何構建ApplicationBuilder物件例項,又是在哪裡呼叫Build方法的呢?我們繼續往下看。
我們知道在使用.Net通用模板的建立專案的時候,在Program裡面有一句程式碼,如下:
Host.CreateDefaultBuilder(args)
//這個方法主要是構建web主機,如Kestrel,整合IIS等操作
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
追溯其原始碼的位置時,實際上它是作為了IHostedService服務來執行的,如果有不清楚IHostedService的小夥伴可以點選這裡,先看下官方文件的解釋和用法,看完之後你就明白了。我們再來看原始碼:
public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder)
{
if (configure is null)
{
throw new ArgumentNullException(nameof(configure));
}
if (configureWebHostBuilder is null)
{
throw new ArgumentNullException(nameof(configureWebHostBuilder));
}
if (builder is ISupportsConfigureWebHost supportsConfigureWebHost)
{
return supportsConfigureWebHost.ConfigureWebHost(configure, configureWebHostBuilder);
}
var webHostBuilderOptions = new WebHostBuilderOptions();
//下面兩行執行的程式碼,是關於構建Host主機的,以後新的文章來說
configureWebHostBuilder(webHostBuilderOptions);
var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions);
//執行自定的方法,例如模板方法裡面的UseStartUp
configure(webhostBuilder);
//主要看這裡,將其新增到HostedService
builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
return builder;
}
internal sealed partial class GenericWebHostService : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
HostingEventSource.Log.HostStart();
var serverAddressesFeature = Server.Features.Get<IServerAddressesFeature>();
var addresses = serverAddressesFeature?.Addresses;
//配置服務地址
if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0)
{
var urls = Configuration[WebHostDefaults.ServerUrlsKey];
if (!string.IsNullOrEmpty(urls))
{
serverAddressesFeature!.PreferHostingUrls = WebHostUtilities.ParseBool(Configuration, WebHostDefaults.PreferHostingUrlsKey);
foreach (var value in urls.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
addresses.Add(value);
}
}
}
//定義最終返回的委託變數
RequestDelegate? application = null;
try
{
//預設StartUp類裡面的Config
var configure = Options.ConfigureApplication;
if (configure == null)
{
throw new InvalidOperationException($"No application configured. Please specify an application via IWebHostBuilder.UseStartup, IWebHostBuilder.Configure, or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration.");
}
//構建ApplicationBuilder
var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);
//如果存在IStartupFilter,那麼把要執行的中介軟體放到前面
foreach (var filter in StartupFilters.Reverse())
{
configure = filter.Configure(configure);
}
configure(builder);
//執行Build,開始構建委託鏈
application = builder.Build();
}
catch (Exception ex)
{
Logger.ApplicationError(ex);
if (!Options.WebHostOptions.CaptureStartupErrors)
{
throw;
}
var showDetailedErrors = HostingEnvironment.IsDevelopment() || Options.WebHostOptions.DetailedErrors;
application = ErrorPageBuilder.BuildErrorPageApplication(HostingEnvironment.ContentRootFileProvider, Logger, showDetailedErrors, ex);
}
var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory);
//啟動服務
await Server.StartAsync(httpApplication, cancellationToken);
}
}
從上面可以看出,最終是作為IHostedService來執行的,而StartAsync方法,則是在Host.Build().Run()中的Run方法裡面統一執行所有註冊過IHostedService服務的集合,也就是在Run階段才開始構建管道(讀者可以自行看下原始碼,以後的文章我也會講到)。
總結
通過解讀原始碼可以看出中介軟體有以下特點:
- 目前自定義的中介軟體要麼需要繼承IMiddleware(不能傳遞引數),要麼需要構造指定規則的類。
- Use不會使管道短路(除非呼叫方不呼叫next),Map和Run會使管道短路,更多的是,Run不會再往下傳遞,也就是終止,而Map可能會往下傳遞。
- 委託鏈的構造是在Run方法中執行的,並且作為IHostedService託管服務執行的。
上述文章中所展示的原始碼並不是全部的原始碼,筆者只是挑出其重點部分展示。由於文采能力有限?,如果有沒說明白的或者沒有描述清楚的,又或者有錯誤的地方,還請評論指正。