深入解析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。

image

圖1RelatedAssemblyAttribute不具有可傳遞性

六、設計總覽

綜上所述,一個應用可以分解成一組代表應用組成部分的ApplicationPart對象,派生的AssemblyPart類型體現了針對程序集的應用分解維度,它實現了IApplicationPartTypeProvider接口並將程序集中定義的類型輸出到實現的Types屬性中。作爲創建ApplicationPart對象的工廠,抽象類ApplicationPartFactory旨在提供由指定程序集承載的所有ApplicationPart對象,派生於該抽象類的DefaultApplicationPartFactory類型最終創建的是根據指定程序集創建的AssemblyPart對象。

image

圖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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章