上一篇文章《ASP.NET Core中使用默認MVC路由》提到了如何使用默認的MVC路由配置,通過這個配置,我們就可以把請求路由到Controller和Action,通常情況下我們使用默認的路由器就可以了。
但是有些情況下,我們需要創建自己的路由規則,不是簡單的修改MVC路由模板這么簡單,比如我們需要針對一些特定的URL做特殊處理,這種情況通常是我們需要兼容一些舊的URL,但是升級之后總不能不管吧,要們做跳轉或者給用戶一個友好提示等等。如果舊的URL是MVC的那種格式Controller/Action格式還好辦,如果是webform格式的URL,比如xxx.aspx或者靜態文件URL,比如bbb.html。這時候我們不能使用默認的MVC路由器來處理我們的請求,我們需要提供一個特定的Router,也可以為理解為路由處理器,如果請求的URL匹配得上,那么就交給一個特定的handler處理。
這里我們提到了Handler,實際上可以理解為Httphandler,沒錯就是它。在Webform里面,之前版本的MVC都一直存在,web請求最終都給交給一個特定的Handler處理。在Webform里面的handler是aspx的code behind文件,在MVC里面是Routehandler,之后各自實現自己的process方法。
好了不深入去看,我們接着上一篇的例子,創建自己的一個Router,這Router可以實現以下功能
1.這個Router的作用是兼容不存在的Url,對於這些不存在的Url,我們給出友好提示
2.我們還可以將某些特定URL的請求轉交給MVC框架去處理,這里說的是轉交,而不是直接讓MVC路由去處理。
實現過程
1.在項目根目錄下創建一個類,名字為LegacyRoute,實現IRouter接口,實現代碼如下
public class LegacyRoute : IRouter { private readonly string[] _urls; public LegacyRoute(params string[] urls) { _urls = urls; } public Task RouteAsync(RouteContext context) { var requestedUrl = context.HttpContext.Request.Path.Value.TrimEnd('/'); if (_urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase)) { context.Handler = async ctx => { var response = ctx.Response; byte[] bytes = Encoding.ASCII.GetBytes($"This URL: {requestedUrl} is not available now"); await response.Body.WriteAsync(bytes, 0, bytes.Length); }; } return Task.CompletedTask; } public VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; } }
上述代碼實現了IRouter的兩個接口,這兩個接口方法的簡單介紹一下
RouteAsync:這個處理請求的關鍵方法,有請求過來的時候,並且URL能匹配的上,系統會調用這個Router進行匹配,看能否處理這個請求,如果能處理則給出響應。
比如這個例子,Router保存了一些需要兼容的舊的Url,如果請求過來的Url包含在里面,那么直接Repsonse輸出結果。
處理的方式是創建一個Handler的實例給RouteContext,這個Handler的類型是RequestDelegate,是一個委托,這個實例可以理解為一個中間件實例,參考《ASP.NET Core中Middleware的使用》里面的說明。
GetVirtualPath:這個方法是返回用戶能看到的URL路徑,比如在cshtml里面調用Url.Action得到的Url,會調用這個方法獲取對應的URL,這個方法一會再講講如何實現。
2.定義好了Router之后,然后就是應用到特定的Url,路由配置當然也是在Startup里面完成。
由於我們要把自定義的Router加入到當前的Routes集合,所以之前使用的簡化版UseMvcWithDefaultRoute需要改成如下方式
app.UseMvc(routes => { routes.Routes.Add(new LegacyRoute( "/articles/aspwinform.html", "/old/mvc3")); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
這里自定義的LegacyRoute要放在MVC的路由之前,否則會被MVC的路由提前攔截。
LegacyRoute的構造方法里提供了兩個需要兼容的舊的Url,這時候啟動項目,並且輸入這兩個URL會給出一段文字提示。
如果輸入其他不存在的路徑,那么還是返回404錯誤的
3 這時這個Router只是簡單的修改Response內容,那如果需要返回更多的信息咋辦,不能在這方寸方法里寫大堆html代碼吧,自己定義一套模板模式,
那幾乎又是重寫了一套邏輯。MVC的模板已經很強大了,那么我們完全可以把這個路由接收到請求再次轉交給MVC去處理,這樣就能用到高達上的Razor模板了。
我們先新建一個Controller,名字為LegacyController,增加一個簡單的Action
public class LegacyController : Controller { public ViewResult GetLegacyUrl(string legacyUrl) => View("GetLegacyUrl", legacyUrl); }
然后在Views文件夾創建Legacy目錄,在Legacy目錄創建GetLegacyUrl.cshtml文件,然后給一些內容文字什么的,具體不給截圖了,都很簡。
@model string @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Routing</title> </head> <body class="panel-body"> <h2>GetLegacyURL</h2> This URL: @Model is not available now </body> </html>
創建這兩個文件是為了后面操作做鋪墊。
緊接着修改LegacyRoute為如下代碼
public class LegacyRoute : IRouter { private readonly string[] _urls; private readonly IRouter _mvcRoute; public LegacyRoute(IServiceProvider services, params string[] urls) { _urls = urls; _mvcRoute = services.GetRequiredService<MvcRouteHandler>(); } public async Task RouteAsync(RouteContext context) { var requestedUrl = context.HttpContext.Request.Path.Value.TrimEnd('/'); if (_urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase)) { context.RouteData.Values["controller"] = "Legacy"; context.RouteData.Values["action"] = "GetLegacyUrl"; context.RouteData.Values["legacyUrl"] = requestedUrl; await _mvcRoute.RouteAsync(context); } } public VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; } }
主要改動是注入了IServiceProvider,這是Core里面用於查找服務的Provider,用它實現service locate功能,查找我們需要的服務組件,這是IOC的知識點,先不詳述。
引入IServiceProvider主要是為了得到MvcRouteHandler這個服務組件。
設置contexnt的RouteData的數據,然后將整個context實例交給MvcRouteHandler的實例去處理,這樣就順利將某些特定舊Url的請求轉交給了Mvc去處理。
LegacyRoute的構造方法改了,那么在Startup里面也要調整,要提供IServiceProvider的實例,還好通過IApplicationBuilder的實例就可以輕松獲得
routes.Routes.Add(new LegacyRoute( app.ApplicationServices, "/articles/aspwinform.html", "/old/mvc3"));
4 到這一步的時候,看起來已經可以處理某些特定的URL的請求。但是LegacyRoute里面還有一個方法沒有實現,那就是GetVirtualPath,這是干嘛的呢,實際上就是用於生成對外顯示的URL路徑。
使用如下代碼完善GetVirtualPath方法
public VirtualPathData GetVirtualPath(VirtualPathContext context) { if (context.Values.ContainsKey("legacyUrl")) { var url = context.Values["legacyUrl"] as string; if (_urls.Contains(url)) { return new VirtualPathData(this, url); } } return null; }
這個代碼是判斷是否在路由參數里提供了legacyUrl參數,如果有則進一步處理,並返回一個VirtualPathData實例。
說這么多可能不太好理解,實際上這個用在cshtml里面就明白了
在頁面里創建一個a標記,使用如下代碼
<a asp-route-legacyurl="/old/mvc3">/old/mvc3</a> (由於這里用到了TagHelper,所以還必須做一些處理,具體參考源代碼的配置)
那么生成的的Html代碼如下
<a href="/old/mvc3">/old/mvc3</a>
看起來比較的多余,跟直接寫死/old/mvc3路徑一樣效果,但是走的路徑不同,如果修改了路由規則,那么通過asp-route-*方式設置的路徑也會跟着修改。
到這里基本實現了自定義的路由,基本的思路就是這樣,當然能做跟多復雜的事情,甚至定義一個完整的路由規則。
完整代碼示例可以從以下路徑下載
https://github.com/shenba2014/AspDotNetCoreMvcExamples/tree/master/CustomRouter