ASP.NET Core MVC應用模型的構建[2]: 定製應用模型

在對應用模型的基本構建方式具有大致的瞭解之後,我們來系統地認識一下描述應用模型的ApplicationModel類型。對於一個描述MVC應用模型的ApplicationModel對象來說,它承載的元數據絕大部分是由默認註冊的DefaultApplicationModelProvider對象提供的,在接下來針對ApplicationModel及其相關類型(ControllerModel、ActionModel和ParameterModel等)的介紹中,我們還會着重介紹DefaultApplicationModelProvider對象採用怎樣的方式提取並設置這些元數據。

一、幾個重要的接口
二、ApplicationModel
三、自定義IApplicationModelProvider
四、自定義IApplicationModelConvention

一、幾個重要的接口

在正式介紹ApplicationModel及其相關類型的定義之前,我們先來認識如下幾個重要的接口,針對不同模型節點的類型分別實現了這些接口的一個或者多個。認識這些接口有助於我們更好地理解應用模型的層次結構以及每種模型節點的用途。

IPropertyModel

爲了讓應用模型的構建方式具有更好的擴展性,ApplicationModel類型以及描述其他描述模型節點的類型(ControllerModel、ActionModel和ParameterModel等)都提供了一個字典類型的Properties屬性,自定義的IApplicationModelProvider實現類型以及各種形式的約定類型都可以將任意屬性存儲到這個字典中。這個Properties屬性是對IPropertyModel接口的實現。

public interface IPropertyModel
{
    IDictionary<object, object> Properties { get; }
}

ICommonModel

描述MVC應用模型的ApplicationModel對象由描述所有Controller類型的ControllerModel對象組成,而ControllerModel對象則通過描述其Action方法和屬性的ActionModel和PropertyModel對下組成。這三種分別描述類型、方法和屬性的模型節點本質上都是對一個MemberInfo對象的封裝,描述對應節點的元數據主要由標註在它們上面的特性來提供,所以標註的特性成了這些模型節點重要的元素。除此之外,這些模型節點還應該具有一個唯一的命名。綜上這些元素被統一定義在如下這個ICommonModel接口中,該接口派生於IPropertyModel接口。

public interface ICommonModel : IPropertyModel
{
    MemberInfo 		        MemberInfo { get; }
    string 			Name { get; }
    IReadOnlyList<object> 	Attributes { get; }
}

IFilterModel

針對MVC應用的請求總是被路由到某個匹配的Action,針對請求的處理體現在對目標Action的執行。這裏所謂的“執行Action”不僅僅包括針對目標方法的執行,還需要執行應用在該Action上的一系列過濾器。過濾器使我們可以很容易地“干預”針對目標Action的執行流程,它們可以直接註冊到Action方法上,也可以註冊到Controller類型,甚至可以在應用範圍進行全局註冊,所以MVC框架爲這些包含過濾器註冊的模型節點(ApplicationModel、ControllerModel和ActionModel)定義瞭如下這個IFilterModel接口。

public interface IFilterModel
{
    IList<IFilterMetadata> 	Filters { get; }
}

如上面的代碼片段所示,IFilterModel接口定義了唯一的Filters屬性返回一個IFilterMetadata對象的列表,IFilterMetadata接口是對過濾器元數據的描述。

IApiExplorerModel

當我們在面向Controller的MVC編程模型上開發API的時候,我們希望應用能夠提供在API層面的元數據。這些面向開發人員的元數據告訴我們當前應用提供了怎樣的API終結點,每個終結點的路徑是什麼、支持何種HTTP方法、需要怎樣的輸入、輸入和響應具有怎樣的結構等。MVC框架專門提供了一個名爲“ApiExplorer”的模塊來完成針對API元數據的導出任務。我們可以利用API元數據自動生成在線開發文檔(比如著名的Swagger就是這麼幹的),也可以針對不同的語言生成調用API的客戶端代碼。

如果說ActionDescriptor對象是Action面向運行時的描述,那麼Action面向API的描述就體現爲一個ApiDescription對象。我們可以在Controller類型或者具體的Action方法上標註實現IApiDescriptionGroupNameProvider接口的特性對ApiDescription對象進行分組(設置GroupName屬性),也可以標註實現了IApiDescriptionVisibilityProvider接口的特性控制對應API的可見性(如果IgnoreApi屬性設置爲True,ApiExplorer將不會生成對應的ApiDescription對象)。如下所示的ApiExplorerSettingsAttribute特性是對這兩個接口的實現。IApiExplorerModel接口定義的ApiExplorer屬性返回的ApiExplorerModel對象於此對應。

public interface IApiDescriptionGroupNameProvider
{
    string GroupName { get; }
}
public interface IApiDescriptionVisibilityProvider
{
    bool IgnoreApi { get; }
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited=true)]
public class ApiExplorerSettingsAttribute : Attribute, IApiDescriptionGroupNameProvider, IApiDescriptionVisibilityProvider
{
    public string 	GroupName { get; set; }
    public bool 	IgnoreApi { get; set; }
}

針對API分組和可見性的設置體現在面向應用(ApplicationModel)、Controller類型(ControllerModel)和Action方法(ActionModel)的模型節點上,所以它們都會實現如下這個IApiExplorerModel,兩個設置體現在ApiExplorer返回的ApiExplorerModel對象上。

public interface IApiExplorerModel
{
    ApiExplorerModel 	ApiExplorer { get; set; }
}
public class ApiExplorerModel
{
    public bool? 	IsVisible { get; set; }
    public string 	GroupName { get; set; }
}

IBindingModel

MVC框架採用“模型綁定”的機制來綁定目標Action方法的參數列表和定義在Controller類型中相應的屬性,所以描述參數的ParameterModel對象和描述Controller屬性的PropertyModel對象需要提供服務於模型綁定的元數據。MVC爲這兩種模型節點定義瞭如下這個IBindingModel接口,它利用BindingInfo屬性返回的BindingInfo對象提供綁定元數據。

public interface IBindingModel
{
    BindingInfo BindingInfo { get; set; }
}

二、ApplicationModel

如下所示的是描述應用模型的ApplicationModel類型的定義,它的核心是Controllers屬性返回的一組ControllerModel對象。該類型實現了IPropertyModel、IFilterModel和IApiExplorerModel接口,DefaultApplicationModelProvider對象只會提取在應用級別全局註冊的過濾器,並生成相應的IFilterMetadata對象添加到Filters屬性中。

public class ApplicationModel : IPropertyModel, IFilterModel, IApiExplorerModel
{
    public IList<ControllerModel> 		Controllers { get; }
    public IList<IFilterMetadata> 		Filters { get; }
    public ApiExplorerModel 			ApiExplorer { get; set; }
    public IDictionary<object, object> 	Properties { get; }
}

在瞭解了DefaultApplicationModelProvider對象針對應用模型的大致構建規則之後,我們利用一個簡單的實例演示來對此做一個驗證。由於構建應用模型的ApplicationModelFactory是一個內部類型,所以我們在作爲演示程序的MVC應用中定義瞭如下這個ApplicationModelProducer類型。如代碼片段所示,它會利用注入的IServiceProvider對象來提供ApplicationModelFactory對象。在定義的Create方法中,ApplicationModelProducer根據反射的方式調用ApplicationModelFactory的CreateApplicationModel方法根據指定的Controller類型創建出描述應用模型的ApplicationModel對象。

public class ApplicationModelProducer
{
    private readonly Func<Type[], ApplicationModel> _factory;
    public ApplicationModelProducer(IServiceProvider serviceProvider)
    {
        var assemblyName = new AssemblyName("Microsoft.AspNetCore.Mvc.Core");
        var assemly = Assembly.Load(assemblyName);
        var typeName ="Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModelFactory";
        var factoryType = assemly.GetTypes().Single(it => it.FullName ==typeName);
        var factory = serviceProvider.GetService(factoryType);
        var method = factoryType.GetMethod("CreateApplicationModel");
        _factory = controlerTypes =>
        {
            var typeInfos = controlerTypes.Select(it => it.GetTypeInfo());
            return (ApplicationModel)method.Invoke(factory, new object[] { typeInfos });
        };
    }
    public ApplicationModel Create(params Type[] controllerTypes) => _factory(controllerTypes);
}

爲了驗證針對全局過濾器的註冊,我們定義瞭如下這個FoobarAttribute特性。如代碼片段所示,FoobarAttribute派生於ActionFilterAttribute特性。從標註的AttributeUsage特性來看,多個FoobarAttribute特性可以同時標註到Controller類型或者Action方法上。

[AttributeUsage(AttributeTargets.Class| AttributeTargets.Method, AllowMultiple = true)]
public class FoobarAttribute : ActionFilterAttribute
{
}

在如下所示的應用承載程序中,我們調用IWebHostBuilder接口的ConfigureServices方法添加了針對ApplicationModelProducer類型的服務註冊。在調用AddControllersWithViews擴展方法的過程中,我們創建了一個FoobarAttribute對象並將它添加到MvcOptions對象的Filters屬性中,意味着我們在應用範圍內全局註冊了這個FoobarAttribute過濾器。

class Program
{
    static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                .ConfigureServices(services => services
                    .AddSingleton<ApplicationModelProducer>()
                    .AddRouting()
                    .AddControllersWithViews(options=>options.Filters.Add(new FoobarAttribute())))
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(endpoints => endpoints.MapControllers())))
            .Build()
            .Run();
    }
}

我們在定義瞭如下三個用於測試的Controller類型(FooController、BarController和BazController)。我們將用於呈現主頁的Action方法定義在HomeController類型中。簡單起見,我們直接將ApplicationModelProducer對象注入到Index方法中,並通過標註的FromServicesAttribute特性指示利用註冊的服務來綁定該參數。Index方法利用這個ApplicationModelProducer對象構建出根據三個測試Controller類型創建的ApplicationModel對象,並將其作爲Model呈現在默認View中。

public class FooController
{
    public void Index() => throw new NotImplementedException();
}
public class BarController
{
    public void Index() => throw new NotImplementedException();
}
public class BazController
{
    public void Index() => throw new NotImplementedException();
}

public class HomeController: Controller
{
    [HttpGet("/")]
    public IActionResult Index([FromServices]ApplicationModelProducer producer)
    {
        var applicationModel = producer.Create(typeof(FooController), typeof(BarController), typeof(BazController));
        return View(applicationModel);
    }
}

如下所示的就是Action方法Index對應View的定義。如代碼片段所示,這是一個Model類型爲ApplicationModel的強類型View。在這個View中,我們將構成ApplicationModel對象的所有ControllerModel的名稱、過濾器的類型以及ApiExplorer相關的兩個對象以表格的形式呈現出來。

@model Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModel
@{
    var controllers = Model.Controllers;
    var filters = Model.Filters;
}
<html>
<head>
    <title>Application</title>
</head>
<body>
    <table border="1" cellpadding="0" cellspacing="0">
        <tr>
            <td rowspan="@controllers.Count">Controllers</td>
            <td>@controllers[0].ControllerName</td>
        </tr>
        @for (int index = 1; index < controllers.Count; index++)
        {
            <tr><td>@controllers[index].ControllerName</td></tr>
        }
        <tr>
            <td rowspan="@filters.Count">Filters</td>
            <td>@filters[0].GetType().Name</td>
        </tr>
        @for (int index = 1; index < filters.Count; index++)
        {
            <tr><td>@filters[index].GetType().Name</td></tr>
        }
        <tr>
            <td rowspan="2">ApiExplorer</td>
            <td>IsVisible = @Model.ApiExplorer.IsVisible </td>
        </tr>
        <tr><td>GroupName = @Model.ApiExplorer.GroupName </td></tr>

    </table>
</body>
</html>

演示程序啓動之後,如果利用瀏覽器訪問其根路徑,我們會得到如圖1所示的輸出結果。我們可以從輸出結果中看到組成ApplicationModel對象的三個Controller的名稱。ApplicationModel對象的Filters屬性列表中包含三個全局過濾器,除了我們顯式註冊的FoobarAttribute特性之外,還具有一個在不支持提供媒體類型情況下對請求進行處理的UnsupportedContentTypeFilter過濾器,它是在AddMvcCore擴展方法中註冊的。另一個用來保存臨時數據的SaveTempDataAttribute特性則是通過AddControllersWithViews擴展方法註冊的。默認下,ApplicationModel對象的ApiExplorer屬性返回的ApiExplorerModel對象並沒有做相應的設置。

clip_image002

圖1 應用模型的默認構建規則

三、自定義IApplicationModelProvider

由於MVC框架針對目標Action的處理行爲完全由描述該Action的ActionDescriptor對象決定,而最初的元數據則來源於應用模型,所以有時候一些針對請求流程的控制需要間接地利用針對應用模型的定製來實現。通過前面的內容,我們知道應用模型的定製可以通過註冊自定義的IApplicationModelProvider實現類型,接下來我們就來做相應的演示。

通過上面演示的勢力可以看出,默認情況下構建出來的ApplicationModel對象的ApiExplorer屬性並沒有作具體的設置,接下來我們將此設置實現在一個IApplicationModelProvider實現類型中。具體來說,我們希望在MVC應用所在項目的程序集上標註如下這個ApiExplorerAttribute特性來設置與ApiExplorer相關的兩個屬性。我們將針對該特性的標註按照如下的方式定義在Program.cs中,該特性將GroupName設置爲 “Foobar” 。

[AttributeUsage(AttributeTargets.Assembly)]
public class ApiExplorerAttribute:Attribute
{
    public bool 	IsVisible => true;
    public string 	GroupName { get; set; }
}

[assembly: ApiExplorer(GroupName = "Foobar")]

針對ApiExplorerAttribute特性的解析以及基於該特性設置對應用模型的定製實現在如下這個ApiExplorerApplicationModelProvider類型中。如代碼片段所示,該類型的構造函數中注入了代表承載環境的IHostEnvironment對象,我們利用它得到當前應用的名稱,並將它作爲程序集名稱得到標註的ApiExplorerAttribute特性,進而得到基於ApiExplorer的設置。在實現的OnProvidersExecuting方法中,我們將相關設置應用到ApplicationModel對象上。

public class ApiExplorerApplicationModelProvider : IApplicationModelProvider
{
    private readonly bool? 	_isVisible;
    private readonly string 	_groupName;

    public int Order => -1000;

    public ApiExplorerApplicationModelProvider(IHostEnvironment hostEnvironment)
    {
        var assembly = Assembly.Load(new AssemblyName(hostEnvironment.ApplicationName));
        var attribute = assembly.GetCustomAttribute<ApiExplorerAttribute>();
        _isVisible = attribute?.IsVisible;
        _groupName = attribute?.GroupName;
    }
    public void OnProvidersExecuted(ApplicationModelProviderContext context) { }

    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    {
        context.Result.ApiExplorer.GroupName??= _groupName;
        context.Result.ApiExplorer.IsVisible ??= _isVisible;
    }
}

爲了上面這個自定義的ApiExplorerApplicationModelProvider類型,我們對應用承載程序做了如下的改動。如代碼片段所示,我們只需要調用IWebHostBuilder的ConfigureServices方法將該類型作爲服務註冊到依賴注入框架中即可。

class Program
{
    static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                .ConfigureServices(services => services
                    .AddSingleton<IApplicationModelProvider, ApiExplorerApplicationModelProvider>()
                    .AddSingleton<ApplicationModelProducer>()
                    .AddRouting()
                    .AddControllersWithViews(options=>options.Filters.Add(new FoobarAttribute())))
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(endpoints => endpoints.MapControllers())))
            .Build()
            .Run();
    }
}

改動後的演示程序啓動後,我們利用瀏覽器訪問應用的主頁,可以得到如圖2所示的輸出結果。從瀏覽器上的輸出結果可以看出,對於ApplicationModelFactory最終構建的ApplicationModel對象來說,它的ApiExplorer屬性這次得到了相應的設置。

clip_image004

圖2 註冊自定義IApplicationModelProvider實現類型定製應用模型

四、自定義IApplicationModelConvention

除了利用自定義的IApplicationModelProvider實現類型對應用模型進行定製之外,我們還可以註冊各種類型的約定達到相同的目的。上面演示的針對ApiExplorer相關設置的定製完全可以利用如下這個ApiExplorerConvention類型來完成。如代碼片段所示,ApiExplorerConvention類型實現了IApplicationModelConvention接口,我們直接在構造函數中指定ApiExplorer相關的兩個屬性,並在實現的Apply方法中將其應用到表示應用模型的ApplicationModel對象上。

public class ApiExplorerConvention : IApplicationModelConvention
{
    private readonly bool? 	_isVisible;
    private readonly string 	_groupName;

    public ApiExplorerConvention(bool? isVisible, string groupName)
    {
        _isVisible = isVisible;
        _groupName = groupName;
    }

    public void Apply(ApplicationModel application)
    {
        application.ApiExplorer.IsVisible ??= _isVisible;
        application.ApiExplorer.GroupName ??= _groupName;
    }
}

用於定製應用模型的各種約定需要註冊到代表MVC應用配置選項的MvcOptions對象上,所以我們需要對應用承載程序作相應的修改。如下面你代碼片段所示,在調用IServiceCollection接口的AddControllersWithViews擴展方法是,我們創建了一個ApiExplorerConvention對象,並將其添加到作爲配置選項的MvcOptions對象的Conventions屬性上。改動後的演示程序啓動後,我們利用瀏覽器訪問應用的主頁依然可以得到如圖2所示的輸出結果。

class Program
{
    static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                .ConfigureServices(services => services
                    .AddSingleton<ApplicationModelProducer>()
                    .AddRouting()
                    .AddControllersWithViews(options=>
                    {
                        options.Filters.Add(new GlobalFilter());
                        options.Conventions.Add(new ApiExplorerConvention(true, "Foobar"));
                    }))
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(endpoints => endpoints.MapControllers())))
            .Build()
            .Run();
    }
}

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

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