Asp.Net MVC 路由
當用戶通過URL訪問網站時,要把用戶請求的URL映射到正確的應用程序的操作上。那么如何實現這個映射--Routing(路由)。
路由並不專屬於Asp.Net MVC
,而是建立在Asp.Net Framework
之上的一個組件,所以所有依賴Asp.Net Framework
的都可以使用路由。如WebForms,API等,但是Asp.Net MVC 和路由密切相關。
圖:路由關系圖
路由工作流程
Asp.Net是一個管道模型,一個Http請求先經過
HttpModule
,再通過HttpHandlerFactory
,創建一個對應的HttpHandler
處理對應的請求。所以對Asp.Net的所有的擴展也是通過注冊這些管道事件來實現的。因為路由是建立在Asp.Net Framework
之上的,所以路由也是注冊實現了管道事件。但是是通過注冊HttpModule
的PostResolveRequestCache
事件來實現的。
為什么不注冊HttpHandler來實現呢?
因為:
如果把請求的管道模型比作一個運行的火車的話,
HttpHandler
是請求火車的目的地。HttpModule
是一個沿途的站點,要在終點前分析好這個請求是到哪個目的地。
HttpHandler
多用來處理響應處理。HttpModule
多用來處理通用性和響應內容無關的功能。
小結:
路由就是一個實現了IHttpModule
接口的UrlRoutingModule
的HttpModule
,在管道事件中攔截請求,分析Url,匹配路由,再交給HttpHandler
處理的過程。
路由如何攔截請求
上述認識到路由是通過實現了接口IHttpModule
的類--UrlRoutingModule
來注冊管道事件,在該類中實現了請求攔截,路由匹配,創建指定HttpHandler。
所以路由組件中UrlRoutingModule
就是是關鍵。
通過該類的源代碼可以發現。UrlRoutingModule
注冊了PostResolveRequestCache
事件。注冊該事件,純粹是因為要在HttpHandler
目的地創建之前執行路由。因為在管道事件中PostMapRequestHandler
事件是把請求交給HttpHandler
來處理。而PostResolveRequestCache
在該事件之前。(Asp.Net管道事件)
//UrlRoutingModule源碼
...
//注冊事件PostResolveRequestCache
application.PostResolveRequestCache += OnApplicationPostResolveRequestCache;
...
查看UrlRoutingModule
代碼,發現該類的一個PostResolveRequestCache
方法,實現了路由的工作。
// UrlRoutingModule的本地方法
public virtual void PostResolveRequestCache(HttpContextBase context) {
// 根據HttpContext的Url匹配路由對象,該對象包含了Controller,Action和參數
// Match the incoming URL against the route table
RouteData routeData = RouteCollection.GetRouteData(context);
// Do nothing if no route found
if (routeData == null) {
return;
}
//由匹配的路由對象創建一個MVCRouteHandler
// If a route was found, get an IHttpHandler from the route's RouteHandler
IRouteHandler routeHandler = routeData.RouteHandler;
if (routeHandler == null) {
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
SR.GetString(SR.UrlRoutingModule_NoRouteHandler)));
}
// This is a special IRouteHandler that tells the routing module to stop processing
// routes and to let the fallback handler handle the request.
if (routeHandler is StopRoutingHandler) {
return;
}
//封裝匹配的路由對象和HttpContext,創建新的RequestContext
RequestContext requestContext = new RequestContext(context, routeData);
// Dev10 766875 Adding RouteData to HttpContext
context.Request.RequestContext = requestContext;
//獲取MVCHandler
IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
if (httpHandler == null) {
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentUICulture,
SR.GetString(SR.UrlRoutingModule_NoHttpHandler),
routeHandler.GetType()));
}
if (httpHandler is UrlAuthFailureHandler) {
if (FormsAuthenticationModule.FormsAuthRequired) {
UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this);
return;
}
else {
throw new HttpException(401, SR.GetString(SR.Assess_Denied_Description3));
}
}
// Remap IIS7 to our handler
context.RemapHandler(httpHandler);
}
即:
- 根據
HttpContext
,路由匹配規則,匹配一個RouteData
對象。 - 調用
RouteData
對象的RouteHandler
獲取IRouteHandler
的MVCRouteHandler
。 - 由匹配的
RouteData
和HttpContext
創建RequestContext
- 由2的
MVCRouteHandler
和3的RequestContext
創建IHttpHandler
-MVCHandler
. HttpHandler
管道事件執行。
流程如下圖所示:
之后HttpHandler
的運行可以參考如下整個生命周期:
Asp.Net MVC 生命周期圖:
路由的使用
Global.asax
的MVCApplication
是管理Asp.Net應用程序生命周期的管道事件的類。在類中實現管道事件或方法會在對應的管道事件中調用。
配置路由
在App_Start
文件下,新建RouteConfig.cs
文件里配置路由信息。通過靜態方法RouteCollection.MapRoute()
配置路由信息。
如:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");//忽略該模式的URL
routes.MapRoute(
name: "Default",//路由名稱
url: "{controller}/{action}/{id}",//路由模板
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }//路由默認值,參數id可以為空
);
}
}
-
name:為該路由名稱
-
url:為路由模板,
{}
是占位符。 -
defaults:為路由默認值
注冊路由
Global.asax
的MVCApplication
繼承HttpApplication
。而HttpApplication
則是管理整個管道周期的實例。在該類中通過注冊事件,或方法可以在管道事件中被調用。注冊路由到應用程序就是在Application_Start()
方法中實現。
如:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);//路由注冊到應用程序
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
URL匹配
在配置路由里創建了一個路由名為Default
的路由。該Default
路由由controller
,action
,id
三部分組成,其中id
為可選參數。
該路由可以匹配如下url:
- xxx.com/home/index/1
- xxx.com/home/index
- xxx.com/home
- xxx.com/
這些URL都會映射到如下Action:
public class HomeController :Controller
{
public ActionResult Index()
{
return View();
}
}
或
//在路由中id參數是可為空的,所以對於值類型的參數必須是可空的值類型。
public class HomeController :Controller
{
public ActionResult Index(int? id)
{
return View();
}
}
並且該Action
的參數名稱需要和Route
中的參數(id)一致。即也是id。才可以匹配xxx.com/home/index/1
否則只能通過url傳參匹配xxx.com/home/index?myparam=1
如:如果定義的Action如下
public class HomeController :Controller
{
public ActionResult Index(string str)
{
return View();
}
}
輸入xxx.com/home/index/1
時,會認為參數為空,即str
並沒有被賦值,但是依然會調用index
方法,只不過是認為str
為空。但是當你通過url傳參請求時xxx.com/home/index?str=hello
,是可以匹配到這個Action
,也可以給str
賦值。
在同一個Controller下是不允許有Action重載的
如:
public class HomeController :Controller
{
public ActionResult Index(int? id)
{
return View();
}
public ActionResult Index()
{
return View();
}
}
在請求時提示錯誤:在對控制器類型“HomeController”的操作Index的請求方法不明確。
路由順序和優先級
路由引擎在定位路由時,會遍歷路由集合中的所有路由。只要發現了一個匹配的路由,會立即停止搜索。所以定義路由一定要注意路由的先后循序。一般是越是精確的放在前面。
如:有一個如下的路由配置
routes.MapRoute{
name: "one",
url:"{site}",
defaults:new{controller="MyControllerOne",action="Index"}
}
routes.MapRoute{
name:"two",
url:"Admin",
defaults:new {controller="Admin",action="Index"}
}
第一個路由有一個{site}占位符。默認的控制器為MyControllerOne
。第二個路由是一個常量Admin,
默認的控制器為Admin
。這兩個都是正確的路由配置。但是當我們輸入urlxxx.com/admin
時,我們預想的是請求AdminController
下的Index
操作方法。但是根據上面的路由映射,該url會匹配第一個路由,然后就停止了路由查找。此時觸發的Controller
為MyControllerOne
。
路由約束
之前的路由配置,都沒有url的參數的類型信息。如果我們的Action是一個Int類型,但是url中的參數是個字符串,這樣就會導致錯誤。所以如果有url的類型約束可以規避這個錯誤的發生。
在Asp.Net MVC中我們可以通過正則表達式來約束路由。
如:
routes.MapRoute{
"Default",
"{controller}/{action}/{id}",
new{controller="Home",action="Index",id=UrlParameter.Optional},
new{id="\d+"}//該id為整數
}
除了使用正則表達式來約束路由,我們還可以通過繼承IRouteConstraint接口自定義約束規則
如:
public class MyRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
//獲取id的值
var id = values[parameterName];
//id驗證方法
return true;
}
}
更新路由配置
routes.MapRoute{
"Default",
"{controller}/{action}/{id}",
new{controller="Home",action="Index",id=UrlParameter.Optional},
new{id=new MyRouteConstraint()}
}
That's it
參考資料:
如有不對,請多多指教。