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. JS ----- JS原型与原型链终极详解(一)

    一. 普通对象与函数对象 JavaScript 中,万物皆对象!但对象也是有区别的.分为普通对象和函数对象,Object .Function 是 JS 自带的函数对象.下面举例说明 var o1 = ...

  2. go中channel源码剖析

    channel 前言 设计的原理 共享内存 csp channel channel的定义 源码剖析 环形队列 创建 写入数据 读取数据 channel的关闭 优雅的关闭 M个receivers,一个s ...

  3. 通过docker-compose搭建mongo的replica set高可用

    通过docker-compose搭建mongo的replica set高可用 前言 备份数据 备份数据到本地 数据恢复 集群搭建 生成keyFile 创建yml文件 初始化副本集 增加副本集 将节点初 ...

  4. gym.spaces中找不到prng解决方案

    gym.spaces中找不到prng解决方案 在运行飞桨MADDPG问题是遇到模型无法导入不存的的问题: ModuleNotFoundError: No module named 'multiagen ...

  5. 驱动开发:WinDBG 枚举SSDT以及SSSDT地址

    在前面的博文<驱动开发:内核读取SSDT表基址>中已经教大家如何寻找SSDT表基地址了,今天给大家分享两个适用于WinDBG调试器上的脚本文件,该脚本文件可以很好的枚举出当前系统内的SSD ...

  6. jetbrains 系列 terminal history 设置

    之前的版本中 jetbrains 的 terminal 使用的是 ~/.zsh_history, 改版后使用的不是一个 history, 就会出现在 iterm2 中使用的 command, 在 py ...

  7. 会话跟踪技术之SESSION

    会话跟踪技术之SESSION 一般来说,登录信息既可以存储在session中,也可以存储在cookie中,他们之间的差别在于session可以方便的存取多种数据类型,而cookie只支持字符串类型,同 ...

  8. U390630 分考场题解

    题目链接:U390630 分考场 本题来自于2019年蓝桥杯国赛的题.在洛谷上也被标为了假题.原因是首先官方在需要输出浮点数的情况下,并没有开启spj,并且官方所给的数据当中,总有一两个数据以不知道到 ...

  9. 小知识:在Exadata平台上使用ExaWatcher收集信息

    在非Exadata平台上,我们通常会使用DBA已经很熟悉的OSW,如果有不熟悉的朋友可以参考我之前的随笔初步了解OSW: OSW 快速安装部署 OSW Analyzer分析oswbb日志发生异常 而在 ...

  10. 【LGR-156-Div.3】洛谷网校 8 月普及组月赛 I & MXOI Round 1 & 飞熊杯 #2(同步赛)

    [LGR-156-Div.3]洛谷网校 8 月普及组月赛 I & MXOI Round 1 & 飞熊杯 #2(同步赛) \(T1\) luogu P9581 宝箱 \(100pts\) ...