.Net Core後端架構實戰【2-實現動態路由與Dynamic API】

2023-04-08 18:00:20

摘要:基於.NET Core 7.0WebApi後端架構實戰【2-實現動態路由與Dynamic API】  2023/02/22, ASP.NET Core 7.0, VS2022

引言

    使用過ABP vNext和Furion框架的可能都會對它們的動態API感到好奇,不用手動的去定義,它會動態的去建立API控制器。後端程式碼
架構的複雜在核心程式碼,如果這些能封裝的好提升的是小組整體的生產力。靈圖圖書的扉頁都會有這樣一句話:"站在巨人的肩膀上"。我在
這裡大言不慚的說上一句我希望我也能成為"巨人"!

動態路由

在.Net Core WebAPI程式中通過可全域性或區域性修改的自定義Route屬性和URL對映元件匹配傳入的HTTP請求替代預設路由即為動態路由

WebApplicationBuilder

在3.1以及5.0的版本中,Configure方法中會自動新增UseRouting()與UseEndpoints()方法,但是在6.0以上版本已經沒有了。其實在建立WebApplicationBuilder範例的時候預設已經新增進去了。請看原始碼:

var builder = WebApplication.CreateBuilder(args);
WebApplication.cs檔案中
/// <summary>
/// Initializes a new instance of the class with preconfigured defaults.
/// </summary>
/// <param name="args">Command line arguments</param>
/// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder CreateBuilder(string[] args) =>
    new(new WebApplicationOptions() { Args = args });
WebApplicationBuilder.cs檔案中,webHostBuilder.Configure(ConfigureApplication)這句程式碼他將包含註冊路由與終結點的方法新增到了宿主程式啟動的設定當中。
internal WebApplicationBuilder(WebApplicationOptions options, Action? 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);
    // We specify the command line here last since we skipped the one in the call to ConfigureDefaults.
    // The args can contain both host and application settings so we want to make sure
    // we order those configuration providers appropriately without duplicating them
    if (args is { Length: > 0 })
    {
        _bootstrapHostBuilder.ConfigureAppConfiguration(config =>
        {
            config.AddCommandLine(args);
        });
    }
    _bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
    {
        // Runs inline.
//看這裡 webHostBuilder.Configure(ConfigureApplication); // Attempt to set the application name from options options.ApplyApplicationName(webHostBuilder); }); // Apply the args to host configuration last since ConfigureWebHostDefaults overrides a host specific setting (the application n _bootstrapHostBuilder.ConfigureHostConfiguration(config => { if (args is { Length: > 0 }) { config.AddCommandLine(args); } // Apply the options after the args options.ApplyHostConfiguration(config); }); Configuration = new(); // This is chained as the first configuration source in Configuration so host config can be added later without overriding app c Configuration.AddConfiguration(_hostConfigurationManager); // Collect the hosted services separately since we want those to run after the user's hosted services _services.TrackHostedServices = true; // This is the application configuration var (hostContext, hostConfiguration) = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder); // Stop tracking here _services.TrackHostedServices = false; // Capture the host configuration values here. We capture the values so that // changes to the host configuration have no effect on the final application. The // host configuration is immutable at this point. _hostConfigurationValues = new(hostConfiguration.AsEnumerable()); // Grab the WebHostBuilderContext from the property bag to use in the ConfigureWebHostBuilder var webHostContext = (WebHostBuilderContext)hostContext.Properties[typeof(WebHostBuilderContext)]; // Grab the IWebHostEnvironment from the webHostContext. This also matches the instance in the IServiceCollection. Environment = webHostContext.HostingEnvironment; Logging = new LoggingBuilder(Services); Host = new ConfigureHostBuilder(hostContext, Configuration, Services); WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services); Services.AddSingleton(_ => Configuration); }

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; } }
WebHostBuilderExtensions.cs檔案中,Configure方法用於加入設定項,GetWebHostBuilderContext方法用於獲取宿主機構建的上下文資訊,即已設定的主機資訊。
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(options =>
            {
                var webhostBuilderContext = GetWebHostBuilderContext(context);
                options.ConfigureApplication = app => configure(webhostBuilderContext, app);
            });
        }
    });
    return this;
}
private static WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context) { if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var contextVal)) { var options = new WebHostOptions(context.Configuration, Assembly.GetEntryAssembly()?.GetName().Name ?? string.Empty); var webHostBuilderContext = new WebHostBuilderContext { Configuration = context.Configuration, HostingEnvironment = new HostingEnvironment(), }; webHostBuilderContext.HostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options); context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext; context.Properties[typeof(WebHostOptions)] = options; return webHostBuilderContext; } // Refresh config, it's periodically updated/replaced var webHostContext = (WebHostBuilderContext)contextVal; webHostContext.Configuration = context.Configuration; return webHostContext; }

UseRouting

原始碼如下圖所示:

erifyRoutingServicesAreRegistered用於驗證路由服務是否已註冊到容器內部

②判斷在請求管道的共用資料字典的Properties中是否有GlobalEndpointRouteBuilderKey的鍵,如果沒有則New一個新的終結點路由構建者物件,並將EndpointRouteBuilder新增到共用字典中。後面UseEndpoints(Action<IEndpointRouteBuilder> configure)執行時,會將前面New的DefaultEndpointRouteBuilder 範例取出,並進一步設定它: configure(EndpointRouteBuilder範例)

③將EndpointRoutingMiddleware中介軟體註冊到管道中,該中介軟體根據請求和Url匹配最佳的Endpoint,然後將該終結點交由EndpointMiddleware 處理。

UseEndpoints

原始碼如下圖所示:

 ①VerifyEndpointRoutingMiddlewareIsRegistered方法將EndpointRouteBuilder從請求管道的共用字典中取出,如果沒有則說明之前沒有呼叫UseRouting(),所以呼叫UseEndpoints()之前要先呼叫UseRouting()VerifyEndpointRoutingMiddlewareIsRegistered方法如下圖所示:

EndpointMiddleware主要是在EndpointRoutingMiddleware篩選出endpoint之後,呼叫該endpointendpoint.RequestDelegate(httpContext)進行請求處理。並且這個中介軟體會最終執行RequestDelegate委託來處理請求。請求的處理大部分功能在中介軟體EndpointRoutingMiddleware中,它有個重要的屬性_endpointDataSource儲存了上文中初始化階段生成的MvcEndpointDataSource,而中介軟體EndpointMiddleware的功能比較簡單,主要是在EndpointRoutingMiddleware篩選出endpoint之後,呼叫該endpoint.RequestDelegate(httpContext)方法進行請求處理。

看一下Endpoint類原始碼,Endpoint就是定義誰(Action)來執行請求的物件

public class Endpoint
{
    ///<summary>
    /// Creates a new instance of.
    ///</summary>
    ///<param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
    ///<param name="metadata">
    /// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
    ///</param>
    ///<param name="displayName">
    /// The informational display name of the endpoint. May be null.
/// </param> public Endpoint( RequestDelegate? requestDelegate, EndpointMetadataCollection? metadata, string? displayName) { // All are allowed to be null RequestDelegate = requestDelegate; Metadata = metadata ?? EndpointMetadataCollection.Empty; DisplayName = displayName; } /// <summary> /// Gets the informational display name of this endpoint. /// </summary> public string? DisplayName { get; } /// <summary> /// Gets the collection of metadata associated with this endpoint. /// public EndpointMetadataCollection Metadata { get; } /// <summary> /// Gets the delegate used to process requests for the endpoint. /// </summary> public RequestDelegate? RequestDelegate { get; } /// <summary> /// Returns a string representation of the endpoint. /// </summary> public override string? ToString() => DisplayName ?? base.ToString(); }

Metadata非常重要,是存放控制器還有Action的後設資料,在應用程式啟動的時候就將控制器和Action的關鍵資訊給存入,例如路由、特性、HttpMethod等

RequestDelegate 用於將請求(HttpContext)交給資源(Action)執行

AddControllers

我們來看下AddControllers()AddMvcCore()及相關聯的原始碼

MvcServiceCollectionExtensions檔案中,AddControllersCore方法用於新增控制器的核心服務,它最主要的作用是主要作用就是掃描所有的有關程式集封裝成ApplicationPart。

public static class MvcServiceCollectionExtensions
{
    /// <summary>
    /// Adds services for controllers to the specified. This method will not
    /// register services used for views or pages.
    /// </summary>
    ///<param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
    /// <returns>An <see cref="IMvcBuilder"/> that can be used to further configure the MVC services.</returns>
    /// <remarks>
    /// <para>
    /// This method configures the MVC services for the commonly used features with controllers for an API. This
    /// combines the effects of <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/>,
    /// <see cref="MvcApiExplorerMvcCoreBuilderExtensions.AddApiExplorer(IMvcCoreBuilder)"/>,
    /// <see cref="MvcCoreMvcCoreBuilderExtensions.AddAuthorization(IMvcCoreBuilder)"/>,
    /// <see cref="MvcCorsMvcCoreBuilderExtensions.AddCors(IMvcCoreBuilder)"/>,
    /// <see cref="MvcDataAnnotationsMvcCoreBuilderExtensions.AddDataAnnotations(IMvcCoreBuilder)"/>,
    /// and <see cref="MvcCoreMvcCoreBuilderExtensions.AddFormatterMappings(IMvcCoreBuilder)"/>.
/// </para> /// <para> /// To add services for controllers with views call <see cref="AddControllersWithViews(IServiceCollection)"/> /// on the resulting builder.
/// </para> /// <para> /// To add services for pages call <see cref="AddRazorPages(IServiceCollection)"/> /// on the resulting builder. /// on the resulting builder. /// </remarks> public static IMvcBuilder AddControllers(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } //新增Controllers核心服務 var builder = AddControllersCore(services); return new MvcBuilder(builder.Services, builder.PartManager); } private static IMvcCoreBuilder AddControllersCore(IServiceCollection services) { // This method excludes all of the view-related services by default. var builder = services .AddMvcCore()//這個是核心,返回IMvcCoreBuilder物件,其後的服務引入都是基於它的 .AddApiExplorer() .AddAuthorization() .AddCors() .AddDataAnnotations() .AddFormatterMappings(); if (MetadataUpdater.IsSupported) { services.TryAddEnumerable( ServiceDescriptor.Singleton<IActionDescriptorChangeProvider, HotReloadService>()); } return builder; } }

AddMvcCore方法用於新增MVC的核心服務,下面的GetApplicationPartManager方法先獲取ApplicationPartManager物件,然後將當前程式集封裝成了ApplicationPart放進ApplicationParts集合中。
ConfigureDefaultFeatureProviders(partManager)主要作用是建立了一個新的ControllerFeatureProvider範例放進了partManager的FeatureProviders屬性中,注意這個ControllerFeatureProvider物件在後面遍歷ApplicationPart的時候負責找出裡面的Controller。
AddMvcCore()方法其後是新增Routing服務再接著新增Mvc核心服務然後構建一個MvcCoreBuilder範例並返回


///<summary>
/// Extension methods for setting up essential MVC services in an.
///</summary>
public static class MvcCoreServiceCollectionExtensions
{
    ///<summary>
    /// Adds the minimum essential MVC services to the specified 
    /// <see cref="IServiceCollection" />. Additional services
    /// including MVC's support for authorization, formatters, and validation must be added separately 
    /// using the <see cref="IMvcCoreBuilder"/> returned from this method.
    ///</summary>
    ///<param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
    /// <returns>
    /// An <see cref="IMvcCoreBuilder"/> that can be used to further configure the MVC services.
    /// </returns>
    /// <remarks>
    /// The <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/> 
    /// approach for configuring
    /// MVC is provided for experienced MVC developers who wish to have full control over the 
    /// set of default services
    /// registered. <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/> 
    /// will register
    /// the minimum set of services necessary to route requests and invoke controllers. 
    /// It is not expected that any
    /// application will satisfy its requirements with just a call to
    /// <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/>
    /// . Additional configuration using the
    /// <see cref="IMvcCoreBuilder"/> will be required.
    /// </remarks>
    public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }
        //獲取注入的IWebHostEnvironment環境物件
        var environment = GetServiceFromCollection(services);
        //獲取程式中所有關聯的程式集的ApplicationPartManager
        var partManager = GetApplicationPartManager(services, environment);
        services.TryAddSingleton(partManager);
        //給ApplicationPartManager新增ControllerFeature
        ConfigureDefaultFeatureProviders(partManager);
        //呼叫services.AddRouting();
        ConfigureDefaultServices(services);
        //新增MVC相關聯的服務至IOC容器中
        AddMvcCoreServices(services);
        var builder = new MvcCoreBuilder(services, partManager);
        return builder;

    }

    private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services, IWebHostEnvironment? environment)
    {
        var manager = GetServiceFromCollection(services);
        if (manager == null)
        {
            manager = new ApplicationPartManager();
            //獲取當前主程式集的名稱
            var entryAssemblyName = environment?.ApplicationName;
            if (string.IsNullOrEmpty(entryAssemblyName))
            {
                return manager;
            }
            //找出所有參照的程式集並將他們新增到ApplicationParts中
            manager.PopulateDefaultParts(entryAssemblyName);
        }

        return manager;
    }

    private static void ConfigureDefaultFeatureProviders(ApplicationPartManager manager)
    {
        if (!manager.FeatureProviders.OfType().Any())
        {
            manager.FeatureProviders.Add(new ControllerFeatureProvider());
        }
    }

    private static void ConfigureDefaultServices(IServiceCollection services)
    {
        services.AddRouting();
    }

    internal static void AddMvcCoreServices(IServiceCollection services)
    {
        //
        // Options
        //
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IConfigureOptions, MvcCoreMvcOptionsSetup>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IPostConfigureOptions, MvcCoreMvcOptionsSetup>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IConfigureOptions, ApiBehaviorOptionsSetup>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IConfigureOptions, MvcCoreRouteOptionsSetup>());

        //
        // Action Discovery
        //
        // These are consumed only when creating action descriptors, then they can be deallocated
        services.TryAddSingleton();
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IApplicationModelProvider, DefaultApplicationModelProvider>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IApplicationModelProvider, ApiBehaviorApplicationModelProvider>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IActionDescriptorProvider, ControllerActionDescriptorProvider>());

        services.TryAddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>();

        //
        // Action Selection
        //
        services.TryAddSingleton<IActionSelector, ActionSelector>();
        services.TryAddSingleton();

        // Will be cached by the DefaultActionSelector
        services.TryAddEnumerable(ServiceDescriptor.Transient<IActionConstraintProvider, DefaultActionConstraintProvider>());

        // Policies for Endpoints
        services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, ActionConstraintMatcherPolicy>());

        //
        // Controller Factory
        //
        // This has a cache, so it needs to be a singleton
        services.TryAddSingleton<IControllerFactory, DefaultControllerFactory>();

        // Will be cached by the DefaultControllerFactory
        services.TryAddTransient<IControllerActivator, DefaultControllerActivator>();

        services.TryAddSingleton<IControllerFactoryProvider, ControllerFactoryProvider>();
        services.TryAddSingleton<IControllerActivatorProvider, ControllerActivatorProvider>();
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IControllerPropertyActivator, DefaultControllerPropertyActivator>());

        //
        // Action Invoker
        //
        // The IActionInvokerFactory is cachable
        services.TryAddSingleton<IActionInvokerFactory, ActionInvokerFactory>();
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IActionInvokerProvider, ControllerActionInvokerProvider>());

        // These are stateless
        services.TryAddSingleton();
        services.TryAddEnumerable(
            ServiceDescriptor.Singleton<IFilterProvider, DefaultFilterProvider>());
        services.TryAddSingleton<IActionResultTypeMapper, ActionResultTypeMapper>();

        //
        // Request body limit filters
        //
        services.TryAddTransient();
        services.TryAddTransient();
        services.TryAddTransient();

        //
        // ModelBinding, Validation
        //
        // The DefaultModelMetadataProvider does significant caching and should be a singleton.
        services.TryAddSingleton<IModelMetadataProvider, DefaultModelMetadataProvider>();
        services.TryAdd(ServiceDescriptor.Transient(s =>
        {
            var options = s.GetRequiredService<IOptions>().Value;
            return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders);
        }));
        services.TryAddSingleton<IModelBinderFactory, ModelBinderFactory>();
        services.TryAddSingleton(s =>
        {
            var options = s.GetRequiredService<IOptions>().Value;
            var metadataProvider = s.GetRequiredService();
            return new DefaultObjectValidator(metadataProvider, options.ModelValidatorProviders, options);
        });
        services.TryAddSingleton();
        services.TryAddSingleton();

        //
        // Random Infrastructure
        //
        services.TryAddSingleton<MvcMarkerService, MvcMarkerService>();
        services.TryAddSingleton<ITypeActivatorCache, TypeActivatorCache>();
        services.TryAddSingleton<IUrlHelperFactory, UrlHelperFactory>();
        services.TryAddSingleton<IHttpRequestStreamReaderFactory, MemoryPoolHttpRequestStreamReaderFactory>();
        services.TryAddSingleton<IHttpResponseStreamWriterFactory, MemoryPoolHttpResponseStreamWriterFactory>();
        services.TryAddSingleton(ArrayPool.Shared);
        services.TryAddSingleton(ArrayPool.Shared);
        services.TryAddSingleton<OutputFormatterSelector, DefaultOutputFormatterSelector>();
        services.TryAddSingleton<IActionResultExecutor, ObjectResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, PhysicalFileResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, VirtualFileResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, FileStreamResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, FileContentResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, RedirectResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, LocalRedirectResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, RedirectToActionResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, RedirectToRouteResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, RedirectToPageResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, ContentResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor, SystemTextJsonResultExecutor>();
        services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
        services.TryAddSingleton<ProblemDetailsFactory, DefaultProblemDetailsFactory>();

        //
        // Route Handlers
        //
        services.TryAddSingleton(); // Only one per app
        services.TryAddTransient(); // Many per app

        //
        // Endpoint Routing / Endpoints
        //
        services.TryAddSingleton();
        services.TryAddSingleton();
        services.TryAddSingleton();
        services.TryAddSingleton();
        services.TryAddSingleton();
        services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicControllerEndpointMatcherPolicy>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IRequestDelegateFactory, ControllerRequestDelegateFactory>());

        //
        // Middleware pipeline filter related
        //
        services.TryAddSingleton();
        // This maintains a cache of middleware pipelines, so it needs to be a singleton
        services.TryAddSingleton();
        // Sets ApplicationBuilder on MiddlewareFilterBuilder
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, MiddlewareFilterBuilderStartupFilter>());
    }
}

下面的PopulateDefaultParts()方法從當前程式集找到所有參照到了的程式集(包括[assembly:ApplicationPart(「demo」)]中標記的)把他們封裝成ApplciationPart,然後把他們放在了ApplciationPartManager的ApplicationParts屬性中,用於後面篩選Controller提供資料基礎。


namespace Microsoft.AspNetCore.Mvc.ApplicationParts
{
    /// 
    /// Manages the parts and features of an MVC application.
    /// 
    public class ApplicationPartManager
    {

        /// 
        /// Gets the list of  instances.
        /// 
        /// Instances in this collection are stored in precedence order. An  that appears
        /// earlier in the list has a higher precedence.
        /// An  may choose to use this an interface as a way to resolve conflicts when
        /// multiple  instances resolve equivalent feature values.
        /// 
        /// 
        public IList ApplicationParts { get; } = new List();

        internal void PopulateDefaultParts(string entryAssemblyName)
        {
            //獲取相關聯的程式集
            var assemblies = GetApplicationPartAssemblies(entryAssemblyName);

            var seenAssemblies = new HashSet();

            foreach (var assembly in assemblies)
            {
                if (!seenAssemblies.Add(assembly))
                {
                    // "assemblies" may contain duplicate values, but we want unique ApplicationPart instances.
                    // Note that we prefer using a HashSet over Distinct since the latter isn't
                    // guaranteed to preserve the original ordering.
                    continue;
                }

                var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly);
                foreach (var applicationPart in partFactory.GetApplicationParts(assembly))
                {
                    ApplicationParts.Add(applicationPart);
                }
            }
        }

        private static IEnumerable GetApplicationPartAssemblies(string entryAssemblyName)
        {
            //載入當前主程式集
            var entryAssembly = Assembly.Load(new AssemblyName(entryAssemblyName));

            // Use ApplicationPartAttribute to get the closure of direct or transitive dependencies
            // that reference MVC.
            var assembliesFromAttributes = entryAssembly.GetCustomAttributes()
                .Select(name => Assembly.Load(name.AssemblyName))
                .OrderBy(assembly => assembly.FullName, StringComparer.Ordinal)
                .SelectMany(GetAssemblyClosure);

            // The SDK will not include the entry assembly as an application part. We'll explicitly list it
            // and have it appear before all other assemblies \ ApplicationParts.
            return GetAssemblyClosure(entryAssembly)
                .Concat(assembliesFromAttributes);
        }

        private static IEnumerable GetAssemblyClosure(Assembly assembly)
        {
            yield return assembly;

            var relatedAssemblies = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false)
                .OrderBy(assembly => assembly.FullName, StringComparer.Ordinal);

            foreach (var relatedAssembly in relatedAssemblies)
            {
                yield return relatedAssembly;
            }
        }
    }
}

MapControllers

我們接下來看下Controller裡的Action是怎樣註冊到路由模組的。MapControllers()方法執行時就會遍歷遍歷已經收集到的ApplicationPart進而將其中Controller裡面的Action方法轉換封裝成一個個的EndPoint放到路由中介軟體的設定物件RouteOptions中然後交給Routing模組處理。還有一個重要作用是將EndpointMiddleware中介軟體註冊到http管道中。EndpointMiddleware的一大核心程式碼主要是執行Endpoint 的RequestDelegate 委託,也即對Controller 中的Action 的執行。所有的Http請求都會走到EndpointMiddleware中介軟體中,然後去執行對應的Action。在應用程式啟動的時候會把我們的所有的路由資訊新增到一個EndpointSource的集合中去的,所以在MapController方法,其實就是在構建我們所有的路由請求的一個RequestDelegate,然後在每次請求的時候,在EndpointMiddleWare中介軟體去執行這個RequestDelegate,從而走到我們的介面中去。簡而言之,這個方法就是將我們的所有路由資訊新增到一個EndpointDataSource的抽象類的實現類中去,預設是ControllerActionEndpointDataSource這個類,在這個類中有一個基礎類別ActionEndpointDataSourceBase,ControllerActionEndpointDataSource初始化的時候會訂閱所有的Endpoint的集合的變化,每變化一次會向EndpointSource集合新增Endpoint,從而在請求的時候可以找到這個終結點去呼叫。

我們來看下MapControllers()的原始碼

public static class ControllerEndpointRouteBuilderExtensions
{
    ///
    /// Adds endpoints for controller actions to the without specifying any routes.
    ///
    ///The .
    /// An  for endpoints associated with controller actions.
    public static ControllerActionEndpointConventionBuilder MapControllers(this IEndpointRouteBuilder endpoints)
    {
        if (endpoints == null)
        {
            throw new ArgumentNullException(nameof(endpoints));
        }

        EnsureControllerServices(endpoints);

        return GetOrCreateDataSource(endpoints).DefaultBuilder;
    }

    private static void EnsureControllerServices(IEndpointRouteBuilder endpoints)
    {
        var marker = endpoints.ServiceProvider.GetService();
        if (marker == null)
        {
            throw new InvalidOperationException(Resources.FormatUnableToFindServices(
                nameof(IServiceCollection),
                "AddControllers",
                "ConfigureServices(...)"));
        }
    }

    private static ControllerActionEndpointDataSource GetOrCreateDataSource(IEndpointRouteBuilder endpoints)
    {
        var dataSource = endpoints.DataSources.OfType().FirstOrDefault();
        if (dataSource == null)
        {
            var orderProvider = endpoints.ServiceProvider.GetRequiredService();
            var factory = endpoints.ServiceProvider.GetRequiredService();
            dataSource = factory.Create(orderProvider.GetOrCreateOrderedEndpointsSequenceProvider(endpoints));
            endpoints.DataSources.Add(dataSource);
        }

        return dataSource;
    }
}

首先EnsureControllerServices方法檢查mvc服務是否注入了,GetOrCreateDataSource方法執行完就獲取到了dateSource,dateSource中就是所有的Action資訊。需要注意的是ControllerActionEndpointDataSource這個類,它裡面的方法幫我們建立路由終結點。我們來看一下它的定義:

internal class ControllerActionEndpointDataSource : ActionEndpointDataSourceBase
{
    private readonly ActionEndpointFactory _endpointFactory;
    private readonly OrderedEndpointsSequenceProvider _orderSequence;
    private readonly List _routes;

    public ControllerActionEndpointDataSource(
        ControllerActionEndpointDataSourceIdProvider dataSourceIdProvider,
        IActionDescriptorCollectionProvider actions,
        ActionEndpointFactory endpointFactory,
        OrderedEndpointsSequenceProvider orderSequence)
        : base(actions)
    {
        _endpointFactory = endpointFactory;

        DataSourceId = dataSourceIdProvider.CreateId();
        _orderSequence = orderSequence;

        _routes = new List();

        DefaultBuilder = new ControllerActionEndpointConventionBuilder(Lock, Conventions);

        // IMPORTANT: this needs to be the last thing we do in the constructor.
        // Change notifications can happen immediately!
        Subscribe();
    }

    public int DataSourceId { get; }

    public ControllerActionEndpointConventionBuilder DefaultBuilder { get; }

    // Used to control whether we create 'inert' (non-routable) endpoints for use in dynamic
    // selection. Set to true by builder methods that do dynamic/fallback selection.
    public bool CreateInertEndpoints { get; set; }

    public ControllerActionEndpointConventionBuilder AddRoute(
        string routeName,
        string pattern,
        RouteValueDictionary? defaults,
        IDictionary<string, object?>? constraints,
        RouteValueDictionary? dataTokens)
    {
        lock (Lock)
        {
            var conventions = new List<Action>();
            _routes.Add(new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, _orderSequence.GetNext(), conventions));
            return new ControllerActionEndpointConventionBuilder(Lock, conventions);
        }
    }

    protected override List CreateEndpoints(IReadOnlyList actions, IReadOnlyList<Action> conventions)
    {
        var endpoints = new List();
        var keys = new HashSet(StringComparer.OrdinalIgnoreCase);

        // MVC guarantees that when two of it's endpoints have the same route name they are equivalent.
        //
        // However, Endpoint Routing requires Endpoint Names to be unique.
        var routeNames = new HashSet(StringComparer.OrdinalIgnoreCase);

        // For each controller action - add the relevant endpoints.
        //
        // 1. If the action is attribute routed, we use that information verbatim
        // 2. If the action is conventional routed
        //      a. Create a *matching only* endpoint for each action X route (if possible)
        //      b. Ignore link generation for now
        for (var i = 0; i < actions.Count; i++)
        {
            if (actions[i] is ControllerActionDescriptor action)
            {
                _endpointFactory.AddEndpoints(endpoints, routeNames, action, _routes, conventions, CreateInertEndpoints);

                if (_routes.Count > 0)
                {
                    // If we have conventional routes, keep track of the keys so we can create
                    // the link generation routes later.
                    foreach (var kvp in action.RouteValues)
                    {
                        keys.Add(kvp.Key);
                    }
                }
            }
        }

        // Now create a *link generation only* endpoint for each route. This gives us a very
        // compatible experience to previous versions.
        for (var i = 0; i < _routes.Count; i++)
        {
            var route = _routes[i];
            _endpointFactory.AddConventionalLinkGenerationRoute(endpoints, routeNames, keys, route, conventions);
        }

        return endpoints;
    }

    internal void AddDynamicControllerEndpoint(IEndpointRouteBuilder endpoints, string pattern, Type transformerType, object? state, int? order = null)
    {
        CreateInertEndpoints = true;
        lock (Lock)
        {
            order ??= _orderSequence.GetNext();

            endpoints.Map(
                pattern,
                context =>
                {
                    throw new InvalidOperationException("This endpoint is not expected to be executed directly.");
                })
                .Add(b =>
                {
                    ((RouteEndpointBuilder)b).Order = order.Value;
                    b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(transformerType, state));
                    b.Metadata.Add(new ControllerEndpointDataSourceIdMetadata(DataSourceId));
                });
        }
    }
}

CreateEndpoints方法中會遍歷每個ActionDescriptor物件,ActionDescriptor物件裡面儲存的是Action方法的後設資料。然後建立一個個的Endpoint範例,Endpoint物件裡面有一個RequestDelegate引數,當請求進入的時候會執行這個委託進入對應的Action。另外這其中還有一個DefaultBuilder屬性,可以看到他返回的是ControllerActionEndpointConventionBuilder物件,這個物件是用來構建約定路由的。AddRoute方法也是用來新增約定路由的。我們再來看下建構函式中的Subscribe()方法,這個方法是呼叫父類別ActionEndpointDataSourceBase中的。我們來看一下這個類:

internal abstract class ActionEndpointDataSourceBase : EndpointDataSource, IDisposable
{
    private readonly IActionDescriptorCollectionProvider _actions;

    // The following are protected by this lock for WRITES only. This pattern is similar
    // to DefaultActionDescriptorChangeProvider - see comments there for details on
    // all of the threading behaviors.
    protected readonly object Lock = new object();

    // Protected for READS and WRITES.
    protected readonly List<Action> Conventions;

    private List? _endpoints;
    private CancellationTokenSource? _cancellationTokenSource;
    private IChangeToken? _changeToken;
    private IDisposable? _disposable;

    public ActionEndpointDataSourceBase(IActionDescriptorCollectionProvider actions)
    {
        _actions = actions;

        Conventions = new List<Action>();
    }

    public override IReadOnlyList Endpoints
    {
        get
        {
            Initialize();
            Debug.Assert(_changeToken != null);
            Debug.Assert(_endpoints != null);
            return _endpoints;
        }
    }

    // Will be called with the lock.
    protected abstract List CreateEndpoints(IReadOnlyList actions, IReadOnlyList<Action> conventions

    protected void Subscribe()
    {
        // IMPORTANT: this needs to be called by the derived class to avoid the fragile base class
        // problem. We can't call this in the base-class constuctor because it's too early.
        //
        // It's possible for someone to override the collection provider without providing
        // change notifications. If that's the case we won't process changes.
        if (_actions is ActionDescriptorCollectionProvider collectionProviderWithChangeToken)
        {
            _disposable = ChangeToken.OnChange(
                () => collectionProviderWithChangeToken.GetChangeToken(),
                UpdateEndpoints);
        }
    }

    public override IChangeToken GetChangeToken()
    {
        Initialize();
        Debug.Assert(_changeToken != null);
        Debug.Assert(_endpoints != null);
        return _changeToken;
    }

    public void Dispose()
    {
        // Once disposed we won't process updates anymore, but we still allow access to the endpoints.
        _disposable?.Dispose();
        _disposable = null;
    }

    private void Initialize()
    {
        if (_endpoints == null)
        {
            lock (Lock)
            {
                if (_endpoints == null)
                {
                    UpdateEndpoints();
                }
            }
        }
    }

    private void UpdateEndpoints()
    {
        lock (Lock)
        {
            var endpoints = CreateEndpoints(_actions.ActionDescriptors.Items, Conventions);

            // See comments in DefaultActionDescriptorCollectionProvider. These steps are done
            // in a specific order to ensure callers always see a consistent state.

            // Step 1 - capture old token
            var oldCancellationTokenSource = _cancellationTokenSource;

            // Step 2 - update endpoints
            _endpoints = endpoints;

            // Step 3 - create new change token
            _cancellationTokenSource = new CancellationTokenSource();
            _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);

            // Step 4 - trigger old token
            oldCancellationTokenSource?.Cancel();
        }
    }
}

_actions屬性是注入進來的,這個物件是我們在services.AddMvcCore()中注入進來的:services.TryAddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>();我們來說下ChangeToken.OnChange()方法,他裡面有兩個委託型別的引數,GetChangeToken()它的作用是用來感知ActionDescriptor資料來源的變化,然後執行UpdateEndpoints方法中的具體的邏輯:

  • 首先更新ActionDescriptors物件的具體後設資料資訊
  • 獲取舊的令牌
  • 更新終結點
  • 建立新的令牌
  • 廢棄舊的令牌

大家做的專案都有鑑權、授權的功能。而每一個角色可以存取的資源是不相同的,因此策略鑑權是非常關鍵的一步,它可以阻止非此選單資源的角色使用者存取此選單的介面。一般來說有一個介面表(Module)、一個選單表(Permission)、一個介面選單關係表(ModulePermission),介面需要掛在選單下面,假如一個專案几百個介面,那錄起來可就麻煩了。按照我們上面說的,在管道構建時,程式就會掃描所有相關程式集中Controller的Action然後交給「路由」模組去管理。Action的這些後設資料資訊會存在我們上面說的IActionDescriptorCollectionProvider中的ActionDescriptorCollection物件的ActionDescriptor集合中,這樣在http請求到來時「路由」模組才能尋找到正確的Endpoint,進而找到Action並呼叫執行。那麼我們就可以讀到專案中所有註冊的路由,然後匯入到資料庫表中