深入解析ASP.NET Core MVC应用的模块化设计[上篇]

时间:2024-03-05 08:49:10

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;
    }
}