1. 程式人生 > 其它 >asp.net core 6 管道構建流程之看一看

asp.net core 6 管道構建流程之看一看

.net core 6已經出來很久了,相關的書也看了一些,原始碼也看了一些,現在梳理一下我的理解。

asp.net core 6 註冊中介軟體寫法

public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            //省略
            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseAuthorization();
            app.UseAuthorization();
            app.MapControllers();

            app.Run();
        }

可以看到相關的對於中介軟體的寫法,和3.1還是有些不同的,3.1是專門寫在startup檔案裡的。啟動方式也做了改變,基於webapplication的啟動方式
和3.1的 host啟動有一些區別,那麼就來具體看看中介軟體是怎麼註冊,最後構建管道的把。
首先準備好原始碼,由於先前編譯過asp.net core 6的原始碼(想要嘗試一下可以主頁進去看看我的編譯記錄過程),所以暫時不用反編譯程式設計了。
首先看看 WebApplication.CreateBuilder(args);的原始碼

//直接new一個有預設的引數
public static WebApplicationBuilder CreateBuilder(string[] args) =>
            new(new() { Args = args });

那就繼續看看 webapplicationbuilder把

webapplicationbuilder

internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilder>? configureDefaults = null)
        {
            Services = _services;

            var args = options.Args;

            // Run methods to configure both generic and web host defaults early to populate config from appsettings.json
            // environment variables (both DOTNET_ and ASPNETCORE_ prefixed) and other possible default sources to prepopulate
            // the correct defaults.
            _bootstrapHostBuilder = new BootstrapHostBuilder(Services, _hostBuilder.Properties);

            // Don't specify the args here since we want to apply them later so that args
            // can override the defaults specified by ConfigureWebHostDefaults
            _bootstrapHostBuilder.ConfigureDefaults(args: null);

            // This is for testing purposes
            configureDefaults?.Invoke(_bootstrapHostBuilder);

            //省略

            _bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
            {
                // Runs inline.
                webHostBuilder.Configure(ConfigureApplication);

                // Attempt to set the application name from options
                options.ApplyApplicationName(webHostBuilder);
            });

            //省略
        }

可以看到// run inline這句註釋,差不多這就是我們想要找的東西了,直接進去看看 首先看看 configurationApplication是什麼東西

private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app)
        {
            Debug.Assert(_builtApplication is not null);

            // UseRouting called before WebApplication such as in a StartupFilter
            // lets remove the property and reset it at the end so we don't mess with the routes in the filter
            if (app.Properties.TryGetValue(EndpointRouteBuilderKey, out var priorRouteBuilder))
            {
                app.Properties.Remove(EndpointRouteBuilderKey);
            }

            if (context.HostingEnvironment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // Wrap the entire destination pipeline in UseRouting() and UseEndpoints(), essentially:
            // destination.UseRouting()
            // destination.Run(source)
            // destination.UseEndpoints()

            // Set the route builder so that UseRouting will use the WebApplication as the IEndpointRouteBuilder for route matching
            app.Properties.Add(WebApplication.GlobalEndpointRouteBuilderKey, _builtApplication);

            // Only call UseRouting() if there are endpoints configured and UseRouting() wasn't called on the global route builder already
            if (_builtApplication.DataSources.Count > 0)
            {
                // If this is set, someone called UseRouting() when a global route builder was already set
                if (!_builtApplication.Properties.TryGetValue(EndpointRouteBuilderKey, out var localRouteBuilder))
                {
                    app.UseRouting();
                }
                else
                {
                    // UseEndpoints will be looking for the RouteBuilder so make sure it's set
                    app.Properties[EndpointRouteBuilderKey] = localRouteBuilder;
                }
            }

            // Wire the source pipeline to run in the destination pipeline
            app.Use(next =>
            {
                _builtApplication.Run(next);
                return _builtApplication.BuildRequestDelegate();
            });

            if (_builtApplication.DataSources.Count > 0)
            {
                // We don't know if user code called UseEndpoints(), so we will call it just in case, UseEndpoints() will ignore duplicate DataSources
                app.UseEndpoints(_ => { });
            }

            // Copy the properties to the destination app builder
            foreach (var item in _builtApplication.Properties)
            {
                app.Properties[item.Key] = item.Value;
            }

            // Remove the route builder to clean up the properties, we're done adding routes to the pipeline
            app.Properties.Remove(WebApplication.GlobalEndpointRouteBuilderKey);

            // reset route builder if it existed, this is needed for StartupFilters
            if (priorRouteBuilder is not null)
            {
                app.Properties[EndpointRouteBuilderKey] = priorRouteBuilder;
            }
        }

一下就發現些很有趣的東西了,從3.1到6我們發現有些預設的註冊中介軟體不需要我們寫了,原來是它預設幫我們註冊了。可以看到如果你在程式碼中寫了app.MapControllers();以及相關一些終結點資料來源的配置,即是_builtApplication.DataSources.Count > 0 那麼我們就會注入app.UseRouting(); app.UseEndpoints(_ => { });然後我們寫的關於中介軟體的註冊在哪呢,可以看到
_builtApplication.Run(next);
return _builtApplication.BuildRequestDelegate();
_builtApplication就是 webapplication,直接將下面的預設註冊的中介軟體放入到我們自己註冊的中介軟體後面,最後返回這些中介軟體構建的管道,這些就把預設新增的中介軟體和我們自己的註冊的中介軟體連線起來了。很巧妙。同時也說明了,useendpoints這個中介軟體如果你在程式碼中不顯示寫出來的話,等到框架自動幫我們新增,那麼它肯定是在我們自己的中介軟體後面。中介軟體的順序會導致一些問題,比如我在 3.1 裡面些 app.useOcelot寫在最後面,那麼我們的請求還可以先走我們自己定於的一些路由邏輯,最後由ocelot閘道器分發,但是如果你在 6 裡面也寫在最後面,且沒有顯示的寫useendpoints,那麼你所有的請求都會由ocelot分發,而不會寫你自己定義的一些路由邏輯。所以我們有掌握了一些小知識。(其實是我學習Ocelot的時候踩過的坑)。
這部分程式碼看完了,我們繼續往上看 webHostBuilder.Configure(ConfigureApplication);看看configure寫了什麼。

public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, IApplicationBuilder> configureApp)
        {
            if (configureApp == null)
            {
                throw new ArgumentNullException(nameof(configureApp));
            }

            // Light up the ISupportsStartup implementation
            if (hostBuilder is ISupportsStartup supportsStartup)
            {
                return supportsStartup.Configure(configureApp);
            }

            var startupAssemblyName = configureApp.GetMethodInfo().DeclaringType!.Assembly.GetName().Name!;

            hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);

            return hostBuilder.ConfigureServices((context, services) =>
            {
                services.AddSingleton<IStartup>(sp =>
                {
                    return new DelegateStartup(sp.GetRequiredService<IServiceProviderFactory<IServiceCollection>>(), (app => configureApp(context, app)));
                });
            });
        }

哦豁,又是一些有趣的程式碼,如果要判斷走的邏輯 ,那我們就要知道hostBuilder是不是ISupportsStartup型別,那是不是呢,答案是是的,我們注入的委託物件的型別是GenericWebHostBuilder,它實現了 ISupportsStartup方法。可以看下原始碼,_bootstrapHostBuilder.ConfigureWebHostDefaults是怎麼來注入這個委託物件的

public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
        {
            if (configure is null)
            {
                throw new ArgumentNullException(nameof(configure));
            }

            return builder.ConfigureWebHost(webHostBuilder =>
            {
                WebHost.ConfigureWebDefaults(webHostBuilder);

                configure(webHostBuilder);
            });
        }
 public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
        {
            if (configure is null)
            {
                throw new ArgumentNullException(nameof(configure));
            }

            return builder.ConfigureWebHost(configure, _ => { });
        }
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));
            }

            // Light up custom implementations namely ConfigureHostBuilder which throws.
            if (builder is ISupportsConfigureWebHost supportsConfigureWebHost)
            {
                return supportsConfigureWebHost.ConfigureWebHost(configure, configureWebHostBuilder);
            }

            var webHostBuilderOptions = new WebHostBuilderOptions();
            configureWebHostBuilder(webHostBuilderOptions);
            var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions);
            configure(webhostBuilder);
            builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
            return builder;
        }

可以看到一系列不停的呼叫邏輯 ,直到最後的程式碼才到了重點, var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions);看到了我們new了一個webhostbuilder,然後呼叫我們的委託物件,configure(webhostBuilder);所以需要看看 GenericWebHostBuilder是否實現了 ISupportsStartup型別

GenericWebHostBuilder

internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
    {
        public IWebHostBuilder Configure(Action<WebHostBuilderContext, IApplicationBuilder> configure)
        {
            var startupAssemblyName = configure.GetMethodInfo().DeclaringType!.Assembly.GetName().Name!;

            UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);

            // Clear the startup type
            _startupObject = configure;

            _builder.ConfigureServices((context, services) =>
            {
                if (object.ReferenceEquals(_startupObject, configure))
                {
                    services.Configure<GenericWebHostServiceOptions>(options =>
                    {
                        var webhostBuilderContext = GetWebHostBuilderContext(context);
                        options.ConfigureApplication = app => configure(webhostBuilderContext, app);
                    });
                }
            });

            return this;
        }
    }

可以看到實現了 ISupportsStartup型別,順便將其configure方法貼了出來,具體看看,還是服務註冊,但是我們仔細看看這一句
services.Configure(options =>
{
var webhostBuilderContext = GetWebHostBuilderContext(context);
options.ConfigureApplication = app => configure(webhostBuilderContext, app);
});
將我們對於中介軟體的註冊複製到了 GenericWebHostServiceOptions.ConfigureApplication上,那這就是重點了,為啥說是重點呢,我們都知道 asp.net core服務其實也可以看作一個長期的後臺服務,那麼這個服務是哪個呢,實際上就是
GenericWebHostService這個服務,服務的啟動就是startasync方法,那麼就看看這個類以及方法

GenericWebHostService

internal sealed partial class GenericWebHostService : IHostedService
    {
        public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
                                     IServer server,
                                     ILoggerFactory loggerFactory,
                                     DiagnosticListener diagnosticListener,
                                     ActivitySource activitySource,
                                     DistributedContextPropagator propagator,
                                     IHttpContextFactory httpContextFactory,
                                     IApplicationBuilderFactory applicationBuilderFactory,
                                     IEnumerable<IStartupFilter> startupFilters,
                                     IConfiguration configuration,
                                     IWebHostEnvironment hostingEnvironment)
        {
            Options = options.Value;
            Server = server;
            Logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Hosting.Diagnostics");
            LifetimeLogger = loggerFactory.CreateLogger("Microsoft.Hosting.Lifetime");
            DiagnosticListener = diagnosticListener;
            ActivitySource = activitySource;
            Propagator = propagator;
            HttpContextFactory = httpContextFactory;
            ApplicationBuilderFactory = applicationBuilderFactory;
            StartupFilters = startupFilters;
            Configuration = configuration;
            HostingEnvironment = hostingEnvironment;
        }

        public GenericWebHostServiceOptions Options { get; }
        //省略
public async Task StartAsync(CancellationToken cancellationToken)
        {
            HostingEventSource.Log.HostStart();
           //省略
            RequestDelegate? application = null;
            try
            {
                var configure = Options.ConfigureApplication;
                 //省略
                var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);

                foreach (var filter in StartupFilters.Reverse())
                {
                    configure = filter.Configure(configure);
                }
                configure(builder);
                // Build the request pipeline
                application = builder.Build();
            }
            catch (Exception ex)
            {
               // 省略
            }
            var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory);
            await Server.StartAsync(httpApplication, cancellationToken);
            //省略
        }

}

可以看到這個類注入了 IOptions options,然後在啟動方法裡直接用Options.ConfigureApplication獲取到了我們先前的中介軟體註冊,然後獲取IStartupFilter註冊的中介軟體註冊,最後用建立的var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);build先呼叫IStartupFilter的中介軟體,那麼就可以解釋為啥這個中介軟體在管道呼叫的最前面了。然後在註冊我們的中介軟體,最後
// Build the request pipeline
application = builder.Build();
註釋都寫了,構建請求管道,然後丟到我們的服務裡面去,整個管道就這樣構成了,邏輯也走了一遍。

總結

實際上就是通過各種方案把我們對於中介軟體的註冊以及預設的註冊的中介軟體轉移到 GenericWebHostServiceOptions.ConfigureApplication上最後構建管道處理模型。

題外話

以上都是自己推測的邏輯,那麼可能你錯了呢,所以還是需要做個驗證,開啟神器dnspy,寫個demo丟進去,打斷點執行一下,看看是不是我們想的要執行的邏輯。
首先打斷點


然後執行,看看結果


完美,邏輯確實是和我們想的那樣。