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应用的模块化设计[上篇]的更多相关文章

  1. asp.net core mvc权限控制:分配权限

    前面的文章介绍了如何进行权限控制,即访问控制器或者方法的时候,要求当前用户必须具备特定的权限,但是如何在程序中进行权限的分配呢?下面就介绍下如何利用Microsoft.AspNetCore.Ident ...

  2. ASP.NET Core MVC之Serilog日志处理,你了解多少?

    前言 本节我们来看看ASP.NET Core MVC中比较常用的功能,对于导入和导出目前仍在探索中,项目需要自定义列合并,所以事先探索了如何在ASP.NET Core MVC进行导入.导出,更高级的内 ...

  3. ASP.NET Core MVC之ViewComponents(视图组件)

    前言 大概一个来星期未更新博客了,久违了各位,关于SQL Server性能优化会和ASP.NET Core MVC穿插来讲,如果你希望我分享哪些内容可以在评论下方提出来,我会筛选并看看技术文档来对你的 ...

  4. 你想要的都在这里,ASP.NET Core MVC四种枚举绑定方式

    前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...

  5. ASP.NET Core MVC 模型绑定用法及原理

    前言 查询了一下关于 MVC 中的模型绑定,大部分都是关于如何使用的,以及模型绑定过程中的一些用法和概念,很少有关于模型绑定的内部机制实现的文章,本文就来讲解一下在 ASP.NET Core MVC ...

  6. ASP.NET Core MVC 控制器创建与依赖注入

    本文翻译自<Controller activation and dependency injection in ASP.NET Core MVC>,由于水平有限,故无法保证翻译完全准确,欢 ...

  7. 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. ...

  8. 跨平台应用集成(在ASP.NET Core MVC 应用程序中集成 Microsoft Graph)

    作者:陈希章 发表于 2017年6月25日 谈一谈.NET 的跨平台 终于要写到这一篇了.跨平台的支持可以说是 Office 365 平台在设计伊始就考虑的目标.我在前面的文章已经提到过了,Micro ...

  9. ASP.NET Core MVC 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide

    一.概述 ASP.NET Core MVC 提供了基于角色( Role ).声明( Chaim ) 和策略 ( Policy ) 等的授权方式.在实际应用中,可能采用部门( Department , ...

  10. ASP.NET Core 入门教程 4、ASP.NET Core MVC控制器入门

    一.前言 1.本教程主要内容 ASP.NET Core MVC控制器简介 ASP.NET Core MVC控制器操作简介 ASP.NET Core MVC控制器操作简介返回类型简介 ASP.NET C ...

随机推荐

  1. 2022美亚杯个人wp

    检材文件下载链接:https://pan.baidu.com/s/1kg8FMeMaj6BIBmuvUZHA3Q?pwd=ngzs 提取码:ngzs 个人赛与团队赛下载文件解压密码:MeiyaCup2 ...

  2. Go 循环之for循环,仅此一种

    Go 循环之for循环,仅此一种 目录 Go 循环之for循环,仅此一种 一.for 循环介绍 二.for 循环结构 2.1 基本语法结构 2.2 省略初始值 2.3 省略初始语句和结束语句 2.4 ...

  3. HEVC扩展备用安装方法

    这个玩意微软商店免费但是下架了,购买需要RMB 安装 转到 https://store.rg-adguard.net/ 在左侧的下拉菜单选择"ProductId" 把链接中&quo ...

  4. 微信小程序网页嵌入开发

    无脑开发 下载微信开发者工具 新建一个项目找到index开头的进去全选删除粘贴下面代码 <!-- html --> <!-- 指向嵌入外部链接的地址 --> <web-v ...

  5. 【Jmeter】Request1输出作为Request2输入-后置处理器

    [Jmeter]基础介绍-详细 接上文,继续介绍Jmeter,本文关注点为如何解决上文中提到的第一个问题,即: 需要实现Request1的返回作为Request2的RequestBody或Header ...

  6. 4.3 Windows驱动开发:监控进程与线程对象操作

    在内核中,可以使用ObRegisterCallbacks这个内核回调函数来实现监控进程和线程对象操作.通过注册一个OB_CALLBACK_REGISTRATION回调结构体,可以指定所需的回调函数和回 ...

  7. 7.0 Python 面向对象编程

    python是一种面向对象的编程语言,面向对象编程(Object-Oriented Programming,OOP)是一种编程思想,其核心概念是"对象".对象是指一个具有特定属性和 ...

  8. Docker从认识到实践再到底层原理(六-2)|Docker容器操作实例

    前言 那么这里博主先安利一些干货满满的专栏了! 首先是博主的高质量博客的汇总,这个专栏里面的博客,都是博主最最用心写的一部分,干货满满,希望对大家有帮助. 高质量博客汇总 然后就是博主最近最花时间的一 ...

  9. Azure - 机器学习:创建机器学习所需资源,配置工作区

    本文中你可以创建使用 Azure 机器学习所需的资源,包含工作区和计算实例. 关注TechLead,分享AI全维度知识.作者拥有10+年互联网服务架构.AI产品研发经验.团队管理经验,同济本复旦硕,复 ...

  10. ABC 317 A - G

    ABC 317 A - G 代码去 Atcoder 全部提交搜索 Std_Code 查看代码 懒人专用 A $ p_i $ 升序,找最小的 $ i $ 满足 $ p_i + h \ge x $ 直接枚 ...