.Net Core后端架构实战【2-实现动态路由与Dynamic API】
摘要:基于.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
之后,调用该
endpoint
的
endpoint.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并调用执行。那么我们就可以读到项目中所有注册的路由,然后导入到数据库表中