【ASP.NET Core】MVC控制器的各種自定義:特性化的路由規則

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中有某部分是要通過代碼來獲取,而不是硬編碼的話,那這種折騰就有用了。總之,一句話:技巧老周都告訴你了,至於怎麼去運用,看實際需要唄。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章