ASP.NET MVC Controller的激活
最近抽空看了一下asp.net mvc的部分,顺带写篇文章做个笔记以便日后查看。
在urlroutingmodule模块中,将请求处理程序映射到了mhandler中,因此,说起controller的激活,首先要从mvchandler入手,mvchandler实现了三个接口:ihttpasynchandler, ihttphandler, irequiressessionstate。 其处理逻辑主要实现在同步和异步的processrequest方法中,总的来说,该方法在执行的时候,大致经历以下几个步骤:
预处理(在响应头中添加版本信息并去除未赋值的可选路由参数)通过controllerbuilder获取controlerfactory,并使用controller工厂创建controller根据是否是异步处理,调用controller中相应的方法(executecore或beginexecute)释放controller
其中第一步在processrequestinit方法中进行处理,本文主要是分析第两步中的controller是如何创建出来的。
controller的创建是通过controllerfactory实现的,而controllerfactory的创建又是在controllerbuilder中完成的,因此我们先了解一下controllerbuilder的工作原理。
controllerbuilder
从源码中可以看出,在controllerbuilder类中,并没有直接实现对controller工厂的创建,controllerfactory的创建实际上是委托给一个继承自iresolver接口的singleserviceresolver类的实例来实现的,这一点从getcontrollerfactory方法中可以看出,它是通过调用singleserviceresolver对象的current属性来完成controller工厂的创建的。
public icontrollerfactory getcontrollerfactory() { return _serviceresolver.current; //依赖iresolver接口创建工厂 }
并且在源码中还发现,singleserviceresolver类是internal级别的,这意味着外部无法直接访问,那么controllerbuilder是如何借助singleserviceresolver来实现工厂的注册呢?继续看代码,controllerbuilder类和singleserviceresolver类都有一个func
类型的委托字段,我们姑且称为工厂委托,
//controllerbuilder.cs private func _factorythunk = () => null; //工厂委托 //singleserviceresolver.cs private func _currentvaluethunk; //工厂委托
该委托实现了工厂的创建,而通过setcontrollerfactory方法仅仅是更改了controllerbuilder类的工厂委托字段,并没有更改singleserviceresolver类的工厂委托字段,
public void setcontrollerfactory(icontrollerfactory controllerfactory) { if (controllerfactory == null) { throw new argumentnullexception("controllerfactory"); } _factorythunk = () => controllerfactory; //更改controllerbuilder的工厂委托字段 }
因此必须将相应的更改应用到singleserviceresolver类中才能实现真正的注册,我们知道,如果是单纯的引用赋值,那么更改一个引用并不会对另外一个引用造成改变,比如:
func f1 = ()=>null; func f2 = f1; //f1与f2指向同一个对象 object o = new object(); f1 = ()=>o; //更改f1后,f2仍然指向之前的对象 bool b1 = f1() == o; //true bool b2 = f2() == null; //true, f1()!=f2()
所以,controllerbuilder在实例化singleserviceresolver对象的时候,并没有将自身的工厂委托字段直接赋值给singleserviceresolver对象的对应字段(因为这样的话setcontrollerfactory方法注册的委托无法应用到singleserviceresolver对象中),而是通过委托来进行了包装,这样就会形成一个闭包,在闭包中进行引用,如下所示:
func f1 = ()=>null; func f2 = ()=>f1(); //通过委托包装f1,形成闭包 object o = new object(); f1 = ()=>o; //更改f1后,f2与f1保持同步 bool b1 = f1() == o; //true bool b2 = f2() == o; //true, f1()==f2() //controllerbuilder.cs internal controllerbuilder(iresolver serviceresolver) { _serviceresolver = serviceresolver ?? new singleserviceresolver( () => _factorythunk(), //封装委托,闭包引用 new defaultcontrollerfactory { controllerbuilder = this }, "controllerbuilder.getcontrollerfactory"); }
这样singleserviceresolver对象中的工厂委托就会与controllerbuilder对象中的对应字段保持同步了,setcontrollerfactory方法也就达到了替换默认工厂的目的。
闭包引用测试代码:
using system; class program { public static void main(string[] args) { func f1 = ()=>null; func f2 = f1; //f1与f2指向同一个对象 object o = new object(); f1 = ()=>o; //更改f1后,f2仍然指向之前的对象 bool b1 = f1() == o; //true bool b2 = f2() == null; //true, f1()!=f2() print("直接赋值:"); print(f1(),"f1() == {0}"); print(f2(),"f2() == {0}"); print(f1() == f2(),"f1() == f2() ? {0}"); func ff1 = ()=>null; func ff2 = ()=>ff1(); //通过委托包装f1,形成闭包 object oo = new object(); ff1 = ()=>oo; //更改f1后,f2与f1保持同步 bool bb1 = ff1() == oo; //true bool bb2 = ff2() == oo; //true, f1()==f2() print("委托赋值:"); print(ff1(),"ff1() == {0}"); print(ff2(),"ff2() == {0}"); print(ff1() == ff2(),"ff1() == ff2() ? {0}"); console.readline(); } static void print(object mess,string format = "{0}") { string message = mess == null ? "null" : mess.tostring(); console.writeline(string.format(format,message)); } }
下面看一下singleserviceresolver类是如何实现对象的创建的,该类是个泛型类,这意味着可以构造任何类型的对象,不仅限于controllerfactory,实际上在mvc中,该类在很多地方都得到了应用,例如:controllerbuilder、defaultcontrollerfactory、buildmanagerviewengine等,实现了对多种对象的创建。
singleserviceresolver
该类实现了iresolver接口,主要用来提供指定类型的实例,在singleserviceresolver类中有三种方式来创建对象:
1、private lazy _currentvaluefromresolver; //内部调用_resolverthunk 2、private func _currentvaluethunk; //委托方式 3、private tservice _defaultvalue; //默认值方式 private func _resolverthunk; //idependencyresolver方式
从current方法中可以看出他们的优先级:
public tservice current { get { return _currentvaluefromresolver.value ?? _currentvaluethunk() ?? _defaultvalue; } }
_currentvaluefromresolver
实际上是对_resolverthunk
的封装,内部还是调用_resolverthunk
来实现对象的构造,所以优先级是:_resolverthunk > _currentvaluethunk > _defaultvalue
,即:idependencyresolver方式 > 委托方式 > 默认值方式。
singleserviceresolver在构造函数中默认实现了一个defaultdependencyresolver对象封装到委托字段_resolverthunk
中,该默认的resolver是以activator.createinstance(type)的方式创建对象的,但是有个前提,指定的type不能是接口或者抽象类,否则直接返回null。
在controllerbuilder类中实例化singleserviceresolver对象的时候指定的是icontrollerfactory接口类型,所以其内部的singleserviceresolver对象无法通过idependencyresolver方式创建对象,那么创建controllerfactory对象的职责就落到了_currentvaluethunk
(委托方式)和_defaultvalue
(默认值方式)这两个方式上,前面说过,singleserviceresolver类中的委托字段实际上是通过闭包引用controllerbuilder类中的相应委托来创建对象的,而在controllerbuilder类中,这个对应的委托默认是返回null,
private func _factorythunk = () => null;
因此,默认情况下singleserviceresolver类的第二种方式也失效了,那么此时也只能依靠默认值方式来提供对象了,在controllerbuilder类中这个默认值是defaultcontrollerfactory:
internal controllerbuilder(iresolver serviceresolver) { _serviceresolver = serviceresolver ?? new singleserviceresolver( () => _factorythunk(), new defaultcontrollerfactory { controllerbuilder = this }, //默认值 "controllerbuilder.getcontrollerfactory"); }
所以,在默认情况下是使用defaultcontrollerfactory类来构造controller的。
在创建singleserviceresolver对象的时候,可以从三个地方判断出真正创建对象的方法是哪种:
new singleserviceresolver( //1、看泛型接口,如果为接口或抽象类,则idependencyresolver方式失效 () => _factorythunk(), //2、看_factorythunk()是否返回null,如果是则委托方式失效 new defaultcontrollerfactory { controllerbuilder = this }, //3、以上两种都失效,则使用该默认值 "controllerbuilder.getcontrollerfactory");
通过以上创建对象的过程可以得知,有两种方式可以替换默认的对象提供器:
替换默认的dependencyresolver,可以通过dependencyresolver类的静态方法setresolver方法来实现:
customdependencyresolver customresolver = new customdependencyresolver(); dependencyresolver.setresolver(customresolver);
将以上语句放在程序启动的地方,例如:application_start
通过前面介绍的controllerbuilder类的setcontrollerfactory方法
注:第一种方式的优先级更高。
controllerfactory
通过controllerbuilder创建出controllerfactory对象后,下面就要利用该对象完成具体controller的创建,controllerfactory都实现了icontrollerfactory接口,通过实现createcontroller
方法完成对controller的实例化,createcontroller的内部逻辑非常简单,就两步:获取controller类型,然后创建controller对象。
获取controller类型
根据控制器名称获取控制器type的过程,有必要深入了解一下,以便于我们在日后遇到相关问题的时候能够更好的进行错误定位。获取类型的逻辑都封装在getcontrollertype方法中,该过程根据路由数据中是否含有命名空间信息,分为三个阶段进行类型搜索:
首先,如果当前路由数据中存在命名空间信息,则在缓存中根据控制器名称和命名空间搜索对应的类型,如果找到唯一一个类型,则返回该类型,找到多个直接抛异常其次,如果当前路由数据中不存在命名空间信息,或在第一阶段的搜索没有找到对应的类型,并且usenamespacefallback==true
,此时会获取controllerbuilder中设置的命名空间信息,利用该信息和控制器名称在缓存中进行类型搜索,如果找到唯一一个类型,则返回该类型,找到多个直接抛异常最后,如果路由数据和controllerbuilder中都没有命名空间信息,或者在以上两个阶段都没有搜索到对应的controller类型,那么会忽略命名空间,在缓存中仅按照控制器名称进行类型搜索,如果找到唯一一个类型,则返回该类型,找到多个直接抛异常
因此,命名空间的优先级是:routedata > controllerbuilder
在缓存中搜索类型的时候,如果是第一次查找,会调用controllertypecache.ensureinitialized方法将保存在硬盘中的xml缓存文件加载到一个字典类型的内存缓存中。如果该缓存文件不存在,则会遍历当前应用引用的所有程序集,找出所有public权限的controller类型(判断条件:实现icontroller接口、非抽象类、类名以controller结尾),然后将这些类型信息进行xml序列化,生成缓存文件保存在硬盘中,以便于下次直接从缓存文件中加载,同时将类型信息分组以字典的形式缓存在内存中,提高搜索效率,字典的key为controllername(不带命名空间)。
controller类型搜索流程如下图所示:
创建controller对象
获取controller类型以后,接下来就要进行controller对象的创建。在defaultcontrollerfactory类的源码中可以看到,同controllerbuilder类似,该类的构造函数中也实例化了一个singleserviceresolver对象,按照之前介绍的方法,我们一眼就可以看出,该对象是利用默认值的方式提供了一个defaultcontrolleractivator对象。
_activatorresolver = activatorresolver ?? new singleserviceresolver( //1、泛型为接口,idependencyresolver方式失效 () => null, //2、返回了null,委托方式失效 new defaultcontrolleractivator(dependencyresolver), //3、以上两种方式均失效,则使用该提供方式 "defaultcontrollerfactory constructor");
实际上defaultcontrollerfactory类仅实现了类型的搜索,对象的真正创建过程需要由defaultcontrolleractivator类来完成,默认情况下,defaultcontrolleractivator创建controller的过程是很简单的,因为它实际上使用的是一个叫做defaultdependencyresolver的类来进行controller创建的,在该类内部直接调用activator.createinstance(servicetype)
方法完成对象的实例化。
从defaultcontrollerfactory和defaultcontrolleractivator这两个类的创建过程可以发现,mvc提供了多种方式(idependencyresolver方式、委托方式 、默认值方式)来提供对象,因此在对mvc相关模块进行扩展的时候,也有多种方式可以采用。
controller中的数据容器
controller中涉及到几个给view传值的数据容器:tempdata、viewdata和viewbag。前两者的不同之处在于tempdata仅存储临时数据,里面的数据在第一次读取之后会被移除,即:只能被读取一次;viewdata和viewbag保存的是同一份数据,只不过viewbag是动态对象,对viewdata进行了封装。
public dynamic viewbag { get { if (_dynamicviewdatadictionary == null) { _dynamicviewdatadictionary = new dynamicviewdatadictionary(() => viewdata); //封装viewdata } return _dynamicviewdatadictionary; } }
下面简单说一下tempdata的实现原理。
tempdata
首先看下msdn上是如何解释的:
你可以按使用 viewdatadictionary 对象的相同方式使用 tempdatadictionary 对象传递数据。 但是,tempdatadictionary 对象中的数据仅从一个请求保持到下一个请求,除非你使用 keep 方法将一个或多个键标记为需保留。 如果键已标记为需保留,则会为下一个请求保留该键。
tempdatadictionary 对象的典型用法是,在数据重定向到一个操作方法时从另一个操作方法传递数据。 例如,操作方法可能会在调用 redirecttoaction 方法之前,将有关错误的信息存储在控制器的 tempdata 属性(该属性返回 tempdatadictionary 对象)中。 然后,下一个操作方法可以处理错误并呈现显示错误消息的视图。
tempdata的特性就是可以在两个action之间传递数据,它会保存一份数据到下一个action,并随着再下一个action的到来而失效。所以它被用在两个action之间来保存数据,比如,这样一个场景,你的一个action接受一些post的数据,然后交给另一个action来处理,并显示到页面,这时就可以使用tempdata来传递这份数据。
tempdata实现了idictionary接口,同时内部含有一个idictionary类型的私有字段,并添加了相关方法对字典字段的操作进行了控制,这明显是代理模式的一个应用。因为tempdata需要在action之间传递数据,因此要求其能够对自身的数据进行保存,tempdata依赖itempdataprovider接口实现了数据的加载与保存,默认情况下是使用sessionstatetempdataprovider对象将tempdata中的数据存放在session中。
下面看一下tempdata是如何控制数据操作的,tempdatadictionary源码中有这样一段定义:
internal const string tempdataserializationkey = "__tempdata"; private dictionary _data; private hashset _initialkeys = new hashset(stringcomparer.ordinalignorecase); private hashset _retainedkeys = new hashset(stringcomparer.ordinalignorecase);
私有字典字段_data是真正存放数据的地方,哈希集合_initialkeys和_retainedkeys用来标记数据,_initialkeys中存放尚未被读取的数据key,_retainedkeys存放可以被多次访问的key。
tempdatadictionary对数据操作的控制行为主要体现在在读取数据的时候并不会立即从_data中删除对应的数据,而是通过_initialkeys和_retainedkeys这两个hashset标记每条数据的状态,最后在通过itempdataprovider进行保存的时候再根据之前标记的状态对数据进行过滤,这时才去除已访问过的数据。
相关的控制方法有:trygetvalue、add、keep、peek、remove、clear
1、trygetvalue
public bool trygetvalue(string key, out object value) { _initialkeys.remove(key); return _data.trygetvalue(key, out value); }
该方法在读取数据的时候,会从_initialkeys集合中移除对应的key,前面说过,因为_initialkeys是用来标记数据未访问状态的,从该集合中删除了key,之后在通过itempdataprovider保存的时候就会将数据从_data字典中删除,下一次请求就无法再从tempdata访问该key对应的数据了,即:数据只能在一次请求中使用。
2、add
public void add(string key, object value) { _data.add(key, value); _initialkeys.add(key); }
添加数据的时候在_initialkeys中打上标记,表明该key对应的数据可以被访问。
3、keep
public void keep(string key) { _retainedkeys.add(key); }
调用keep方法的时候,会将key添加到_retainedkeys中,表明该条记录可以被多次访问,为什么可以被多次访问呢,可以从save方法中找到原因:
public void save(controllercontext controllercontext, itempdataprovider tempdataprovider) { // frequently called so ensure delegate is stateless _data.removefromdictionary((keyvaluepair entry, tempdatadictionary tempdata) => { string key = entry.key; return !tempdata._initialkeys.contains(key) && !tempdata._retainedkeys.contains(key); }, this); tempdataprovider.savetempdata(controllercontext, _data); }
可以看出,在保存的时候,会从_data中取出每一条数据,判断该数据的key是否存在于_initialkeys和_retainedkeys中,如果都不存在才会从_data中移除,所以keep方法将key添加到_retainedkeys后,该数据就不会被删除了,即:可以在多个请求中被访问了。
4、peek
public object peek(string key) { object value; _data.trygetvalue(key, out value); return value; }
从代码中可以看出,该方法在读取数据的时候,仅仅是从_data中进行了获取,并没有移除_initialkeys集合中对应的key,因此通过该方法读取数据不影响数据的状态,该条数据依然可以在下一次请求中被使用。
5、remove 与 clear
public bool remove(string key) { _retainedkeys.remove(key); _initialkeys.remove(key); return _data.remove(key); } public void clear() { _data.clear(); _retainedkeys.clear(); _initialkeys.clear(); }
这两个方法没什么多说的,只是在删除数据的时候同时删除其对应的状态。
推荐阅读
-
ASP.NET MVC Controller的激活
-
在Asp.Net MVC中实现CompareValues标签对Model中的属性进行验证
-
你所不知道的ASP.NET Core MVC/WebApi基础系列 (一)
-
ASP.NET MVC 项目设置,移除多余的响应头,woff,woff2 字体文件请求处理
-
ASP.NET MVC实现依赖注入的完整过程
-
学习ASP.NET MVC5框架揭秘笔记-ASP.NET MVC是如何运行的(一)
-
基于asp.net mvc的近乎产品开发培训课程(第四讲)
-
ASP.NET MVC中从前台页面视图(View)传递数据到后台控制器(Controller)方式
-
ASP.NET MVC 学习3、Controller左手从Model获取数据,右手传递到View页面
-
使用ASP.NET MVC的力软快速开发平台有什么优势