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

江北、發表於2023-04-08

摘要:基於.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並呼叫執行。那麼我們就可以讀到專案中所有註冊的路由,然後匯入到資料庫表中?

private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;

public RouteController(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
{
    _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
}
/// <summary>
/// 獲取路由
/// </summary>
/// <returns></returns>
[HttpGet]
public IActionResult Get()
{
    var routes = _actionDescriptorCollectionProvider.ActionDescriptors.Items.Select(x => new
    {
        Action = x.RouteValues["Action"],
        Controller = x.RouteValues["Controller"],
        Name = x.AttributeRouteInfo.Name,
        Method = x.ActionConstraints?.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods.First(),
        Template = x.AttributeRouteInfo.Template
    }).ToList();
    return Ok(routes);
}

上面我們聊了一些原始碼,接下來我們來看下如何實現動態路由

MvcOptions

先說一下MvcOptions類,它為.Net Core 整個框架提供基礎配置。這樣說估計太抽象了,我舉例一下哈。例如Action加上[FromBody],客戶端傳入的Body為null的話,介面會報400錯誤:A non-empty request body is required。可以使用模型驗證AllowEmptyInputInBodyModelBinding引數配置null值可傳入(.Net5之後可以根據需要按請求進行配置)。還有FilterCollection集合這個引數,從MVC時代沿用到現在的五種資源過濾器,其實他們都預設繼承自IFilterMetadata空介面,而FilterCollection集合就是承載這些Filter的容器且繼承自Collection<IFilterMetadata>,關於AOP管道中介軟體這些我後面會單獨抽原始碼來講。好了我們這篇主要要說一下它裡面的IList<IApplicationModelConvention>引數。

IApplicationModelConvention

我們先看下它的原始碼:

我們可以寫一個類繼承它,實現它的Apply方法,修改.Net Core程式內部對路由、控制器的預設生成行為,然後將它新增到Convention集合中?

透過Apply方法來進行自定義,可以修改的內容由ApplicationModel物件提供。特別是它裡面的ControllerModel物件,有了它我們可以直接對控制器進行各種配置和操作。

看一下ApplicationModel物件的定義:

/// <summary>
/// A model for configuring controllers in an MVC application.
/// </summary>
[DebuggerDisplay("ApplicationModel: Controllers: {Controllers.Count}, Filters: {Filters.Count}")]
public class ApplicationModel : IPropertyModel, IFilterModel, IApiExplorerModel
{
    /// <summary>
    /// Initializes a new instance of <see cref="ApplicationModel"/>.
    /// </summary>
    public ApplicationModel()
    {
        ApiExplorer = new ApiExplorerModel();
        Controllers = new List<ControllerModel>();
        Filters = new List<IFilterMetadata>();
        Properties = new Dictionary<object, object?>();
    }
    /// <summary>
    /// Gets or sets the <see cref="ApiExplorerModel"/> for the application.
    /// </summary>
    /// <remarks>
    /// <see cref="ApplicationModel.ApiExplorer"/> allows configuration of default settings
    /// for ApiExplorer that apply to all actions unless overridden by
    /// <see cref="ControllerModel.ApiExplorer"/> or <see cref="ActionModel.ApiExplorer"/>.
    ///
    /// If using <see cref="ApplicationModel.ApiExplorer"/> to set <see cref="ApiExplorerModel.IsVisible"/> to
    /// <c>true</c>, this setting will only be honored for actions which use attribute routing.
    /// </remarks>
    public ApiExplorerModel ApiExplorer { get; set; }
    /// <summary>
    /// Gets the <see cref="ControllerModel"/> instances.
    /// </summary>
    public IList<ControllerModel> Controllers { get; }
    /// <summary>
    /// Gets the global <see cref="IFilterMetadata"/> instances.
    /// </summary>
    public IList<IFilterMetadata> Filters { get; }
    /// <summary>
    /// Gets a set of properties associated with all actions.
    /// These properties will be copied to <see cref="Abstractions.ActionDescriptor.Properties"/>.
    /// </summary>
    public IDictionary<object, object?> Properties { get; }
}

ApiExplorer可以用來配置控制器的組資訊還有可見性

Controllers可以獲取Controller的相關資訊,再借助IControllerModelConvention對其進行定製擴充套件

Filters存放的都是空介面,起到標記作用,換句話說就是在請求管道構建的時候用於判斷是否為Filter類

Properties屬於共享字典

給路由新增全域性配置

services.AddControllers(options =>
{
    options.UseCentralRoutePrefix(new RouteAttribute("core/v1/api/[controller]/[action]"));
});

新增我們自定義擴充套件方法

public static class MvcOptionsExtensions
{
    /// <summary>
    /// 擴充套件方法
    /// </summary>
    /// <param name="opts"></param>
    /// <param name="routeAttribute"></param>
    public static void UseCentralRoutePrefix(this MvcOptions opts, IRouteTemplateProvider routeAttribute)
    {
//新增我們自定義實現 opts.Conventions.Insert(0, new RouteConvention(routeAttribute)); } }

 具體的實現類

/// <summary>
/// 全域性路由字首配置
/// </summary>
public class RouteConvention : IApplicationModelConvention
{
    /// <summary>
    /// 定義一個路由字首變數
    /// </summary>
    private readonly AttributeRouteModel _centralPrefix;
    /// <summary>
    /// 呼叫時傳入指定的路由字首
    /// </summary>
    /// <param name="routeTemplateProvider"></param>
    public RouteConvention(IRouteTemplateProvider routeTemplateProvider)
    {
        _centralPrefix = new AttributeRouteModel(routeTemplateProvider);
    }

    //實現Apply方法
    public void Apply(ApplicationModel application)
    {
//遍歷所有的 Controller foreach (var controller in application.Controllers) {        var matchedSelectors = controller.Selectors.Where(x => x.AttributeRouteModel != null).ToList(); if (matchedSelectors.Any())//該Controller已經標記了RouteAttribute { foreach (var selectorModel in matchedSelectors) { // 在當前路由上再新增一個 路由字首 selectorModel.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_centralPrefix, selectorModel.AttributeRouteModel); } } var unmatchedSelectors = controller.Selectors.Where(x => x.AttributeRouteModel == null).ToList(); if (unmatchedSelectors.Any())//該Controller沒有標記RouteAttribute { foreach (var selectorModel in unmatchedSelectors) { // 新增一個路由字首 selectorModel.AttributeRouteModel = _centralPrefix; } } } } }

POCO控制器

在Java中有一個叫POJO的名詞,即"Plain Old Java Object",直譯就是簡單的Java物件,其實它表示的是沒有繼承任何類,也沒有實現任何介面的物件。在C#中也有一個相同含義的名詞叫POCO(Plain Old C# Object),兩者表示的含義是一樣的。在.Net Core中有一個POCO Controller的特性,它不用繼承Controller或ControllerBase,只需要在類名後加上Controller的字尾或標記[Controller]特性也能擁有Controller的功能。

下面簡單演示一下:

public class TestController
{

    [HttpGet]
    public async Task<IEnumerable<int>> Get()
    {
        Func<int, int> triple = m => m * 3;
        var range = Enumerable.Range(1, 3);
        return range.Select(triple);
    }
}

[Controller]
public class TestOnce
{
    [HttpGet]
    public async Task<IEnumerable<dynamic>> Index()
    => Enumerable.Range(1, 100).Select(triple => new { triple });

}

 上面兩個類中的Action會被正確掃描並新增到終結點中:

一個(控制器)類如果加上[NonController]就不會被註冊到路由中?。我們接下來還是看下原始碼:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts;

namespace Microsoft.AspNetCore.Mvc.Controllers;

/// <summary>
/// Discovers controllers from a list of <see cref="ApplicationPart"/> instances.
/// </summary>
public class ControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
    private const string ControllerTypeNameSuffix = "Controller";

    /// <inheritdoc />
    public void PopulateFeature(
        IEnumerable<ApplicationPart> parts,
        ControllerFeature feature)
    {
        foreach (var part in parts.OfType<IApplicationPartTypeProvider>())
        {
            foreach (var type in part.Types)
            {
                if (IsController(type) && !feature.Controllers.Contains(type))
                {
                    feature.Controllers.Add(type);
                }
            }
        }
    }

    /// <summary>
    /// Determines if a given <paramref name="typeInfo"/> is a controller.
    /// </summary>
    /// <param name="typeInfo">The <see cref="TypeInfo"/> candidate.</param>
    /// <returns><see langword="true" /> if the type is a controller; otherwise <see langword="false" />.</returns>
    protected virtual bool IsController(TypeInfo typeInfo)
    {
        if (!typeInfo.IsClass)
        {
            return false;
        }

        if (typeInfo.IsAbstract)
        {
            return false;
        }

        // We only consider public top-level classes as controllers. IsPublic returns false for nested
        // classes, regardless of visibility modifiers
        if (!typeInfo.IsPublic)
        {
            return false;
        }

        if (typeInfo.ContainsGenericParameters)
        {
            return false;
        }

        if (typeInfo.IsDefined(typeof(NonControllerAttribute)))
        {
            return false;
        }

        if (!typeInfo.Name.EndsWith(ControllerTypeNameSuffix, StringComparison.OrdinalIgnoreCase) &&
            !typeInfo.IsDefined(typeof(ControllerAttribute)))
        {
            return false;
        }

        return true;
    }
}

其實POCO控制器的核心就在於IApplicationFeatureProvider<ControllerFeature>這個介面,ControllerFeatureProvider是其預設的實現類。

我們重新寫一個類繼承自ControllerFeatureProvider,把IsController方法進行重寫加入我們的判斷邏輯,其它我就不囉嗦了,上面的程式碼很清楚白了了?

自定義控制器規則

定義一個介面和一個特性使之成為我們的規則

public interface ICoreDynamicController { }

[AttributeUsage(AttributeTargets.Class, Inherited = true)]
public class CoreDynamicControllerAttribute : Attribute { }

繼承ControllerFeatureProvider類並且實現IsController方法:

public class CoreDynamicExtendControlleFeatureProvider : ControllerFeatureProvider
{
    protected override bool IsController(TypeInfo typeInfo)
    {
        var type = typeInfo.AsType();
        if ((typeof(ICoreDynamicController).IsAssignableFrom(type) || //判斷是否繼承ICoreDynamicController介面
            type.IsDefined(typeof(CoreDynamicControllerAttribute), true) || // 判斷是否標記了ICoreDynamicController特性
            type.BaseType == typeof(Microsoft.AspNetCore.Mvc.Controller)) && //判斷基型別是否是Controller
            (typeInfo.IsPublic && !typeInfo.IsAbstract && !typeInfo.IsGenericType && !typeInfo.IsInterface)) //必須是Public、不能是抽象類、必須是非泛型的
        {
return true; } return false; } }

 現在方法已經寫好了,但是我們要把它配置到Mvc中才行。這裡要說一下MvcCoreMvcBuilderExtensions類的IMvcBuilderConfigureApplicationPartManager方法,它的引數是一個委託,委託中的引數是ApplicationPartManager ApplicationPartManager中有一個FeatureProviders的屬性,它裡面全是IApplicationFeatureProvider的例項。程式啟動的時候會迴圈這些例項,我們把自己的自定義實現類新增進來,這樣Core程式就能識別我們的控制器,並且賦予其控制器所有的功能。無圖無真相,請看原始碼:

 所以把我們自定義的識別類新增進來即可

services.AddControllers().ConfigureApplicationPartManager
(t => t.FeatureProviders.Add(new CoreDynamicExtendControlleFeatureProvider()));

 如下示例:

public class Test : ICoreDynamicController
{

    [HttpGet]
    public IEnumerable<int> Get(int value)
    {
        yield return value;
    }
}

Dynamic Api

使用過ABP vNext框架的小夥伴都應該知道,如果一個類實現了IRemoteServiceIApplicationService介面,那麼它會被自動選擇為API控制器。ABP vNext框架在動態API功能中遵從約定大於配置的原則,例如方法名稱以GetList,GetAllGet開頭則請求的HttpMethod都為HttpGet

ABP vNext官方文件:API/Auto API Controllers | Documentation Center | ABP.IO

我們藉助它的思想來實現我們的動態API

實現Apply方法

在AspNetCore框架中給出了三個配置控制器、方法和引數的配置介面,分別是IControllerModelConventionIActionModelConvention和IParameterModelConvention。在它們的Apply方法中,傳入了一個 MVC 啟動階段掃描到的型別,對應的分別是ControllerModelActionModelParameterModel我們可以透過這三個Model加入我們的自定義配置。還是一樣我們要繼承IApplicationModelConvention介面篩選出符合條件的控制器,然後遍歷其中的Action給其新增路由與HttpMethos(要根據Action的字首進行判斷)。本專案是根據下面列舉的條件進行判斷的(注意:得到ActionMethodName的時候要ToUpper或ToLower這樣方便判斷):

  • Get:如果方法以GETQUERY開頭
  • Post:如果方法以CREATESAVEINSERTADD開頭
  • Put:如果方法以UPDATEEDIT開頭
  • Delete:如果方法以DeleteREMOVE開頭
"HttpMethodInfo": [
  {
    "MethodKey": "Get",
    "MethodVal": [ "GET", "QUERY" ]
  },
  {
    "MethodKey": "Post",
    "MethodVal": [ "CREATE", "SAVE", "INSERT", "ADD" ]
  },
  {
    "MethodKey": "Put",
    "MethodVal": [ "UPDATE", "EDIT" ]
  },
  {
    "MethodKey": "Delete",
    "MethodVal": [ "Delete", "REMOVE" ]
  }
]
public class CoreDynamicControllerConvention : IApplicationModelConvention
{
    private IConfiguration _configuration;
    private List<HttpMethodConfigure> httpMethods = new();
    public CoreDynamicControllerConvention(IConfiguration configuration)
    {
        _configuration = configuration;
        httpMethods = (List<HttpMethodConfigure>)_configuration.GetSection("HttpMethodInfo").Get(typeof(List<HttpMethodConfigure>));
    }
    public void Apply(ApplicationModel application)
    {
        //迴圈每一個控制器資訊
        foreach (var controller in application.Controllers)
        {
            var controllerType = controller.ControllerType.AsType();
            //是否繼承ICoreDynamicController介面
            if (typeof(ICoreDynamicController).IsAssignableFrom(controllerType))
            {
                foreach (var item in controller.Actions)
                {
                    ConfigureSelector(controller.ControllerName, item);
                }
            }
        }
    }

    private void ConfigureSelector(string controllerName, ActionModel action)
    {
        for (int i = 0; i < action.Selectors.Count; i++)
        {
            if (action.Selectors[i].AttributeRouteModel is null)
                action.Selectors.Remove(action.Selectors[i]);
        }

        if (action.Selectors.Any())
        {
            foreach (var item in action.Selectors)
            {
                var routePath = string.Concat("api/", controllerName, action.ActionName).Replace("//", "/");
                var routeModel = new AttributeRouteModel(new RouteAttribute(routePath));
                //如果沒有路由屬性
                if (item.AttributeRouteModel == null) item.AttributeRouteModel = routeModel;
            }
        }
        else
        {
            action.Selectors.Add(CreateActionSelector(controllerName, action));
        }
    }

    private SelectorModel CreateActionSelector(string controllerName, ActionModel action)
    {
        var selectorModel = new SelectorModel();
        var actionName = action.ActionName;
        string httpMethod = string.Empty;
        //是否有HttpMethodAttribute
        var routeAttributes = action.ActionMethod.GetCustomAttributes(typeof(HttpMethodAttribute), false);
        //如果標記了HttpMethodAttribute
        if (routeAttributes != null && routeAttributes.Any())
        {
            httpMethod = routeAttributes.SelectMany(m => (m as HttpMethodAttribute).HttpMethods).ToList().Distinct().FirstOrDefault();
        }
        else
        {
            var methodName = action.ActionMethod.Name.ToUpper();

            foreach (var item in httpMethods)
            {
                if (item.MethodVal.Contains(methodName))
                {
                    httpMethod = item.MethodKey;
                    break;
                }
            }
        }
        return ConfigureSelectorModel(selectorModel, action, controllerName, httpMethod);
    }

    public SelectorModel ConfigureSelectorModel(SelectorModel selectorModel, ActionModel action, string controllerName, string httpMethod)
    {
        var routePath = string.Concat("api/", controllerName, action.ActionName).Replace("//", "/");
        //給此Action新增路由
        selectorModel.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(routePath));
        //新增HttpMethod
        selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod }));
        return selectorModel;
    }
}

控制器中就很簡單了:

public class Test : IDynamicController
{
    private readonly IHttpContextAccessor _httpAccessor;
    public Test(IHttpContextAccessor httpAccessor) => _httpAccessor = httpAccessor;

    public async Task SaveData()
    => _httpAccessor.HttpContext.Response.WriteAsJsonAsync(new { _ = this.GetType() });

    public async Task DeleteData()
    => _httpAccessor.HttpContext.Response.WriteAsJsonAsync(new { _ = this.GetType() });

    public async Task QueryData()
    => _httpAccessor.HttpContext.Response.WriteAsJsonAsync(new { _ = this.GetType() });

    public async Task UpdateData()
    => _httpAccessor.HttpContext.Response.WriteAsJsonAsync(new { _ = this.GetType() });
}

只要你把基本的原理弄懂了,其實就是機械式的編碼了?

相關文章