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对象上。

图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描述其参数。

图2 应用模型总体结构
三、IApplicationModelProvider
在软件设计中我们经常会遇到这样的场景:我们需要构建一个由若干不同元素组成的复合对象,不同的组成元素具有不同的构建方式,MVC框架几乎基于采用了同一种模式来处理这样的场景。举个简单的例子:对象Foo需要实现的功能需要委托一组Bar对象来实现。MVC框架针对这种需求大都采用如图3所示模式来实现:Foo先创建一个上下文,并提供必要的输入,然后驱动每个Bar对象在这个上下文中完成各自的处理任务。所有Bar对象针对数据和状态的修改,以及产生的输出均体现在这个共享的上下文中,所有对象最终通过这个上下文就可以得到应有的状态或者所需的输出。

图3 基于共享上下文的多对象协作模式(单操作)
有时候我们甚至可以将Bar对象的操作分成两个步骤进行,比如我们将针对这两个步骤的操作分别命名为Executing和Executed。如图4所示,在创建共享上下文之后,Foo对象先按序执行每一个Bar对象的Executing操作,最后再反向执行每个Bar对象的Executed操作,所有的操作均在同一个上下文中执行。

图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/WebAPi 模型绑定探索
		
前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...
 - ASP.NET Core MVC 之模型(Model)
		
1.模型绑定 ASP.NET Core MVC 中的模型绑定将数据从HTTP请求映射到操作方法参数.参数既可以是简单类型,也可以是复杂类型.MVC 通过抽象绑定解决了这个问题. 2.使用模型绑定 当 ...
 - ASP.NET Core MVC/WebAPi 模型绑定探索 转载https://www.cnblogs.com/CreateMyself/p/6246977.html
		
前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...
 - 【转】ASP.NET Core MVC/WebAPi 模型绑定探索
		
前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...
 - ASP.NET Core MVC/WebAPi 模型绑定
		
public class Person { public string Name { get; set; } public string Address { get; set; } public in ...
 - ASP.NET Core MVC 模型绑定用法及原理
		
前言 查询了一下关于 MVC 中的模型绑定,大部分都是关于如何使用的,以及模型绑定过程中的一些用法和概念,很少有关于模型绑定的内部机制实现的文章,本文就来讲解一下在 ASP.NET Core MVC ...
 - ASP.NET Core MVC中URL和数据模型的匹配
		
Http GET方法 首先我们来看看GET方法的Http请求,URL参数和ASP.NET Core MVC中Controller的Action方法参数匹配情况. 我定义一个UserController ...
 - ASP.NET Core MVC如何上传文件及处理大文件上传
		
用文件模型绑定接口:IFormFile (小文件上传) 当你使用IFormFile接口来上传文件的时候,一定要注意,IFormFile会将一个Http请求中的所有文件都读取到服务器内存后,才会触发AS ...
 - ASP.NET Core MVC/WebAPi如何构建路由?
		
前言 本节我们来讲讲ASP.NET Core中的路由,在讲路由之前我们首先回顾下之前所讲在ASP.NET Core中的模型绑定这其中有一个问题是我在项目当中遇见的,我们下面首先来看看这个问题. 回顾A ...
 - 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. ...
 
随机推荐
- [转帖]TiUP 常见运维操作
			
https://docs.pingcap.com/zh/tidb/stable/maintain-tidb-using-tiup 本文介绍了使用 TiUP 运维 TiDB 集群的常见操作,包括查看集群 ...
 - [转帖]Linux中的Page cache和Buffer cache详解
			
1.内存情况 在讲解Linux内存管理时已经提到,当你在Linux下频繁存取文件后,即使系统上没有运行许多程序,也会占用大量的物理内存.这是因为当你读写文件的时候,Linux内核为了提高读写的性能和速 ...
 - 如何减缓vm中慢插入的次数
			
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 偶然发现vm-storage的监控里有这样一个指标:vm_ ...
 - Golang zip压缩文件读写操作
			
创建zip文件 golang提供了archive/zip包来处理zip压缩文件,下面通过一个简单的示例来展示golang如何创建zip压缩文件: func createZip(filename str ...
 - 多模态对比语言图像预训练CLIP:打破语言与视觉的界限
			
多模态对比语言图像预训练CLIP:打破语言与视觉的界限 一种基于多模态(图像.文本)对比训练的神经网络.它可以在给定图像的情况下,使用自然语言来预测最相关的文本片段,而无需为特定任务进行优化.CLIP ...
 - 基于 hugging face 预训练模型的实体识别智能标注方案:生成doccano要求json格式
			
强烈推荐:数据标注平台doccano----简介.安装.使用.踩坑记录_汀.的博客-CSDN博客_doccano huggingface官网 参考:数据标注平台doccano----简介.安装.使用. ...
 - C# 中的函数与方法
			
在C#中,函数和方法都是一段可重用的代码块,用于实现特定的功能.函数是C#中的基本代码块之一,用于完成特定的任务和返回一个值.函数可以具有零个或多个参数,并且可以使用关键字来指定函数的访问级别和返回类 ...
 - 从嘉手札<2023-11-20>
			
写给十年如一日的偶像--Faker "我看了一下,觉得视频还不够清晰,等我换一个清晰点的摄像头再回来直播,不要走开~" 繁星满天,流光飞逝. 世界是一场盛大的表演, 舞台上熙熙攘攘 ...
 - django向数据库更新时间
			
1 今天的日期可以用下面的代码: 2 3 import datetime 4 5 today = datetime.date.today() 6 7 8 9 得到昨天的日期可以用: 10 11 yes ...
 - WebAssembly核心编程[1]:wasm模块实例化的N种方式
			
当我们在一个Web应用中使用WebAssembly,最终的目的要么是执行wasm模块的入口程序(通过start指令指定的函数),要么是调用其导出的函数,这一切的前提需要创建一个通过WebAssembl ...