.NET 7.0 架构实战 动态路由与Dynamic API

引言

《.NET 7.0+WebAPI 后端架构实战》使用过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

源码如下图所示:

.NET 7.0 架构实战 动态路由与Dynamic API

1、erifyRoutingServicesAreRegistered用于验证路由服务是否已注册到容器内部

2、判断在请求管道的共享数据字典的Properties中是否有GlobalEndpointRouteBuilderKey的键,如果没有则New一个新的终结点路由构建者对象,并将EndpointRouteBuilder添加到共享字典中。后面UseEndpoints(Action<IEndpointRouteBuilder> configure)执行时,会将前面New的DefaultEndpointRouteBuilder 实例取出,并进一步配置它:configure(EndpointRouteBuilder实例)

3、将EndpointRoutingMiddleware中间件注册到管道中,该中间件根据请求和Url匹配最佳的Endpoint,然后将该终结点交由EndpointMiddleware 处理。

UseEndpoints

源码如下图所示:

.NET 7.0 架构实战 动态路由与Dynamic API

1、VerifyEndpointRoutingMiddlewareIsRegistered方法将EndpointRouteBuilder从请求管道的共享字典中取出,如果没有则说明之前没有调用UseRouting(),所以调用UseEndpoints()之前要先调用UseRouting()VerifyEndpointRoutingMiddlewareIsRegistered方法如下图所示:

.NET 7.0 架构实战 动态路由与Dynamic API

2、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

我们先看下它的源码:

.NET 7.0 架构实战 动态路由与Dynamic API

我们可以写一个类继承它,实现它的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; }
}

1、ApiExplorer可以用来配置控制器的组信息还有可见性

2、Controllers可以获取Controller的相关信息,再借助IControllerModelConvention对其进行定制扩展

3、Filters存放的都是空接口,起到标记作用,换句话说就是在请求管道构建的时候用于判断是否为Filter类

4、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会被正确扫描并添加到终结点中:

.NET 7.0 架构实战 动态路由与Dynamic API

一个(控制器)类如果加上[NonController]就不会被注册到路由中