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

2022-12-01 21:01:25

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