MVC的路由規則配置方式比較多,我們們用得最多的是兩種:
A、全域性規則。就是我們熟悉的”{controller}/{action}“。
app.MapControllerRoute( name: "bug", pattern: "{controller}/{action}" ); app.MapControllerRoute( name: "八阿哥", pattern: "app/{action}", defaults: new { controller = "Home" } );
其中,controller、action、area、page 這些欄位名用於專屬匹配。比如 controller 匹配控制器名稱等。這個老周不必多說了,大夥伴們都知道。大括號({ })括起來的欄位是全域性路由。這些路由可以用於當前應用中所有未指定特性化路由的控制器。上面程式碼中第二條路由,由於URL模板缺少了 controller 欄位,所以 defaults 引數要設定它呼叫的控制器是 Home。
B、特性化路由(區域性路由)。此規則透過 [Route]、[HttpGet]、[HttpPost] 等特性類,在控制器類或方法上配置的路由規則。
[Route("abc")] public class PigController:ControllerBase { [Route("xyz")] public IActionResult Greeting() { return Content("來自豬的問候"); } }
這樣的規則會進行合併。即控制器上的是”abc“,方法上是”xyz“,所以你要呼叫Greeting方法就要訪問URL:
http://www.xxx.com/abc/xyz
如果控制器上沒有 [Route],只有方法上有。
public class PigController:ControllerBase { [Route("haha/hehe")] public IActionResult Greeting() { return Content("來自豬的問候"); } }
這時候,要想訪問 Greeting 方法,其URL變為:http://www.aaa.cc/haha/hehe
【總結】其實這個基於特性的路由規則是有規律的——合併模板原則。具體說就是:
1、如果控制器上有指定,就將控制器上的路由與各個方法上的路由合併;
2、如果控制器上未指定路由,那就用方法上的路由。
說白了,就是從外向內,層層合併。
以上所說的都是大家熟悉的路由玩法,下面老周要說的這種玩法比較複雜,一般不用。
那什麼情況下用?
1、你覺得個個控制器去加 [Route]、[HttpPost] 等太麻煩,想來個痛快的;
2、你想弄個字首,但這個字首可能不是固定的。比如,加個名稱空間做字首,像 http://www.yyy.cn/MyNamespace/MyController/MyAction/Other。這個名稱空間的名稱要透過程式設計,在程式執行的時候獲取,而不是硬編碼。
這樣的話,就可以用到應用程式模型——其實我們這一系列文章都離不開應用程式模型,因為整個MVC應用程式的自定義方式都與其有關。
所以這種方案也是透過實現自定義的約定介面來完成的,其中主要是用到 AttributeRouteModel 類。它的功能與直接用在控制器或方法上的 [Route] 特性差不多,只不過這個類能讓我們透過程式設計的方式設定路由URL。也就是 Template 屬性,它是一個字串,跟 [Route] 中設定的URL一樣的用途,比如
[Route("blogs/[controller]/[action]")] public class KillerController : Controller ...
就相當於 AttributeRouteModel.Template = "blogs/[controller]/[action]"。在特性化的路由規則上,controller、action 這些欄位都寫在中括號裡面。
下面老周就給大夥演示一下,主要實現:
1、以當前程式集的名稱為URL字首;
2、字首後接控制器名稱;
3、控制器名後面接操作方法名稱。
假設當前程式集名為 MyHub,控制器名為 Home,操作方法為 Goodbye,那麼,呼叫 Goodbye 方法的URL是:https://mycool.net/myhub/home/goodbye。
這個都是應用程式在執行後自動設定的,要是程式集改名為 MyGooood,那麼URL字首就自動變為 /mygooood。
從以上分析看,此約定要改控制器的路由,也要改操作方法的路由,所以,實現的約定介面應為 IControllerModelConvention。下面是程式碼:
public class CustControllerConvension : IControllerModelConvention { public void Apply(ControllerModel controller) { // 如果已存在可用的 Attribute Route,就跳過 if (controller.Selectors.Any(s => s.AttributeRouteModel != null)) { return; } // 程式集名稱 string assName = controller.ControllerType.Assembly.GetName().Name ?? ""; // 除掉名稱中的“.” assName = assName.Replace(".", "/"); // 控制器名稱 string ctrlName = controller.ControllerName; // 至少要有一個Selector if (controller.Selectors.Count == 0) { controller.Selectors.Add(new()); } // 先設定Controller上的路由 foreach (var selector in controller.Selectors) { // Assembly name + controller name selector.AttributeRouteModel = new() { Template = AttributeRouteModel.CombineTemplates(assName, ctrlName) }; } // 再設定Action上的路由 foreach (var action in controller.Actions) { if (action.Selectors.Any(s => s.AttributeRouteModel != null)) { // 如果已有Attribute route,就跳過 continue; } // 至少得有一個Selector if (action.Selectors.Count == 0) { action.Selectors.Add(new SelectorModel()); } foreach (var selector in action.Selectors) { // Action的名字作為URL的一部分 selector.AttributeRouteModel = new() { Template = action.ActionName }; } } } }
不管是控制器的還是操作方法的,都允許設定多個SelectorModel物件。這就類似我們在控制器上可以設定多個 [Route]。程式碼在處理之前都先判斷一下是不是有任何 Selector 的 AttributeRouteModel 屬性不為 null,這是為了讓自定義的約定與 [Route]、[HttpGet] 等特性類不衝突。我的意思是如果你在控制器或操作方法上用了 [Route] 特性,那麼這裡就跳過,不要再修改它。
if (controller.Selectors.Any(s => s.AttributeRouteModel != null)) { return; } if (action.Selectors.Any(s => s.AttributeRouteModel != null)) { continue; }
CombineTemplates 是靜態方法,它可以幫我們自動拼接URL,只要你把兩段URL傳遞給它就行了。
所以,上述約定類的規則就是:Assembly Name + Controller Name + Action Name。
約定完了後,還要在初始化MVC功能(註冊服務)時設定一下。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers().AddMvcOptions(opt=> { opt.Conventions.Add(new CustControllerConvension()); }); var app = builder.Build();
注意啊,這樣設定後,約定是作用於全域性的,應用程式內的控制器都會應用。你如果只想區域性用,那就定義了特性類(從Attribute類派生),實現原理一樣的。你可以參考老周在上上篇中舉到的自定義控制器名稱的例子。
應用程式在對映終結點時就不用設定路由了。
app.MapControllers();
app.Run();
現在,我們定義些控制器類測試一下。
public class 大螃蟹Controller : ControllerBase { public IActionResult Greeting() => Content("來自螃蟹精的問候"); }
這裡假設程式集的名稱是 FlyApp。你應該知道怎麼訪問了。看圖。
不過癮的話,可以再寫一個控制器類。
public class HomeController : Controller { public IActionResult Index() { return Content("來自高達的問候"); } public IActionResult Hello() { return Content("來自西海龍王的問候"); } }
繼續測試,看圖。
這裡補充一下,前面我們不是定義了這麼個控制器嗎?
public class PigController:ControllerBase { [Route("haha/hehe")] public IActionResult Greeting() { return Content("來自豬的問候"); } }
現在,如果套用了我們剛寫的 CustControllerConvension 約定後,兩個功能合在一塊兒了,那這個控制器該怎麼訪問呢。我們們的約定在實現時是如果已設定了特性路由就跳過,只有沒設定過的才會處理。來,我們分析一下。在這個 Pig 控制器中,控制器上沒有應用 [Route] 特性,所以 Selector 裡面的 AttributeRouteModel 是 null。所以,會為控制器設定程式集名稱字首 + 控制器名,即 FlyApp/Pig。
接著,它的 Greeting 方法是有 [Route] 特性的,根據我們們的程式碼邏輯,是保留已有的路由的,所以,”haha/hehe“被保留。
然後 Pig 控制器上的和 Greeting 方法上的路由一合併,就是 /flyapp/pig/haha/hehe。看圖。
現在,你明白是咋回事了吧。
------------------------------------------------------------------------------
可能有大夥伴會說:老周,你這樣弄有意思嗎?
老周答曰:沒意思,圖增意趣耳!
老周再曰:其實啊,這個也不是完全沒用的。老周前文說過的,如果你的URL中有某部分是要透過程式碼來獲取,而不是硬編碼的話,那這種折騰就有用了。總之,一句話:技巧老周都告訴你了,至於怎麼去運用,看實際需要唄。