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丢进去,打断点执行一下,看看是不是我们想的要执行的逻辑。
首先打断点


然后运行,看看结果


完美,逻辑确实是和我们想的那样。

posted @ 2022-12-05 18:16  果小天  阅读(252)  评论(0编辑  收藏  举报