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. 开源项目02-OSharp

    项目名称:OSharp 项目所用技术栈: osharp netstandard aspnetcore osharpns ng-alain angular等 项目简介: OSharp是一个基于.NetC ...

  2. Gin CORS 跨域请求资源共享与中间件

    Gin CORS 跨域请求资源共享与中间件 目录 Gin CORS 跨域请求资源共享与中间件 一.同源策略 1.1 什么是浏览器的同源策略? 1.2 同源策略判依据 1.3 跨域问题三种解决方案 二. ...

  3. [洛谷]P1967-最小生成树-好题推荐

    [NOIP2013 提高组] 货车运输 题目背景 NOIP2013 提高组 D1T3 题目描述 A 国有 \(n\) 座城市,编号从 \(1\) 到 \(n\),城市之间有 \(m\) 条双向道路.每 ...

  4. Git 简单实用教程

    相关链接: 码云(gitee)配置SSH密钥 码云gitee创建仓库并用git上传文件 git 上传错误This oplation equires one of the flowi vrsionsot ...

  5. 1.5 为x64dbg编写插件

    任何一个成熟的软件都会具有可扩展性,可扩展性是现代软件的一个重要特征,因为它使软件更易于维护和适应变化的需求,x64dbg也不例外其可通过开发插件的方式扩展其自身功能,x64dbg提供了多种插件接口, ...

  6. Gin 获取请求参数

    1.获取URL?后的参数(不区分请求方式) // 获取请求url ? 后的参数(url:8080/add?name=kelvin) func GetUrlParam(ctx *gin.Context) ...

  7. 记录开发中element树形控件数据应用在页面上的相关问题

    业务场景 根据后台返回数据生成角色权限的树形结构.获取节点数据后,当父节点被勾选时,所有的子节点全部被勾选,而实际上后台并没有返回当前父节点的所有子节点的ID,所以应该只有部分子节点被勾选. 下面第一 ...

  8. 开发者必看!苹果App Store重大调整:App上架必须有ICP备案号

    日前,苹果App Store迎来重大调整,即日起中国大陆上架的App必须具备有效的互联网信息服务提供者(ICP)备案号. 简单说,新App现在需要填写备案号才能提审,这就要求开发者应用需有备案号,另外 ...

  9. 【Flink入门修炼】1-2 Mac 搭建 Flink 源码阅读环境

    在后面学习 Flink 相关知识时,会深入源码探究其实现机制.因此,需要现在本地配置好源码阅读环境. 本文搭建环境: Mac M1(Apple Silicon) Java 8 IDEA Flink 官 ...

  10. NC50454 A Simple Problem with Integers

    题目链接 题目 题目描述 给定数列 \(a[1],a[2], \dots,a[n]\) ,你需要依次进行q个操作,操作有两类: 1 l r x:给定l,r,x,对于所有 \(i \in[l,r]\) ...