ASP.NET Core MVC应用模型的构建[2]: 定制应用模型

2024-02-28 08:17:47 浏览数 (1)

在对应用模型的基本构建方式具有大致的了解之后,我们来系统地认识一下描述应用模型的ApplicationModel类型。对于一个描述MVC应用模型的ApplicationModel对象来说,它承载的元数据绝大部分是由默认注册的DefaultApplicationModelProvider对象提供的,在接下来针对ApplicationModel及其相关类型(ControllerModel、ActionModel和ParameterModel等)的介绍中,我们还会着重介绍DefaultApplicationModelProvider对象采用怎样的方式提取并设置这些元数据。

一、几个重要的接口

在正式介绍ApplicationModel及其相关类型的定义之前,我们先来认识如下几个重要的接口,针对不同模型节点的类型分别实现了这些接口的一个或者多个。认识这些接口有助于我们更好地理解应用模型的层次结构以及每种模型节点的用途。

IPropertyModel

为了让应用模型的构建方式具有更好的扩展性,ApplicationModel类型以及描述其他描述模型节点的类型(ControllerModel、ActionModel和ParameterModel等)都提供了一个字典类型的Properties属性,自定义的IApplicationModelProvider实现类型以及各种形式的约定类型都可以将任意属性存储到这个字典中。这个Properties属性是对IPropertyModel接口的实现。

代码语言:javascript复制
public interface IPropertyModel
{
    IDictionary<object, object> Properties { get; }
}

ICommonModel

描述MVC应用模型的ApplicationModel对象由描述所有Controller类型的ControllerModel对象组成,而ControllerModel对象则通过描述其Action方法和属性的ActionModel和PropertyModel对下组成。这三种分别描述类型、方法和属性的模型节点本质上都是对一个MemberInfo对象的封装,描述对应节点的元数据主要由标注在它们上面的特性来提供,所以标注的特性成了这些模型节点重要的元素。除此之外,这些模型节点还应该具有一个唯一的命名。综上这些元素被统一定义在如下这个ICommonModel接口中,该接口派生于IPropertyModel接口。

代码语言:javascript复制
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接口。

代码语言:javascript复制
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对象于此对应。

代码语言:javascript复制
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对象上。

代码语言:javascript复制
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对象提供绑定元数据。

代码语言:javascript复制
public interface IBindingModel
{
    BindingInfo BindingInfo { get; set; }
}

二、ApplicationModel

如下所示的是描述应用模型的ApplicationModel类型的定义,它的核心是Controllers属性返回的一组ControllerModel对象。该类型实现了IPropertyModel、IFilterModel和IApiExplorerModel接口,DefaultApplicationModelProvider对象只会提取在应用级别全局注册的过滤器,并生成相应的IFilterMetadata对象添加到Filters属性中。

代码语言:javascript复制
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对象。

代码语言:javascript复制
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方法上。

代码语言:javascript复制
[AttributeUsage(AttributeTargets.Class| AttributeTargets.Method, AllowMultiple = true)]
public class FoobarAttribute : ActionFilterAttribute
{
}

在如下所示的应用承载程序中,我们调用IWebHostBuilder接口的ConfigureServices方法添加了针对ApplicationModelProducer类型的服务注册。在调用AddControllersWithViews扩展方法的过程中,我们创建了一个FoobarAttribute对象并将它添加到MvcOptions对象的Filters属性中,意味着我们在应用范围内全局注册了这个FoobarAttribute过滤器。

代码语言:javascript复制
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中。

代码语言:javascript复制
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相关的两个对象以表格的形式呈现出来。

代码语言:javascript复制
@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_image002clip_image002

图1 应用模型的默认构建规则

三、自定义IApplicationModelProvider

由于MVC框架针对目标Action的处理行为完全由描述该Action的ActionDescriptor对象决定,而最初的元数据则来源于应用模型,所以有时候一些针对请求流程的控制需要间接地利用针对应用模型的定制来实现。通过前面的内容,我们知道应用模型的定制可以通过注册自定义的IApplicationModelProvider实现类型,接下来我们就来做相应的演示。

通过上面演示的势力可以看出,默认情况下构建出来的ApplicationModel对象的ApiExplorer属性并没有作具体的设置,接下来我们将此设置实现在一个IApplicationModelProvider实现类型中。具体来说,我们希望在MVC应用所在项目的程序集上标注如下这个ApiExplorerAttribute特性来设置与ApiExplorer相关的两个属性。我们将针对该特性的标注按照如下的方式定义在Program.cs中,该特性将GroupName设置为 “Foobar” 。

代码语言:javascript复制
[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对象上。

代码语言:javascript复制
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方法将该类型作为服务注册到依赖注入框架中即可。

代码语言:javascript复制
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_image004clip_image004

图2 注册自定义IApplicationModelProvider实现类型定制应用模型

四、自定义IApplicationModelConvention

除了利用自定义的IApplicationModelProvider实现类型对应用模型进行定制之外,我们还可以注册各种类型的约定达到相同的目的。上面演示的针对ApiExplorer相关设置的定制完全可以利用如下这个ApiExplorerConvention类型来完成。如代码片段所示,ApiExplorerConvention类型实现了IApplicationModelConvention接口,我们直接在构造函数中指定ApiExplorer相关的两个属性,并在实现的Apply方法中将其应用到表示应用模型的ApplicationModel对象上。

代码语言:javascript复制
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所示的输出结果。

代码语言:javascript复制
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模型

0 人点赞