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对象并没有做相应的设置。
图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属性这次得到了相应的设置。
图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模型
ASP.NET Core MVC应用模型的构建[2]: 定制应用模型的更多相关文章
- ASP.NET Core 中文文档 第四章 MVC(01)ASP.NET Core MVC 概览
原文:Overview of ASP.NET Core MVC 作者:Steve Smith 翻译:张海龙(jiechen) 校对:高嵩 ASP.NET Core MVC 是使用模型-视图-控制器(M ...
- ASP.NET Core MVC 概述
https://docs.microsoft.com/zh-cn/aspnet/core/mvc/overview?view=aspnetcore-2.2 ASP.NET Core MVC 概述 20 ...
- ASP.NET Core MVC/WebAPi 模型绑定探索
前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...
- ASP.NET Core MVC/WebAPi如何构建路由?
前言 本节我们来讲讲ASP.NET Core中的路由,在讲路由之前我们首先回顾下之前所讲在ASP.NET Core中的模型绑定这其中有一个问题是我在项目当中遇见的,我们下面首先来看看这个问题. 回顾A ...
- ASP.NET Core MVC 模型绑定用法及原理
前言 查询了一下关于 MVC 中的模型绑定,大部分都是关于如何使用的,以及模型绑定过程中的一些用法和概念,很少有关于模型绑定的内部机制实现的文章,本文就来讲解一下在 ASP.NET Core MVC ...
- 008.Adding a model to an ASP.NET Core MVC app --【在 asp.net core mvc 中添加一个model (模型)】
Adding a model to an ASP.NET Core MVC app在 asp.net core mvc 中添加一个model (模型)2017-3-30 8 分钟阅读时长 本文内容1. ...
- ASP.NET Core MVC I/O编程模型
1. ASP.NET Core MVC I/O编程模型 1.1. I/O编程模型浅析 1.2. 同步阻塞I/O 1.3. 同步非阻塞I/O 1.4. 异步I/O 1.5. 总结 1.1. I/O编程模 ...
- ASP.NET Core MVC中构建Web API
在ASP.NET CORE MVC中,Web API是其中一个功能子集,可以直接使用MVC的特性及路由等功能. 在成功构建 ASP.NET CORE MVC项目之后,选中解决方案,先填加一个API的文 ...
- 为什么 web 开发人员需要迁移到. NET Core, 并使用 ASP.NET Core MVC 构建 web 和 webservice/API
2018 .NET开发者调查报告: .NET Core 是怎么样的状态,这里我们看到了还有非常多的.net开发人员还在观望,本文给大家一个建议.这仅代表我的个人意见, 我有充分的理由推荐.net 程序 ...
- ASP.NET Core 入门教程 2、使用ASP.NET Core MVC框架构建Web应用
一.前言 1.本文主要内容 使用dotnet cli创建基于解决方案(sln+csproj)的项目 使用Visual Studio Code开发基于解决方案(sln+csproj)的项目 Visual ...
随机推荐
- [转帖]APIServer dry-run and kubectl diff
https://kubernetes.io/blog/2019/01/14/apiserver-dry-run-and-kubectl-diff/ Monday, January 14, 2019 A ...
- [转帖]Oracle 通过 Exadata 云基础设施 X9M 提供卓越的数据库性能和规模
https://www.modb.pro/db/397202 32个节点的RAC 服务器 每个服务器 两个 64核心的AMD CPU 四个线程干管理 252个线程进行数据库处理 252*32=8064 ...
- 记录TritonServer部署多模型到多GPU踩坑 | 京东云技术团队
一.问题是怎么发现的 部署chatglm2和llama2到一个4*V100的GPU机器上遇到问题 config.pbtxt 中设置模型分别在指定gpu上部署实例配置不生效 如以下配置为在gpu0上部署 ...
- sass中使用穿透属性(deep)修改第三方组件样似
<el-form-item> <el-button class="save-btn" type="primary" @click=" ...
- WPF内嵌Http协议的Server端
需求:有时后比如WPF,WinForm,Windows服务这些程序可能需要对外提供接口用于第三方服务主动通信,调用推送一些服务或者数据. 想到的部分实现方式: 一.使用Socket/WebSocket ...
- DES加密和base64加密
DES简介:参考知乎 https://www.zhihu.com/question/36767829 和博客https://www.cnblogs.com/idreamo/p/9333753.html ...
- 从零开始配置 vim(15)——状态栏配置
vim 下侧有一个状态栏,会显示当前打开的文件等一系列内容,只是我们很少去关注它.而且原生的vim也支持对状态栏进行自定义.这篇文章主要介绍如何自定义状态栏 设置状态栏 我们可以采用 set stat ...
- 深度学习应用篇-自然语言处理[10]:N-Gram、SimCSE介绍,更多技术:数据增强、智能标注、多分类算法、文本信息抽取、多模态信息抽取、模型压缩算法等
深度学习应用篇-自然语言处理[10]:N-Gram.SimCSE介绍,更多技术:数据增强.智能标注.多分类算法.文本信息抽取.多模态信息抽取.模型压缩算法等 1.N-Gram N-Gram是一种基于统 ...
- CE修改器入门:代码替换功能
某些游戏重新开始时,数据会存储在与上次不同的地方, 甚至游戏的过程中数据的存储位置也会变动.在这种情况下,你还是可以简单几步搞定它.这次我将尽量阐述如何运用"代码替换"功能,第五关 ...
- SpringCloud-04-http客户端Feign
http客户端Feign 1.Feign的介绍 Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign 其作用就是帮助我们优雅的实现ht ...