ASP.NET Core MVC 原始碼學習:詳解 Action 的匹配
前言
在 上一篇 文章中,我們已經學習了 ASP.NET Core MVC 的啟動流程,那麼 MVC 在啟動了之後,當請求到達過來的時候,它是怎麼樣處理的呢? 又是怎麼樣把我們的請求準確的傳達到我們的 Action 上呢? 那麼,在這邊文章中,我們一起跟蹤原始碼看一下,框架都做了些什麼東西。
Getting Started
我們知道,Startup.cs 中的 Configure(IApplicationBuilder
app)
中,我們使用 app.UseMvc()
在 UseMVC() 程式碼執行的過程中,它可以接收一個 Action<IRouteBuilder>
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
或者是你使用預設的 app.UseMvcWithDefaultRoute()
,這個擴充套件方法在內部已經幫你做了上述程式碼的內容。
那我們今天就從這個 Route 的配置開始看起吧。
RouteContext 如何初始化?
在 IRouteBuilder 通過配置 IRouteBuilder,IRouteBuilder 在 Build() 之後會得到 Router 會得到 IRouter
public static IApplicationBuilder UseMvc(
this IApplicationBuilder app,
Action<IRouteBuilder> configureRoutes)
{
// ......
var routes = new RouteBuilder(app)
{
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
};
configureRoutes(routes);
routes.Routes.Insert(0 , AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));
return app.UseRouter(routes.Build());
}
上面的程式碼有兩個地方需要注意的。
第一個地方是 DefaultHandler
,可以看到預設配置下,MVC
程式從 DI 中獲取 MvcRouteHandler
路由處理程式來作為路由的預設處理程式。
第二個地方是 AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices)
,那麼這個地方是幹嘛的呢?
CreateAttributeMegaRoute 它返回了一個 IRouter ,主要是用來處理帶 RouteAttribute
標記的
Action,我們來看一下這個方法:
public static IRouter CreateAttributeMegaRoute(IServiceProvider services)
{
return new AttributeRoute(
services.GetRequiredService<IActionDescriptorCollectionProvider>(),
services,
actions =>
{
var handler = services.GetRequiredService<MvcAttributeRouteHandler>();
handler.Actions = actions;
return handler;
});
}
在方法內部,new 了一個 AttributeRoute
返回了回去,大家可以看到有一個引數
actions,它使用的是 MvcAttributeRouteHandler
這個處理程式,說明在實際呼叫過程中使用的是 MvcAttributeRouteHandler
進行的路由處理。
OK,我們總結一下關於 MVC 自己的幾個路由處理程式,還是用一個圖比較容易看的清楚,幸運的是,MVC 一共就這3個路由處理程式,我們已經全部接觸到了。
MVC 框架針對於 IRouter 介面的實現有以下三個:
提前告訴你,最左邊綠色的那個 AttributeRoute
其實只是一個包裝,在內部也是通過 MvcAttributeRouteHandler
或者 MvcRouteHandler
進行的處理。那麼,現在關於路由的處理程式只剩下了兩個,他們分別是:
預設處理程式: MvcRouteHandler
,用來處理約定的
Action。
註解處理程式: MvcAttributeRouteHandler
,用來處理註解(Attribute)路由。
細心的同學可能注意到了, MvcAttributeRouteHandler
比 MvcRouteHandler
多了一個 Actions
: ActionDescriptor[]
屬性。
我們再看一下這兩個處理程式的 RouteAsync 方法,這個方法是路由元件的入口方法,我們通過一個對比工具來看一下兩者之間的差距。
圖片看不清楚可以新標籤開啟
可以看到,這兩個 RouteAsync 主要有兩處差距,第一處就是 SelectBestCandidate 這個函式第二個引數
ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
MvcRouteHandler:
在這個流程中,顯示呼叫了 IActionSelect 介面中的 SelectCandidates() 用來找到所有符合條件的候選 Action,然後呼叫了 SelectBestCandidate 找出最佳的一個。
程式走到這裡,這裡會有兩個重點的地方,或者叫有疑問的地方?
1、 程式集中定義的 Action 是怎麼找到的?
要想找到程式定義的所有 Action,那麼首先需要找到 Controller,在上一篇文章中我們已經知道了有一個 MVC 程式用來管理 AssemblyPart 的東西叫 ApplicationPartManager ,它的裡面儲存了所有 MVC 框架在啟動的時候載入的所有程式集,那麼我們可以從這個程式集中找到需要的 Controller。下面這個流程圖顯示了查詢Controller 的流程:
GetControllerTypes
返回的是一個 IEnumerable<TypeInfo>
的集合,有了
Controller 之後,MVC 框架使用了一個物件來包裝 Controller,因為在後續的流程中,除了需要 Controller 之外還需要其他的一些東西,比如 Filter
, ApiExplorer
等。
ApplicationModel
ApplicationModel
就是MVC框架用來包裝 Controller
,Filter
, ApiExplorer
等的一個Model
物件,我們來看一下它的定義:
public class ApplicationModel : IPropertyModel, IFilterModel, IApiExplorerModel
{
public ApplicationModel()
{
ApiExplorer = new ApiExplorerModel();
Controllers = new List<ControllerModel>();
Filters = new List<IFilterMetadata>();
Properties = new Dictionary<object, object>();
}
public ApiExplorerModel ApiExplorer { get; set; }
public IList<ControllerModel> Controllers { get; private set; }
public IList<IFilterMetadata> Filters { get; private set; }
public IDictionary<object, object> Properties { get; }
}
ApplicationModel
裡面關於
Controller 的包裝是一個 IList<ControllerModel>
,看一下 ControllerModel
的定義:
public class ControllerModel : ICommonModel, IFilterModel, IApiExplorerModel
{
//......
public IList<ActionModel> Actions { get; }
public ApiExplorerModel ApiExplorer { get; set; }
public ApplicationModel Application { get; set; }
public IReadOnlyList<object> Attributes { get; }
MemberInfo ICommonModel.MemberInfo => ControllerType;
string ICommonModel.Name => ControllerName;
public string ControllerName { get; set; }
public TypeInfo ControllerType { get; }
public IList<PropertyModel> ControllerProperties { get; }
public IList<IFilterMetadata> Filters { get; }
public IDictionary<string, string> RouteValues { get; }
public IDictionary<object, object> Properties { get; }
public IList<SelectorModel> Selectors { get; }
}
在 ASP.NET Core MVC 框架中,ApplicationModel
有下面幾個提供者,他們用於初始化整個
ApplicationModel 的各個部分,我們還是分別看一下吧。
AuthorizationApplicationModelProvider
:
處理認證相關業務邏輯,在它的Executing方法中會將 AuthorizeFilter
,AllowAnonymousFilter
等過濾器新增到
ApplicationModelProviderContext 裡面的 ApplicationModel 裡。
DefaultApplicationModelProvider
:初始化 ControllerModel
,
新增 Controller 相關的各種資訊,新增使用者自定義 Filter,遍歷 ControllerTypes : 建立
ControllerModel
--> 初始化Properties
--> 初始化Parameters
。
CorsApplicationModelProvider
:跨域資源相關邏輯,新增CorsAuthorizationFilterFactory
,DisableCorsAuthorizationFilter
,CorsAuthorizationFilterFactory
,DisableCorsAuthorizationFilter
等過濾器。
TempDataApplicationModelProvider
:
新增 SaveTempDataPropertyFilterFactory
過濾器,儲存Controller中的TempData資訊,注意
TempDataAttribute 修飾的屬性只能是基元型別或字串。
構建ApplicationModel
MVC 框架通過 ControllerActionDescriptorProvider
中的 BuildModel()
這個方法進行
ApplicationModel 的構建:
internal protected ApplicationModel BuildModel()
{
var controllerTypes = GetControllerTypes();
var context = new ApplicationModelProviderContext(controllerTypes);
for (var i = 0; i < _applicationModelProviders.Length; i++)
{
_applicationModelProviders[i].OnProvidersExecuting(context);
}
for (var i = _applicationModelProviders.Length - 1; i >= 0; i--)
{
_applicationModelProviders[i].OnProvidersExecuted(context);
}
return context.Result;
}
現在,我們已經有一個完整的 ApplicationModel 物件了。
有了 ApplicationModel 物件之後,會再進行一次約定的應用。比如以下Action重寫路由的情況或者配置多個路由的情況。
2、ActionDescriptorCollection是怎麼建立的?
ControllerActionDescriptor構建
ControllerActionDescriptor
的構建是基於ApplicationModel
物件的,下面我就畫了一個流程圖用來展示構建
ControllerActionDescriptor 的整個過程,就不過多描述了。
截止到目前,我們會得到一個 IEnumerable<ControllerActionDescriptor>
集合物件。
在有了 ControllerActionDescriptor
之後,ActionDescriptorCollectionProvider
會提供一個屬性,
public ActionDescriptorCollection ActionDescriptors
{
get
{
if (_collection == null)
{
UpdateCollection();
}
return _collection;
}
}
在這個屬性中使用了 UpdateCollection 這個方法來更新 ActionDescriptorCollection
。
private void UpdateCollection()
{
var context = new ActionDescriptorProviderContext();
for (var i = 0; i < _actionDescriptorProviders.Length; i++)
{
_actionDescriptorProviders[i].OnProvidersExecuting(context);
}
for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
{
_actionDescriptorProviders[i].OnProvidersExecuted(context);
}
_collection = new ActionDescriptorCollection(
new ReadOnlyCollection<ActionDescriptor>(context.Results),
Interlocked.Increment(ref _version));
}
OK , 現在我們有了 ActionDescriptorCollection
,
之後的流程就比較簡單了,但是會涉及到幾個演算法。
接下來,輪到 ActionSelectorDecisionTreeProvider
上場了,它主要是把 ActionDescriptorCollection
,組裝成為一個 IActionSelectionDecisionTree
物件以便於後續的查詢匹配工作, IActionSelectionDecisionTree
的資料結構是一個多叉樹,組裝過程是使用了一個深度優先的遞迴演算法。
我們回到起點,繼續看這張圖:
現在 SelectCandidates 你應該能夠看懂了:
public IReadOnlyList<ActionDescriptor> SelectCandidates(RouteContext context)
{
//IActionSelectionDecisionTree 物件
var tree = _decisionTreeProvider.DecisionTree;
//使用的是一個多叉樹查詢演算法,關於演算法可以看我這篇博文:
//http://www.cnblogs.com/savorboard/p/6582399.html
return tree.Select(context.RouteData.Values);
}
接下來就是 SelectBestCandidates
這個流程:
1、遍歷 Action 列表,評估 Action 的相關約束,返回匹配的 ActionDescriptor 列表。
2、從匹配的 ActionDescriptor 列表中返回最佳的 Action 列表,注意這裡這個方法 SelectBestActions
,它是一個虛方法,預設是沒有實現的會直接返回上一步的結果,也就是說使用者可以通過重寫這個方法來自定義一些Action匹配規則。
3、如果SelectBestActions 返回的是一個ActionDescriptor,則直接返回,當路由系統匹配到多個 Action 的時候,那麼 MVC 需要從這些 Action 候選者中選中最佳的哪一個,當兩個動作通過路由匹配時,MVC必須消除歧義以選擇“最佳”候選者,否則丟擲 AmbiguousActionException
異常
最終 SelectBestCandidates
會返回一個
ActionDescriptor ,即需要執行的 Action。
後續流程的執行,我又畫了一個圖來表示,希望能夠更加清晰一些:
終於講解結束了,心好累,如果你認為本篇文章對你有幫助的話,順手點個【推薦】吧。
MvcAttributeRouteHandler:
下面是MvcAttributeRouteHandler
的 RouteAsync
。
可以看到,在 MvcAttributeRouteHandler
中,少了 SelectCandidates()
這個流程,取而代之的是用
Actions 的屬性引數。 這個Actions 就比較簡單了,就是MVC框架啟動的時候配置的IRouter Action。
然後就是 SelectBestCandidates
這個流程了,參考上文的流程吧,都一樣。
總結
本文詳細描述了 MVC 在 Request 到達的時候是怎麼樣通過自定義的路由處理程式來選擇一個Action 的,並且講解了其中的過程。