1

【ASP.NET Core】MVC控制器的各种自定义:特性化的路由规则 - 东邪独孤

 1 year ago
source link: https://www.cnblogs.com/tcjiaan/p/16942089.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【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。你应该知道怎么访问了。看图。

367389-20221201175329528-686276020.png

不过瘾的话,可以再写一个控制器类。

 public class HomeController : Controller
 {
     public IActionResult Index()
     {
         return Content("来自高达的问候");
     }

     public IActionResult Hello()
     {
         return Content("来自西海龙王的问候");
     }
 }

继续测试,看图。

367389-20221201175536893-1618572399.png

这里补充一下,前面我们不是定义了这么个控制器吗?

 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。看图。

367389-20221201180827316-331160739.png

现在,你明白是咋回事了吧。

------------------------------------------------------------------------------

可能有大伙伴会说:老周,你这样弄有意思吗?

老周答曰:没意思,图增意趣耳!

老周再曰:其实啊,这个也不是完全没用的。老周前文说过的,如果你的URL中有某部分是要通过代码来获取,而不是硬编码的话,那这种折腾就有用了。总之,一句话:技巧老周都告诉你了,至于怎么去运用,看实际需要呗。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK