通过源代码研究ASP.NET MVC中的Controller和View(一)
通过源代码研究ASP.NET MVC中的Controller和View(二)
通过源代码研究ASP.NET MVC中的Controller和View(三)
通过源代码研究ASP.NET MVC中的Controller和View(四)
通过源代码研究ASP.NET MVC中的Controller和View(五)
上篇谈到Controller最终把执行的操作外包给了ActionInvoker,其默认实现大体上是这么一个过程:
- 查找Action(FindAction)
- 获取参数
- InvokeActionMethod
- InvokeActionResult
那我先从查找Action入手研究其逻辑,其相关代码如下:
ControllerDescriptor controllerDescriptor = GetControllerDescriptor( controllerContext );
ActionDescriptor actionDescriptor = FindAction( controllerContext, controllerDescriptor, actionName );
首先获取了一个Controllerdescriptor,然后借助ControllerDescriptor查找ActionDescriptor。
先来看看这两个类型分别代表什么,从名称来看,应该是控制器和行为的描述符,那么具体描述了一些什么东西呢?
GetCustomAttributes和IsDefined三个方法是用于实现ICustomAttributeProvider(实现这个接口用于获取附着在描述对象的特性,System.Reflection下大部分描述元数据的类型都实现了这个接口,如Assembly、MethodInfo等)的,除此之外,主要就是FindAction和GetCanonicalActions(获取经典的行为?)。
继续来看看ActionDescriptor:
除去实现ICustomAttributeProvider之外的三个方法,我看到还有这样几个方法:
- Execute( ControllerContext, IDictionary
) - GetFilters() : FilterInfo
- GetParameters() : ParameterDescriptor[]
- GetSelectors() : ICollection
我发现了一个有趣的事实,ControllerDescriptor通过FindAction方法可以获得一个ActionDescriptor,而ActionDescriptor又可以GetParameters来获取一个ParameterDescriptor的数组。换言之,ControllerDescriptor是一个ActionDescriptor的抽象容器,而ActionDescriptor是一个ParameterDescriptor的抽象容器。从这些名称你能看出啥?
考虑到ControllerDescriptor.ControllerType的存在,我有理由相信,ControllerDescriptor是一个依赖具体类型的描述符,换言之这是一个TypeDescriptor,而从名称来看,ParameterDescriptor应该是参数的描述符,类型描述符包含操作描述符集合,操作描述符包含参数描述符集合。直接推论:ActionDescriptor应该是一个方法描述符(MethodDescriptor),至少是一个被抽象的方法的描述符。它可以传递一些parameter来被Execute,得到一个object。即使ActionDescriptor没有对应某个具体的方法,从GetParamters和Execute来看,它至少可以被当作一个方法来发现、绑定(Bind,利用ParameterDescriptor[])以及调用执行(Execute)。不妨顺便来看看ParameterDescriptor:
这个东西看起来的确就是一个参数描述符。思路大体上能够理顺了,那么接下来,是研究实现的时间。
看看GetControllerDescription的实现:
protected virtual ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext )
{
Type controllerType = controllerContext.Controller.GetType();
ControllerDescriptor controllerDescriptor = DescriptorCache.GetDescriptor( controllerType, () => new ReflectedControllerDescriptor( controllerType ) );
return controllerDescriptor;
}
首先是获取Controller的运行时类型,然后这个DescriptorCache.GetDescriptor从名字和调用上可以大体上猜测,这个方法会首先到缓存中根据controllerType查找有没有缓存的东东,如果没有,就调用后面的匿名方法创建实例返回并缓存,来证实一下:
internal sealed class ControllerDescriptorCache : ReaderWriterCache<Type, ControllerDescriptor>
{
public ControllerDescriptorCache()
{
}
public ControllerDescriptor GetDescriptor( Type controllerType, Func<ControllerDescriptor> creator )
{
return FetchOrCreateItem( controllerType, creator );
}
}
FetchOrCreateItem这个方法名进一步的证实了猜测&#xff0c;我们继续看这个方法的实现&#xff1a;
protected TValue FetchOrCreateItem( TKey key, Func
{
// first, see if the item already exists in the cache
_rwLock.EnterReadLock();
try
{
TValue existingEntry;
if ( _cache.TryGetValue( key, out existingEntry ) )
{
return existingEntry;
}
}
finally
{
_rwLock.ExitReadLock();
}
// insert the new item into the cache
TValue newEntry &#61; creator();
_rwLock.EnterWriteLock();
try
{
TValue existingEntry;
if ( _cache.TryGetValue( key, out existingEntry ) )
{
// another thread already inserted an item, so use that one
return existingEntry;
}
_cache[key] &#61; newEntry;
return newEntry;
}
finally
{
_rwLock.ExitWriteLock();
}
}
结果已经非常明朗&#xff0c;_rwLock的EnterXXX和ExitXXX方法显然是进入和退出读锁以及写锁。去掉这些同步代码看起来就会是这样&#xff1a;
protected TValue FetchOrCreateItem( TKey key, Func
{
TValue existingEntry;
if ( _cache.TryGetValue( key, out existingEntry ) )
return existingEntry;
TValue newEntry &#61; creator();
_cache[key] &#61; newEntry;
return newEntry;
}
现在答案就是一目了然的了。
缓存的逻辑并非主线&#xff0c;还是回到GetControllerDescriptor继续分析。根据之前被证实的猜测&#xff0c;最终创建ControllerDescriptor的&#xff0c;就是这个匿名方法&#xff1a;
() &#61;> new ReflectedControllerDescriptor( controllerType )
换言之&#xff0c;其实的GetControllerDescriptor的实现大体上就是这样&#xff1a;
protected virtual ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext )
{
return new ReflectedControllerDescriptor( controllerContext.Controller.GetType() ) );
}
创建ControllerDescriptor也就是利用Controller的运行时类型创建一个ReflectedControllerDescriptor的实例而已。这进一步证实了ControllerDescriptor其实是一个TypeDescriptor的猜测。
接下来看FindAction的实现&#xff1a;
protected virtual ActionDescriptor FindAction( ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName )
{
ActionDescriptor actionDescriptor &#61; controllerDescriptor.FindAction( controllerContext, actionName );
return actionDescriptor;
}
FindAction啥活儿也没干&#xff0c;直接把工作又外包给了刚创建的ControllerDescriptor对象&#xff0c;我们知道ControllerDescriptor其实是一个ReflectedControllerDescriptor的实例&#xff0c;所以来看看这个实例的实现&#xff1a;
public override ActionDescriptor FindAction( ControllerContext controllerContext, string actionName )
{
if ( controllerContext &#61;&#61; null )
{
throw new ArgumentNullException( "controllerContext" );
}
if ( String.IsNullOrEmpty( actionName ) )
{
throw new ArgumentException( MvcResources.Common_NullOrEmpty, "actionName" );
}
MethodInfo matched &#61; _selector.FindActionMethod( controllerContext, actionName );
if ( matched &#61;&#61; null )
{
return null;
}
return new ReflectedActionDescriptor( matched, actionName, this );
}
调用了_selector的FindActionMethod方法来得到一个方法信息&#xff08;MethodInfo&#xff09;然后用这个方法来创建一个ReflectedActionDescriptor的实例。看来刚才的猜测一点没错&#xff0c;ActionDescriptor的确是一个方法的描述符。那么&#xff0c;这个_selector又是什么&#xff1f;
private readonly ActionMethodSelector _selector;
哈&#xff0c;又引入了一个新的类型ActionMethodSelector&#xff0c;从名字来看&#xff0c;这个类完全是为了Select一个Method而存在的。这个类型没有任何派生类&#xff0c;也不派生自任何类&#xff0c;并且还是一个密封类&#xff08;sealed&#xff09;&#xff0c;职责也非常明确&#xff0c;就是选择ActionMethod&#xff0c;而这个 ActionMethod应该就是我们在控制器中写的什么Index或是 About方法。
还是来看看FindActionMethod的实现&#xff1a;
public MethodInfo FindActionMethod( ControllerContext controllerContext, string actionName )
{
List<MethodInfo> methodsMatchingName &#61; GetMatchingAliasedMethods( controllerContext, actionName );
methodsMatchingName.AddRange( NonAliasedMethods[actionName] );
List<MethodInfo> finalMethods &#61; RunSelectionFilters( controllerContext, methodsMatchingName );
switch ( finalMethods.Count )
{
case 0:
return null;
case 1:
return finalMethods[0];
default:
throw CreateAmbiguousMatchException( finalMethods, actionName );
}
}
先调用了GetMatchingAliasedMethods方法&#xff0c;然后再将这个方法的结果与NonAliasedMethods[actionName]合并&#xff0c;最后RunSelectionFilters&#xff08;运行选择筛选器&#xff09;。最后看获取的方法恰好一个的话就返回。
这里的Matching和Aliased容易把人搞晕&#xff0c;求助谷歌大神&#xff0c;matching是一个形容词&#xff0c;相匹配的意思。aliased谷歌大神也没办法帮我&#xff0c;但我知道alias是别名的意思&#xff0c;推测aliased是alias的过去式&#xff0c;那就是已经alias的意思&#xff0c;或者被alias的意思。也许&#xff0c;就是被别名的意思吧。
所以GetMatchingAliasedMethod的解释就是&#xff1a;获取 相匹配的 被别名的 方法。
呃&#xff0c;&#xff0c;&#xff0c;先不看方法&#xff0c;因为我看到有一个很奇怪的对象叫做NonAliasedMethods&#xff0c;这个东西是哪来的&#xff1f;值是什么&#xff1f;
public ILookup<string, MethodInfo> NonAliasedMethods
{
get;
private set;
}
哈&#xff0c;这玩意儿竟然是个ILookup&#xff0c;不常见啊&#xff0c;那么他的值是哪里来的&#xff0c;看看构造函数&#xff1a;
public ActionMethodSelector( Type controllerType )
{
ControllerType &#61; controllerType;
PopulateLookupTables();
}
然后&#xff1a;
private void PopulateLookupTables()
{
MethodInfo[] allMethods &#61; ControllerType.GetMethods( BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public );
MethodInfo[] actionMethods &#61; Array.FindAll( allMethods, IsValidActionMethod );
AliasedMethods &#61; Array.FindAll( actionMethods, IsMethodDecoratedWithAliasingAttribute );
NonAliasedMethods &#61; actionMethods.Except( AliasedMethods ).ToLookup( method &#61;> method.Name, StringComparer.OrdinalIgnoreCase );
}
哈&#xff0c;在这里看到了两个熟悉的东西&#xff0c;AliasedMethods和NonAliasedMethods。他们分别是这么来的&#xff1a;
首先allMethods是ControllerType&#xff08;就是传给ControllerDescriptor的Controller.GetType()&#xff0c;具体实现可以自己看源代码&#xff09;的所有公开的实例方法集合。然后对这个集合进行了一次筛选&#xff0c;Array.FindAll其实就类似于Where方法&#xff0c;后面的那个IsValidActionMethod是筛选条件&#xff0c;这个方法的实现是这样的&#xff1a;
private static bool IsValidActionMethod( MethodInfo methodInfo )
{
return !(methodInfo.IsSpecialName ||
methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom( typeof( Controller ) ));
}
那么这里定义了几种情况不是合法的ActionMethod&#xff08;会被筛掉&#xff09;&#xff1a;
- 是特殊的名称&#xff08;编译器生成的方法、构造函数等&#xff09;
- 方法的原始声明类型&#xff08;假设一个类型A有一个虚方法virtual Test&#xff0c;被派生类B重写为override Test&#xff0c;则GetBaseDefinition获取到A中定义的虚方法Test&#xff0c;即为原始声明类型&#xff09;是Controller或是Controller的基类&#xff08;IsAssignableFrom&#xff09;。
简单的说&#xff0c;编译器生成的方法和定义在Controller里面的方法&#xff0c;就不是合法的ActionMethod&#xff0c;除此之外&#xff0c;都是。
结合起来&#xff1a;ControllerType里面所有公开的实例方法&#xff0c;除去编译器生成的、构造函数、Controller及其基类定义的方法及他们的重写之外&#xff0c;剩下的都是ActionMethod&#xff08;看来返回值没什么限制哦&#xff0c;但也许限制不在这里&#xff09;。
然后&#xff0c;合法的ActionMethod&#xff08;actionMethods&#xff09;被分成两拨&#xff0c;一拨是满足IsMethodDecoratedWithAliasingAttribute的&#xff08;AliasedMethods&#xff09;&#xff0c;另一拨是剩下的&#xff08;Except&#xff09;。来看看这个名字很长的方法的实现&#xff1a;
private static bool IsMethodDecoratedWithAliasingAttribute( MethodInfo methodInfo )
{
return methodInfo.IsDefined( typeof( ActionNameSelectorAttribute ), true /* inherit */);
}
如果你记心好的话&#xff0c;应该会记得这个IsDefine刚才出现过&#xff0c;没错&#xff0c;这是ICustomAttributeProvider接口的一个成员。他用于检查方法是否定义了&#xff08;附着了&#xff09;某个类型的特性&#xff0c;这里这个类型是ActionNameSelectorAttribute&#xff0c;后面的true表示如果定义了这个特性类的派生类&#xff08;派生特性&#xff09;也算在内。
那么这里的逻辑可以理清了&#xff0c;所有定义了ActionNameSelectorAttribute特性的方法&#xff0c;都是AliasedMethod&#xff08;被别名的方法&#xff09;&#xff0c;除此之外&#xff0c;都是NonAliasedMethod&#xff08;没被别名的方法&#xff09;。
没有被别名的方法会被转换为一个ILookup对象&#xff0c;ILookup说白了&#xff0c;就是GroupBy的结果的可检索Key版本。ILookup首先是一个IEnumerable
好了&#xff0c;ILookup并不是重点&#xff0c;我看到这里作为Key的是method.Name&#xff08;方法名&#xff09;&#xff0c;并且传入了一个StringComparer.OrdinalIgnoreCase&#xff0c;不区分大小写的字符串比较器。也就是说这里的Key将不区分大小写。
回到FindActionMethod方法&#xff0c;那么NonAliasedMethods[actionName]就可以理解了&#xff0c;由于ILookup的Key是method.Name&#xff0c;所以NonAliasedMethods[actionName]就是获取所有名字和actionName一样的方法&#xff08;不区分大小写&#xff09;。
那么继续来看看GetMatchingAliasMethods的实现&#xff1a;
internal List<MethodInfo> GetMatchingAliasedMethods( ControllerContext controllerContext, string actionName )
{
// find all aliased methods which are opting in to this request
// to opt in, all attributes defined on the method must return true
var methods &#61; from methodInfo in AliasedMethods
let attrs &#61; (ActionNameSelectorAttribute[]) methodInfo.GetCustomAttributes( typeof( ActionNameSelectorAttribute ), true /* inherit */)
where attrs.All( attr &#61;> attr.IsValidName( controllerContext, actionName, methodInfo ) )
select methodInfo;
return methods.ToList();
}
LINQ表达式的描述性很强&#xff0c;我很喜欢&#xff0c;这段LINQ表达式直接描述是这样的&#xff1a;
从AliasedMethod集合中获取一个个methodInfo&#xff0c;获取methodInfo的ActionNameSelectorAttribute特性集合并命名为attrs&#xff0c;从中所有这些methodInfo中筛选出attrs集合中每一项都满足IsValidName的项。
简单的说&#xff0c;选择AliasedMethod中&#xff0c;所有ActionNameSelectorAttribute特性都满足IsValidName的methodInfo&#xff0c;那么&#xff0c;IsValidName又是什么逻辑&#xff1f;
这个方法在ActionNameSelectorAttribute中是一个抽象方法&#xff0c;这个类只有一个实现类ActionNameAttribute&#xff0c;所以这个方法也就只有一份实现&#xff08;至少在 MVC框架里&#xff09;&#xff1a;
ActionNameAttribute:
public override bool IsValidName( ControllerContext controllerContext, string actionName, MethodInfo methodInfo )
{
return String.Equals( actionName, Name, StringComparison.OrdinalIgnoreCase );
}
那么这里就是简单的比较了一下actionName和自己的Name属性。这个特性干什么用的基本上也就能推导出来了&#xff0c;如果你想给方法取一个别名&#xff08;不用方法名作为actionName&#xff09;&#xff0c;就可以应用这个特性&#xff0c;然后取一个你喜欢的名字。
这里的实现似乎存在一个非常明显的Bug&#xff0c;如果我为一个方法取了两个别名&#xff0c;那么这个方法应该就不可能被映射到了。因为这里的判断逻辑是所有&#xff08;All&#xff09;的Attribute都要IsValidName&#xff0c;换言之这个actionName要同时等于两个别名&#xff0c;才会被选择&#xff0c;这显然不可能。所以这里的All应该改为Any才对。
不过事实上&#xff0c;一个方法不能被附着两个ActionNameAttribute&#xff0c;因为这个特性是不能多次应用的&#xff08;在这个类型和基类的AttributeUsage定义了AllowMultiple &#61; false&#xff09;&#xff0c;所以不可能出现两个这样的特性。
OK&#xff0c;至此&#xff0c;已经可以完全了解FindActionMethod前段的逻辑了&#xff1a;
- 从被取了别名的&#xff08;Aliased&#xff09;方法中找别名&#xff08;ActionNameAttribute.Name&#xff09;与actionName相匹配的方法
- 再从没有取别名的方法中找方法名与actionName相匹配的方法
- 把这两个结果整合&#xff08;AddRange&#xff09;。
- 再运行SelectionFilter&#xff08;选择筛选器&#xff1f;&#xff09;
- 最后如果结果集里只有一个方法&#xff0c;那么返回&#xff0c;有多个则异常&#xff0c;没有则返回空。
最后来看看选择筛选器干了些什么&#xff1a;
private static List<MethodInfo> RunSelectionFilters( ControllerContext controllerContext, List<MethodInfo> methodInfos )
{
// remove all methods which are opting out of this request
// to opt out, at least one attribute defined on the method must return false
List<MethodInfo> matchesWithSelectionAttributes &#61; new List<MethodInfo>();
List<MethodInfo> matchesWithoutSelectionAttributes &#61; new List<MethodInfo>();
foreach ( MethodInfo methodInfo in methodInfos )
{
ActionMethodSelectorAttribute[] attrs &#61; (ActionMethodSelectorAttribute[]) methodInfo.GetCustomAttributes( typeof( ActionMethodSelectorAttribute ), true /* inherit */);
if ( attrs.Length &#61;&#61; 0 )
{
matchesWithoutSelectionAttributes.Add( methodInfo );
}
else if ( attrs.All( attr &#61;> attr.IsValidForRequest( controllerContext, methodInfo ) ) )
{
matchesWithSelectionAttributes.Add( methodInfo );
}
}
// if a matching action method had a selection attribute, consider it more specific than a matching action method
// without a selection attribute
return ( matchesWithSelectionAttributes.Count > 0 ) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes;
}
首先定义了两个列表&#xff0c;With和Without Selection Attributes&#xff0c;然后遍历所有的方法&#xff0c;获取方法上附着的ActionMethodSelectorAttribute&#xff0c;如果方法上没有这个特性&#xff08;attrs.Length &#61;&#61; 0&#xff09;&#xff0c;那么归入matchesWithoutSelectionAttributes这一拨&#xff0c;如果方法上有这个特性&#xff0c;那么调用特性的IsValidForRequest&#xff0c;为true的归入matchesWithSelectionAttributes这一拨&#xff0c;其他的方法抛弃。
最后&#xff0c;如果With这一拨有任何方法&#xff0c;返回With这一拨&#xff0c;否则返回Without这一拨。
简单的说&#xff1a;
如果有方法附着了ActionMethodSelectorAttribute&#xff0c;而又IsValidForRequest的话&#xff0c;那么就返回这些方法。否则&#xff0c;返回没有附着ActionMethodSelectorAttribute的方法。
当然&#xff0c;ActionMethodSelectorAttribute也是一个抽象类&#xff0c;但他的派生类很多&#xff1a;
不过这不要紧&#xff0c;因为我看到了HttpPostAttribute&#xff0c;其实那就是[HttpPost]么&#xff0c;在MVC范例网站的AccountController里面就能看到&#xff1a;
[HttpPost]
public ActionResult LogOn( LogOnModel model, string returnUrl )
{
if ( ModelState.IsValid )
{
if ( MembershipService.ValidateUser( model.UserName, model.Password ) )
{
FormsService.SignIn( model.UserName, model.RememberMe );
if ( !String.IsNullOrEmpty( returnUrl ) )
{
return Redirect( returnUrl );
}
else
......
我知道HttpPost是用来标识仅当请求是以Post方式提交的时候才调用这个方法&#xff0c;那么这个Attribute的IsValidForRequest的实现则可以简单的检查请求是不是POST提交过来的达到所需要的效果。其实现我瞄了一眼&#xff0c;比较麻烦&#xff0c;就不在这里展开了&#xff0c;还是尽快走主线逻辑吧&#xff0c;这些内容大家如果有兴趣完全可以自行研究。
写在最后&#xff0c;这里的逻辑非常值得注意&#xff0c;由于在SelectionFilters之后&#xff0c;如果方法组中还存在有多个方法&#xff0c;则会直接抛出异常。可以知道&#xff08;最重要结论&#xff09;&#xff1a;
- 同名&#xff08;方法名或别名&#xff09;的方法一定要有不同性质的ActionMethodSelectorAttribute&#xff08;没有也算一种性质&#xff09;。
- 如果同一个性质的ActionMethodSelectorAttribute被应用到两个同名的方法&#xff0c;当这个Attribute验证通过时&#xff0c;将出错&#xff0c;这很危险&#xff0c;也是容易造成隐患的地方。
- MVC框架内所有的这些ActionMethosSelectorAttribute除了AcceptVerbsAttribute之外都是互斥的&#xff08;不可能同时满足&#xff09;&#xff0c;这样的好处是只要两个同名方法没有附着一样类型的特性&#xff0c;就一定不会同时列入候选列表而抛出异常。但如果你自己写了一些ActionMethodSelector与现有的不互斥&#xff0c;你要特别注意会不会有一种特定的情况导致两个同名方法同时满足&#xff0c;这将是很难检出的隐患。
- 方法的签名在FindAction的过程中是被无视的&#xff0c;除非你自己写一个ActionMethodSelectorAttribute来判断方法签名与当前请求是否匹配。
- 综上所述&#xff0c;没事别整同名的方法。
这一篇就到这里了。
在结束之前&#xff0c;我分享一个非常搞笑的ActionMethodSelectorAttribute实现&#xff1a;
namespace System.Web.Mvc
{
using System.Reflection;
[AttributeUsage( AttributeTargets.Method, AllowMultiple &#61; false, Inherited &#61; true )]
public sealed class *******Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest( ControllerContext controllerContext, MethodInfo methodInfo )
{
return false;
}
}
}
这是MVC框架里面的一个类型的源代码&#xff0c;类型的的名字被打上了马赛克&#xff0c;不妨猜猜这个Attribute到底是干啥用的&#xff0c;以及&#xff0c;它的名字是什么。。。。