深入解析ASP.NET Core MVC应用的模块化设计[上篇]
ASP.NET Core MVC的“模块化”设计使我们可以构成应用的基本单元Controller定义在任意的模块(程序集)中,并在运行时动态加载和卸载。这种为“飞行中的飞机加油”的方案是如何实现的呢?该系列的两篇文章将关注于这个主题,本篇着重介绍“模块化”的总体设计,下篇我们将演示将介绍“分散定义Controller”的N种实现方案。
一、ApplicationPart & AssemblyPart
二、ApplicationPartFactory & DefaultApplicationPartFactory
三、IApplicationFeatureProvider & IApplicationFeatureProvider<TFeature>
四、ControllerFeatureProvider
五、ApplicationPartManager
六、设计总览
七、有效Controller类型的提取
一、ApplicationPart & AssemblyPart
MVC构建了一个抽象的模型来描述应用的组成。原则上来说,我们可以根据不同维度来描述当前的MVC应用由哪些部分构成,任何维度针下针对应用组成部分的描述都体现为一个ApplicationPart对象。因为没有限制对应用进行分解的维度,所以“应用组成部分”也是一个抽象的概念,它具有怎样的描述也是不确定的。也正是因为如此,对应的ApplicationPart类型也是一个抽象类型,我们只要任何一个ApplicationPart对象具有一个通过Name属性表示的名称就可以。
public abstract class ApplicationPart
{
public abstract string Name { get; }
}
对于任何一个.NET Core应用来说,程序集永远是基本的部署单元,所以一个应用从部署的角度来看就是一组程序集。如果采用这种应用分解方式,我们可以将一个程序集视为应用一个组成部分,并可以通过如下这个AssemblyPart类型来表示。
public class AssemblyPart : ApplicationPart, IApplicationPartTypeProvider
{
public Assembly Assembly { get; }
public IEnumerable<TypeInfo> Types => Assembly.DefinedTypes;
public override string Name => Assembly.GetName().Name; public AssemblyPart(Assembly assembly) => Assembly = assembly;
}
如上面的代码片段所示,一个AssemblyPart对象是对一个描述程序集的Assembly对象的封装,其Name属性直接返回程序集的名称。AssemblyPart类型还是实现了IApplicationPartTypeProvider接口,如下面的代码片段所示,该接口通过提供的Types属性提供当前定义在当前ApplicationPart范围内容的所有类型。AssemblyPart类型的Types属性会返回指定程序集中定义的所有类型。
public interface IApplicationPartTypeProvider
{
IEnumerable<TypeInfo> Types { get; }
}
二、ApplicationPartFactory & DefaultApplicationPartFactory
如下所示的抽象类ApplicationPartFactory表示创建ApplicationPart对象的工厂。如代码片段所示,该接口定义了唯一的GetApplicationParts方法从指定的程序集中解析出表示应用组成部分的一组ApplicationPart对象。
public abstract class ApplicationPartFactory
{
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
}
如下所示的DefaultApplicationPartFactory是ApplicationPartFactory最常用的派生类。如代码片段所示,DefaultApplicationPartFactory类型实现的GetDefaultApplicationParts方法返回的ApplicationPart集合中只包含根据指定程序集创建的AssemblyPart对象。
public class DefaultApplicationPartFactory : ApplicationPartFactory
{
public static DefaultApplicationPartFactory Instance { get; } = new DefaultApplicationPartFactory(); public override IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly) => GetDefaultApplicationParts(assembly); public static IEnumerable<ApplicationPart> GetDefaultApplicationParts(Assembly assembly)
{
yield return new AssemblyPart(assembly);
}
}
值得一提的是,ApplicationPartFactory类型还定义了如上这个名为GetApplicationPartFactory的静态方法,它会返回指定程序集对应的ApplicationPartFactory对象。这个方法涉及到如下这个ProvideApplicationPartFactoryAttribute特性,我们可以利用这个特性注册一个ApplicationPartFactory类型。GetApplicationPartFactory方法首先会从指定的程序集中提取这样一个特性,如果该特性存在,该方法会根据其GetFactoryType方法返回的类型创建返回的ApplicationPartFactory对象,否则它最终返回的就是DefaultApplicationPartFactory类型的静态属性Instance返回的DefaultApplicationPartFactory对象。
public abstract class ApplicationPartFactory
{
public static ApplicationPartFactory GetApplicationPartFactory(Assembly assembly)
{
var attribute = CustomAttributeExtensions.GetCustomAttribute<ProvideApplicationPartFactoryAttribute>(assembly);
return attribute == null
? DefaultApplicationPartFactory.Instance
: (ApplicationPartFactory)Activator.CreateInstance(attribute.GetFactoryType());
}
}
三、IApplicationFeatureProvider & IApplicationFeatureProvider<TFeature>
了解当前应用由哪些部分组成不是我们的目的,我们最终的意图是从构成应用的所有组成部分中搜集我们想要的信息,比如整个应用范围的所有有效Controller类型。我们将这种需要在应用全局范围内收集的信息抽象为“特性(Feature)”,那么我们最终的目的就变成了:在应用全局范围内构建某个特性。如下这个没有任何成员定义的标记接口IApplicationFeatureProvider代表特性的构建者。
public interface IApplicationFeatureProvider
{}
我们一般将某种特性定义成一个对应的类型,所以有了如下这个IApplicationFeatureProvider<TFeature>类型,泛型参数TFeature代表需要构建的特性类型。如代码片段所示,该接口定义了唯一的PopulateFeature方法来“完善”预先创建的特性对象(feature参数),该方法作为输入的第一个参数(parts)表示应用所有组成部分的ApplicationPart对象集合。
public interface IApplicationFeatureProvider<TFeature> : IApplicationFeatureProvider
{
void PopulateFeature(IEnumerable<ApplicationPart> parts, TFeature feature);
}
四、ControllerFeatureProvider
ControllerFeatureProvider类型实现了IApplicationFeatureProvider<ControllerFeature >接口,也正是它帮助我们解析出应用范围内所有有效的Controller类型。作为特性类型的ControllerFeature具有如下的定义,从所有应用组成部分收集的Controller类型就被存放在Controllers属性返回的集合中。
public class ControllerFeature
{
public IList<TypeInfo> Controllers { get; }
}
在正式介绍ControllerFeatureProvider针对有效Controller类型的解析逻辑之前,我们得先知道一个有效的Controller类型具有怎样的特性。“约定优于配置”是MVC框架的主要涉及原则,名称具有“Controller”后缀(不区分大小写)的类型会自动成为候选的Controller类型。如果某个类型的名称没有采用“Controller”后缀,倘若类型上面标注了ControllerAttribute特性,它依然是候选的Controller类型。用来定义Web API的ApiControllerAttribute是ControllerAttribute的派生类。
[AttributeUsage((AttributeTargets) AttributeTargets.Class, AllowMultiple=false, Inherited=true)]
public class ControllerAttribute : Attribute
{} [AttributeUsage((AttributeTargets) (AttributeTargets.Class | AttributeTargets.Assembly), AllowMultiple=false, Inherited=true)]
public class ApiControllerAttribute : ControllerAttribute, IApiBehaviorMetadata, IFilterMetadata
{}
除了满足上面介绍的命名约定或者特性标注要求外,一个有效的Controller类型必须是一个公共、非抽象的、非泛型的实例类型,所以非公有类型、静态类型、泛型类型和抽象类型均为无效的Controller类型。如果一个类型上标注了NonControllerAttribute特性,它自然也不是有效的Controller类型。由于NonControllerAttribute特性支持继承(Inherited=true),对于某个标注了该特性的类型来说,所有派生于它的类型都不是有效的Controller类型。
[AttributeUsage((AttributeTargets) AttributeTargets.Class, AllowMultiple=false, Inherited=true)]
public sealed class NonControllerAttribute : Attribute
{}
如下所示的是ControllerFeatureProvider类型的完整定义,上述的针对有效Controller类型的判断就是实现在IsController方法中。在实现的PopulateFeature方法中,它从提供的ApplicationPart对象中提取出对应类型同时实现了IApplicationPartTypeProvider接口的提取出来(AssemblyPart就实现了这个接口),然后从它们提供的类型中按照IsController方法提供的规则筛选出有效的Controller类型,并添加到ControllerFeature对象的Controllers属性返回的列表中。
public class ControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
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);
}
}
}
} protected virtual bool IsController(TypeInfo typeInfo)
{
if (!typeInfo.IsClass)
{
return false;
}
if (typeInfo.IsAbstract)
{
return false;
}
if (!typeInfo.IsPublic)
{
return false;
}
if (typeInfo.ContainsGenericParameters)
{
return false;
}
if (typeInfo.IsDefined(typeof(NonControllerAttribute)))
{
return false;
} if (!typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && !typeInfo.IsDefined(typeof(ControllerAttribute)))
{
return false;
} return true;
}
}
五、ApplicationPartManager
在基于应用所有组成部分基础上针对某种特性的构建是通过ApplicationPartManager对象驱动实现的,我们很有必要了解该类型的完整定义。我们可以将表示应用组成部分的ApplicationPart对象添加到ApplicationParts属性表示的列表中,而FeatureProviders属性表示的列表则用于存储注册的IApplicationFeatureProvider对象。用于构建特性对象的PopulateFeature<TFeature>方法会实现了IApplicationFeatureProvider<TFeature>接口的IApplicationFeatureProvider提取出来,并调用其PopulateFeature方法完善指定的TFeature对象。
public class ApplicationPartManager
{
public IList<IApplicationFeatureProvider> FeatureProviders { get; } = new List<IApplicationFeatureProvider>();
public IList<ApplicationPart> ApplicationParts { get; } = new List<ApplicationPart>(); public void PopulateFeature<TFeature>(TFeature feature)
{
foreach (var provider in FeatureProviders.OfType<IApplicationFeatureProvider<TFeature>>())
{
provider.PopulateFeature(ApplicationParts, feature);
}
} internal void PopulateDefaultParts(string entryAssemblyName)
{
var assemblies = GetApplicationPartAssemblies(entryAssemblyName);
var seenAssemblies = new HashSet<Assembly>();
foreach (var assembly in assemblies)
{
if (!seenAssemblies.Add(assembly))
{
continue;
}
var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly);
foreach (var applicationPart in partFactory.GetApplicationParts(assembly))
{
ApplicationParts.Add(applicationPart);
}
}
} private static IEnumerable<Assembly> GetApplicationPartAssemblies(string entryAssemblyName)
{
var entryAssembly = Assembly.Load(new AssemblyName(entryAssemblyName));
var assembliesFromAttributes = entryAssembly
.GetCustomAttributes<ApplicationPartAttribute>()
.Select(name => Assembly.Load(name.AssemblyName))
.OrderBy(assembly => assembly.FullName, StringComparer.Ordinal)
.SelectMany(GetAsemblyClosure);
return GetAsemblyClosure(entryAssembly).Concat(assembliesFromAttributes);
} private static IEnumerable<Assembly> GetAsemblyClosure(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;
}
}
}
定义在ApplicationPartManager类型中的内部方法PopulateDefaultParts同样重要,该方法会根据指定的入口程序集名称来构建组成应用的所有ApplicationPart对象。PopulateDefaultParts方法构建的ApplicationPart对象类型都是AssemblyPart,所以如何得到组成当前应用的程序集成了该方法的核心逻辑,这一逻辑实现在GetApplicationPartAssemblies方法中。
如上面的代码片段所示,GetApplicationPartAssemblies方法返回的程序集除了包含指定的入口程序集之外,还包括通过标注在入口程序集上的ApplicationPartAttribute特性指定的程序集。除此之外,如果前面这些程序集通过标注如下这个RelatedAssemblyAttribute特性指定了关联程序集,这些程序集同样会包含在返回的程序集列表中。
[AttributeUsage((AttributeTargets) AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class RelatedAssemblyAttribute : Attribute
{
public string AssemblyFileName { get; }
public RelatedAssemblyAttribute(string assemblyFileName);
public static IReadOnlyList<Assembly> GetRelatedAssemblies(Assembly assembly, bool throwOnError);
}
从PopulateDefaultParts方法的定义可以看出,我们可以在程序集上标注ApplicationPartAttribute和RelatedAssemblyAttribute特性的方式将非入口程序集作为应用ApplicationPart。这里需要着重强调的是:ApplicationPartAttribute特性只能标注到入口程序集中,而RelatedAssemblyAttribute特性只能标注到入口程序集以及ApplicationPartAttribute特性指向的程序集上,该特性不具有可传递性。以图1为例,我们在入口程序集A上标注了一个指向程序集B的ApplicationPartAttribute特性,同时在程序集B和C上标注了一个分别指向程序集C和D的RelatedAssemblyAttribute特性,那么作为应用ApplicationPart的程序集只包含A、B和C。

图1RelatedAssemblyAttribute不具有可传递性
六、设计总览
综上所述,一个应用可以分解成一组代表应用组成部分的ApplicationPart对象,派生的AssemblyPart类型体现了针对程序集的应用分解维度,它实现了IApplicationPartTypeProvider接口并将程序集中定义的类型输出到实现的Types属性中。作为创建ApplicationPart对象的工厂,抽象类ApplicationPartFactory旨在提供由指定程序集承载的所有ApplicationPart对象,派生于该抽象类的DefaultApplicationPartFactory类型最终创建的是根据指定程序集创建的AssemblyPart对象。

图2 ApplicationPartManager及其相关类型
我们可以利用ApplicationPartManager对象针对组成当前应用的ApplicationPart对象上构建某种类型的特性。具体的特性构建通过注册的一个或者多个IApplicationFeatureProvider对象完成,针对具体特类型的IApplicationFeatureProvider<TFeature>接口派生于该接口。针对Controller类型的提取实现在ControllerFeatureProvider类型中,它实现了IApplicationFeatureProvider<ControllerFeature>接口,提取出来的Controller类型就封装在ControllerFeature对象中。这里提及的接口、类型以及它们之间的关系体现在如图2所示的UML中。
七、有效Controller类型的提取
前面的内容告诉我们,利用ApplicationPartManager对象并借助注册的ControllerFeatureProvider可以帮助我们成功解析出当前应用范围内的所有Controller类型。那么MVC框架用来解析有效Controller类型的是怎样一个ApplicationPartManager对象呢?
ApplicationPartManager会作为MVC框架的核心服务被注册到依赖注入框架中。如下面的代码片段所示,当AddMvcCore扩展方法被执行的时候,它会重用已经注册的ApplicationPartManager实例。如果这样的服务实例不曾注册过,该方法会创建一个ApplicationPartManager对象。AddMvcCore方法接下来会提取出表示当前承载上下文的IWebHostEnvironment对象,并将其ApplicationName属性作为入口程序集调用ApplicationPartManager对象的内部方法PopulateDefaultParts构建出组成当前应用的所有ApplicationPart(AssemblyPart)。
public static class MvcCoreServiceCollectionExtensions
{
public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services)
{
…
var manager = GetServiceFromCollection<ApplicationPartManager>(services);
if (manager == null)
{
manager = new ApplicationPartManager();
IWebHostEnvironment environment = GetServiceFromCollection<IWebHostEnvironment>(services);
var applicationName = environment?.ApplicationName;
if (!string.IsNullOrEmpty(applicationName))
{
manager.PopulateDefaultParts(applicationName);
}
}
if (!manager.FeatureProviders.OfType<ControllerFeatureProvider>().Any())
{
manager.FeatureProviders.Add(new ControllerFeatureProvider());
}
services.TryAddSingleton(manager);
return new MvcCoreBuilder(services, applicationPartManager);
}
private static T GetServiceFromCollection<T>(IServiceCollection services)
{
return (T)services.LastOrDefault(d => d.ServiceType == typeof(T))?.ImplementationInstance;
}
}
接下来用于解析Controller类型的ControllerFeatureProvider对象会被创建出来并注册到ApplicationPartManager对象上。这个ApplicationPartManager对象将作为单例服务被注册到依赖注入框架中。面向Controller的MVC编程模型利用ControllerActionDescriptorProvider对象来提供描述Action元数据的ActionDescriptor对象。如下面的代码片段所示,该类型的构造函数中注入了两个对象,其中ApplicationPartManager对象用来提取当前应用所有有效的Controller类型,ApplicationModelFactory对象则在此基础上进一步构建出MVC应用模型(Application Model),Action元数据就是根据此应用模型创建出来的。具体来说,针对Controller类型的解析实现在私有方法GetControllerTypes中。
internal class ControllerActionDescriptorProvider : IActionDescriptorProvider
{
public int Order => -1000;
private readonly ApplicationPartManager _partManager;
private readonly ApplicationModelFactory _applicationModelFactory; public ControllerActionDescriptorProvider(ApplicationPartManager partManager, ApplicationModelFactory applicationModelFactory)
{
_partManager = partManager;
_applicationModelFactory = applicationModelFactory;
} public void OnProvidersExecuted(ActionDescriptorProviderContext context);
public void OnProvidersExecuting(ActionDescriptorProviderContext context); private IEnumerable<TypeInfo> GetControllerTypes()
{
var feature = new ControllerFeature();
_partManager.PopulateFeature<ControllerFeature>(feature);
return (IEnumerable<TypeInfo>) feature.Controllers;
}
}
深入解析ASP.NET Core MVC应用的模块化设计[上篇]的更多相关文章
- asp.net core mvc权限控制:分配权限
前面的文章介绍了如何进行权限控制,即访问控制器或者方法的时候,要求当前用户必须具备特定的权限,但是如何在程序中进行权限的分配呢?下面就介绍下如何利用Microsoft.AspNetCore.Ident ...
- ASP.NET Core MVC之Serilog日志处理,你了解多少?
前言 本节我们来看看ASP.NET Core MVC中比较常用的功能,对于导入和导出目前仍在探索中,项目需要自定义列合并,所以事先探索了如何在ASP.NET Core MVC进行导入.导出,更高级的内 ...
- ASP.NET Core MVC之ViewComponents(视图组件)
前言 大概一个来星期未更新博客了,久违了各位,关于SQL Server性能优化会和ASP.NET Core MVC穿插来讲,如果你希望我分享哪些内容可以在评论下方提出来,我会筛选并看看技术文档来对你的 ...
- 你想要的都在这里,ASP.NET Core MVC四种枚举绑定方式
前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...
- ASP.NET Core MVC 模型绑定用法及原理
前言 查询了一下关于 MVC 中的模型绑定,大部分都是关于如何使用的,以及模型绑定过程中的一些用法和概念,很少有关于模型绑定的内部机制实现的文章,本文就来讲解一下在 ASP.NET Core MVC ...
- ASP.NET Core MVC 控制器创建与依赖注入
本文翻译自<Controller activation and dependency injection in ASP.NET Core MVC>,由于水平有限,故无法保证翻译完全准确,欢 ...
- 008.Adding a model to an ASP.NET Core MVC app --【在 asp.net core mvc 中添加一个model (模型)】
Adding a model to an ASP.NET Core MVC app在 asp.net core mvc 中添加一个model (模型)2017-3-30 8 分钟阅读时长 本文内容1. ...
- 跨平台应用集成(在ASP.NET Core MVC 应用程序中集成 Microsoft Graph)
作者:陈希章 发表于 2017年6月25日 谈一谈.NET 的跨平台 终于要写到这一篇了.跨平台的支持可以说是 Office 365 平台在设计伊始就考虑的目标.我在前面的文章已经提到过了,Micro ...
- ASP.NET Core MVC 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide
一.概述 ASP.NET Core MVC 提供了基于角色( Role ).声明( Chaim ) 和策略 ( Policy ) 等的授权方式.在实际应用中,可能采用部门( Department , ...
- ASP.NET Core 入门教程 4、ASP.NET Core MVC控制器入门
一.前言 1.本教程主要内容 ASP.NET Core MVC控制器简介 ASP.NET Core MVC控制器操作简介 ASP.NET Core MVC控制器操作简介返回类型简介 ASP.NET C ...
随机推荐
- css3中的圆角border-radius
css3的属性应该加上浏览器前缀 不同的浏览器内核不同,因此css的前缀不同. 常见的几种浏览器内核 火狐浏览器 Geoko内核 前缀是 -mox- 谷歌浏览器, Webkit内核 前缀是 -wekb ...
- RN 动态渲染列表
写在组件中 想要图片出来还应该给图片宽高哈!! alignItems: 'center', //水平居中 动态渲染列表 返回的是一个数组 网络图片的渲染方式 <Image source={{ur ...
- Ant Design Vue中TreeSelect详解
<template> <a-tree-select v-model:value="value" style="width: 320px" :t ...
- 【K哥爬虫普法】老铁需要车牌靓号吗?判刑的那种
我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...
- Android Graphics 显示系统 - 如何模拟多(物理)显示屏?
" 本着花小钱办大事,不花钱也办事的原则,为了避免花钱买设备,那如何更便捷地学习/测试Android多屏显示的内容呢?本文就给大家介绍一种模拟Android多个物理屏幕显示的方法." ...
- ResponseUtil jackson 转换问题;返回结果与 Bean 之间的转换,推荐使用convertValue
/** * 若成功往往有data数据 * 示例: * UserBase userBase = new UserBase(); * userBase.setUserName("李昱鑫" ...
- FDConnection的事务测试讲解。。
总之用事务的宗旨是: 1.不用嵌套事务EnableNested设置为False 2.事务一定要回滚,避免发生异常的情况下,没有回滚 造成,不可估量的错误. try frmClientDm.MyMain ...
- 麒麟V10虚拟机安装(详细)
现在国企和央企单位都在做国产化适配工作,服务器采用:中科曙光(海光Hygon).中科德泰(龙芯Loongson).宝德(鲲鹏Kunpeng)等国产配备国产处理器的服务器:数据库采用:人大金仓(King ...
- 思维分析逻辑 5 DAY
目录 如何分析 结构分析 对比分析 时间序列 相关性分析 机器学习 报告撰写 报告撰写三原则 标准化报告的组成 AB测试 AB测试流程 AB测试注意事项 如何分析 结构分析 对比分析 对比分析:所有的 ...
- Java21 + SpringBoot3使用Spring Security时如何在子线程中获取到认证信息
目录 前言 原因分析 解决方案 方案1:手动设置线程中的认证信息 方案2:使用DelegatingSecurityContextRunnable创建线程 方案3:修改Spring Security安全 ...