ASP.NET Core MVC應用模型的構建[1]: 應用的藍圖

我個人覺得這是ASP.NET Core MVC框架體系最核心的部分。原因很簡單,MVC框架建立在ASP.NET Core路由終結點上,它最終的目的就是將每個Action方法映射爲一個或者多個路由終結點,路由終結點根據附加在Action上的若干元數據構建而成。爲了構建描述當前應用所有Action的元數據,MVC框架會提取出定義在當前應用範圍內的所有Controller類型,並進一步構建出基於Controller的應用模型。應用模型不僅僅是構建Action元數據的基礎,承載API的應用還可以利用它自動生成API開發文檔,一些工具甚至可以利用應用模型自動生成消費API的客戶端代碼。這篇文章大概是兩年之前寫的,可能一些技術細節在最新版本的ASP.NET Core MVC已經發生了改變,但總體設計依然如此。

不論是面向Controller的MVC編程模型,還是面向頁面的Razor Pages編程模型,客戶端請求訪問的目標都是某個Action,所以MVC框架的核心功能就是將請求路由到正確的Action,並通過執行目標Action的方式完成請求當前請求的處理。目標Action應該如何執行由描述它的元數據來決定,而這樣的元數據是通過ApplicationModel類型標識的應用模型構建出來的。應用模型爲MVC應用構建了一個基於Controller的藍圖,我們先從宏觀的角度來看看這張藍圖是如何繪製的。

一、 總體設計
二、ApplicationModel
三、IApplicationModelProvider
四、IApplicationModelConvention
五、其他約定
六、ApplicationModelFactory

一、 總體設計

圖1基本體現了MVC框架構建應用模型的總體設計。代表使用模型的ApplicationModel對象是通過作爲工廠的ApplicationModelFactory對象構建的,但是具體的構建任務卻落在註冊的一系列IApplicationModelProvider和IApplicationModelConvention對象上。

clip_image002

圖1 ApplicationModel的構建模型

具體來說,ApplicationModelFactory工程會先創建一個空的ApplicationModel對象,並利用註冊的IApplicationModelProvider對象對這個對象進行完善和修正。在此之後,代表默認約定的一系列IApplicationModelConvention對象會依次被執行,它們會將針對應用模型的約定規則應用到同一個ApplicationModel對象上。經過這兩個加工環節之後得到的ApplicationModel最終成爲描述應用模型的藍圖。

二、ApplicationModel

表示應用模型的ApplicationModel對象不僅是常見Action元數據的依據,同時還有其他重要的用途。由於ApplicationModel對象繪製了整個應用的藍圖,我們經常不僅可以利用它來生成結構化API文檔(比如Swagger),還可以利用它提供的元數據生成調用API的客戶端代碼。通過ApplicationModel表示的應用模型總體上具有如圖2所示的結構:一個ApplicationModel對象包含多個描述Controller的ControllerModel對象,一個ControllerModel包含多個ActionModel和PropertyModel對象,ActionModel和PropertyModel是對定義在Controller類型中的Action方法和屬性的描述。表示Action方法的ActionModel對象利用ParameterModel描述其參數

clip_image004

圖2 應用模型總體結構

三、IApplicationModelProvider

在軟件設計中我們經常會遇到這樣的場景:我們需要構建一個由若干不同元素組成的複合對象,不同的組成元素具有不同的構建方式,MVC框架幾乎基於採用了同一種模式來處理這樣的場景。舉個簡單的例子:對象Foo需要實現的功能需要委託一組Bar對象來實現。MVC框架針對這種需求大都採用如圖3所示模式來實現:Foo先創建一個上下文,並提供必要的輸入,然後驅動每個Bar對象在這個上下文中完成各自的處理任務。所有Bar對象針對數據和狀態的修改,以及產生的輸出均體現在這個共享的上下文中,所有對象最終通過這個上下文就可以得到應有的狀態或者所需的輸出。

clip_image006

圖3 基於共享上下文的多對象協作模式(單操作)

有時候我們甚至可以將Bar對象的操作分成兩個步驟進行,比如我們將針對這兩個步驟的操作分別命名爲Executing和Executed。如圖4所示,在創建共享上下文之後,Foo對象先按序執行每一個Bar對象的Executing操作,最後再反向執行每個Bar對象的Executed操作,所有的操作均在同一個上下文中執行。

clip_image008

圖4 基於共享上下文的多對象協作模式(兩階段)

瞭解了上面所述的基於共享上下文的多對象協作對象構建模式之後,讀者朋友們對於IApplicationModelProvider接口定義就很好理解了。如下面的代碼片段所示,IApplicationModelProvider接口定了Order屬性來決定了自身的執行順序,而OnProvidersExecuting和OnProvidersExecuted方法分別完成針對Action元數據構建的兩階段任務。

public interface IApplicationModelProvider
{
    int Order { get; }
    void OnProvidersExecuted(ApplicationModelProviderContext context);
    void OnProvidersExecuting(ApplicationModelProviderContext context);
}

這裏作爲構建應用模型的執行上下文通過如下這個ApplicationModelProviderContext類型表示。如代碼片段所示,ApplicationModelProviderContext類型定義了兩個屬性,其中ControllerTypes屬性表示的列表提供了當前應用所有有效的Controller類型,而Result屬性返回的ApplicationModel對象自然代表“待改造”的應用模型。

public class ApplicationModelProviderContext
{
    public IEnumerable<TypeInfo> 	ControllerTypes { get; }
    public ApplicationModel 		Result { get; }

    public ApplicationModelProviderContext(IEnumerable<TypeInfo> controllerTypes);
}

MVC框架提供如下所示的幾個針對IApplicationModelProvider接口的實現類型。對於最終用於描繪當前MVC應用的ApplicationModel對象,其承載的元數據絕大部分是由DefaultApplicationModelProvider對象提供的。AuthorizationApplicationModelProvider和CorsApplicationModelProvider主要提供針對授權和。而ApiBehaviorApplicationModelProvider則負責提供與API相關的描述信息。這些具體實現類型都是內部類型。

  • DefaultApplicationModelProvider:提供構成應用模型的絕大部分元數據。
  • AuthorizationApplicationModelProvider:提供與授權相關元數據。
  • CorsApplicationModelProvider:提供與跨域資源共享(CORS)相關的元數據。
  • ApiBehaviorApplicationModelProvider:提供與API行爲相關的元數據
  • TempDataApplicationModelProvider:爲定義在Controller類型中標註了TempDataAttribute特性的屬性提供與臨時數據保存相關的元數據。
  • ViewDataAttributeApplicationModelProvider:爲定義在Controller類型中標註了ViewDataAttribute特性的屬性提供與視圖數據保存相關的元數據。

IApplicationModelProvider對象針對應用模型的構建是通過ApplicationModelFactory工廠驅動實施的,供這個工廠對象驅策的IApplicationModelProvider對象只需要預先註冊到依賴注入容器框架即可。爲MVC框架註冊基礎服務的AddMvcCore擴展方法具有針對DefaultApplicationModelProvider和ApiBehaviorApplicationModelProvider類型以及ApplicationModelFactory的服務註冊。IServiceCollection接口的AddControllers擴展方法會添加針對AuthorizationApplicationModelProvider和 CorsApplicationModelProvider類型的註冊。針對TempDataApplicationModelProvider ViewDataAttributeApplicationModelProvider類型的服務註冊是在IServiceCollection接口的AddControllersWithViews擴展方法中被註冊的。

四、IApplicationModelConvention

除了通過在依賴注入框架中註冊自定義的IApplicationModelProvider實現類型或者對象方式來定製最終生成的應用模型之外,相同的功能還可以通過註冊相應的IApplicationModelConvention對象來完成。顧名思義,IApplicationModelConvention對象旨在幫助我們爲應用模型設置一些基於約定的元數據。如下面的代碼片段所示,IApplicationModelConvention接口定義了唯一的Apply方法將實現在該方法的約定應用到指定的ApplicationModel對象上。

public interface IApplicationModelConvention
{
    void Apply(ApplicationModel application);
}
與IApplicationModelProvider對象或者實現類型的註冊不同,供ApplicationModelFactory工廠使用的IApplicationModelConvention對象需要註冊到作爲MVC應用配置選項的MvcOptions對象上。具體來說,我們需要將註冊的IApplicationModelConvention對象添加到MvcOptions如下所示的Conventions屬性上。
public class MvcOptions : IEnumerable<ICompatibilitySwitch>
{
    public IList<IApplicationModelConvention> Conventions { get; }
}

五、其他約定

除了利用自定義的IApplicationModelConvention實現類型對整個應用模型進行定製之外,我們還可以針組成應用模型的某種“節點類型”(Controller類型、Action方法、方法參數等)定義相應的約定,這些約定都具有相應的接口。應用模型分別利用ControllerModel、ActionModel和ParameterModel類型來描述Controller類型、Action方法以及方法參數。我們可以分別實現如下的接口定義相應特性,並將它們分別標註到Controller類型、Action方法或者方法參數上,ApplicationModelFactory對象會自動提取這些特性並將它們提供的約定應用到對應類型的模型節點上。

public interface IControllerModelConvention
{
    void Apply(ControllerModel controller);
}

public interface IActionModelConvention
{
    void Apply(ActionModel action);
}

public interface IParameterModelConvention
{
    void Apply(ParameterModel parameter);
}

描述Controller類型屬性的PropertyModel類型的最終目的是爲了能夠採用模型綁定的方式來完整針對對應屬性的綁定,這與針對Action方法參數的綁定是一致的,所以PropertyModel和描述Action方法參數的ParameterModel類型具有相同的基類ParameterModelBase。爲了定製Controller類型屬性和Action方法參數類型的應用模型節點,MVC框架爲我們定義瞭如下這個IParameterModelBaseConvention接口。

public interface IParameterModelBaseConvention
{
    void Apply(ParameterModelBase parameter);
}

我們可以和上面一樣將實現類型定義成標註到屬性和參數上特性,也可以讓實現類型同時也實現IApplicationModelConvention接口。值得一提的是,MVC框架並沒有提供一個針對PropertyModel類型的IPropertyModelConvention接口,針對Action方法參數的IParameterModelConvention接口和IParameterModelBaseConvention接口之間也不存在繼承關係。

六、ApplicationModelFactory

如下所示的是作爲應用模型創建工廠的ApplicationModelFactory類型的定義。如代碼片段所示,ApplicationModelFactory是一個內部類型。ApplicationModelFactory利用在構造函數中注入的參數得到所有註冊的IApplicationModelProvider和IApplicationModelConvention對象。

internal class ApplicationModelFactory
{
    private readonly IApplicationModelProvider[] 		_providers;
    private readonly IList<IApplicationModelConvention> 	_conventions;

    public ApplicationModelFactory(IEnumerable<IApplicationModelProvider> providers, IOptions<MvcOptions> options)
    {
        _providers 	= providers.OrderBy(it => it.Order).ToArray();
        _conventions 	= options.Value.Conventions;
    }

    public ApplicationModel CreateApplicationModel(IEnumerable<TypeInfo> controllerTypes)
    {
        var context = new ApplicationModelProviderContext(controllerTypes);
        for (var index = 0; index < _providers.Length; index++)
        {
            _providers[index].OnProvidersExecuting(context);
        }
        for (int index = _providers.Length - 1; index >= 0; index--)
        {
            _providers[index].OnProvidersExecuted(context);
        }
        ApplicationModelConventions.ApplyConventions(context.Result, _conventions);
        return context.Result;
    }
}

ApplicationModelFactory針對應用模型的構建體現在它的CreateApplicationModel方法上。如上面的代碼片段所示,ApplicationModelFactory對象先根據提供的Controller類型列表創建出一個ApplicationModelProviderContext上下文對象。接下來,ApplicationModelFactory將這個上下文作爲參數,按照Order屬性確定的順序調用每個IApplicationModelProvider對象的OnProvidersExecuting方法,然後再逆序調用它們的OnProvidersExecuted方法。ApplicationModelFactory最後會將通過所有IApplicationModelProvider對象參與構建的ApplicationModel從ApplicationModelProviderContext上下文中提取出來,並將各種方式註冊的約定應用在該對象上,具體的實現體現在如下這個ApplyConventions方法上。

internal static class ApplicationModelConventions
{
    public static void ApplyConventions(ApplicationModel applicationModel, IEnumerable<IApplicationModelConvention> conventions)
    {
        foreach (var convention in conventions)
        {
            convention.Apply(applicationModel);
        }

        var controllers = applicationModel.Controllers.ToArray();
        foreach (var controller in controllers)
        {
            var controllerConventions = controller.Attributes.OfType<IControllerModelConvention>().ToArray();

            foreach (var controllerConvention in controllerConventions)
            {
                controllerConvention.Apply(controller);
            }

            var actions = controller.Actions.ToArray();
            foreach (var action in actions)
            {
                var actionConventions = action.Attributes.OfType<IActionModelConvention>().ToArray();

                foreach (var actionConvention in actionConventions)
                {
                    actionConvention.Apply(action);
                }

                var parameters = action.Parameters.ToArray();
                foreach (var parameter in parameters)
                {
                    var parameterConventions = parameter.Attributes.OfType<IParameterModelConvention>().ToArray();

                    foreach (var parameterConvention in parameterConventions)
                    {
                        parameterConvention.Apply(parameter);
                    }

                    var parameterBaseConventions = GetConventions<IParameterModelBaseConvention>(conventions, parameter.Attributes);
                    foreach (var parameterConvention in parameterBaseConventions)
                    {
                        parameterConvention.Apply(parameter);
                    }
                }
            }

            var properties = controller.ControllerProperties.ToArray();
            foreach (var property in properties)
            {
                var parameterBaseConventions = GetConventions<IParameterModelBaseConvention>(conventions, property.Attributes);

                foreach (var parameterConvention in parameterBaseConventions)
                {
                    parameterConvention.Apply(property);
                }
            }
        }
    }

    private static IEnumerable<TConvention> GetConventions<TConvention>(IEnumerable<IApplicationModelConvention> conventions, IReadOnlyList<object> attributes)
    {
        return Enumerable.Concat(conventions.OfType<TConvention>(), attributes.OfType<TConvention>());
    }
}

如上面的代碼片段所示,註冊在MvcOptions配置選項上的IApplicationModelConvention對象提供的約定會直接應用到ApplicationModel對象上。除此之外,Controller類型、Action方法和方法參數上標註的相應約定特性會被提取出來,它們承載的約定規則會分別應用到對應的ControllerModel、ActionModel和ParameterModel對象上。

對於表示Action方法參數的ParameterModel對象和表示Controller類型屬性的ProperrtyModel對象來說,應用在對應參數和屬性上實現了IParameterModelBaseConvention接口的特性,以及同時實現了IParameterModelBaseConvention接口的IApplicationModelConvention對象,會被提取出來並將它們承載的約定應用到對應的參數或者屬性節點上。


ASP.NET Core MVC應用模型的構建[1]: 應用的藍圖
ASP.NET Core MVC應用模型的構建[2]: 應用模型
ASP.NET Core MVC應用模型的構建[3]: Controller模型
ASP.NET Core MVC應用模型的構建[4]: Action模型

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章