3.4.2 路由

对于Web应用程序,路由是一个非常重要且基本的功能,它的主要功能是根据预先配置的路由信息对客户端传来的请求进行路由映射,映射完成后,再将请求传给对应的路由处理器处理。具体来说,在ASP.NET Core MVC中,路由负责从请求的URL中获取信息,并根据这些信息来定位或映射到对应的Controller与Action。

ASP.NET Core提供了创建路由及路由处理器的接口,要创建路由,首先要先添加与路由相关的服务,然后配置路由中间件。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    var trackPackageRouteHandler = new RouteHandler(context =>
    {
        var routeValues = context.GetRouteData().Values;
        return context.Response.WriteAsync(
            $"Hello! Route values: {string.Join(", ", routeValues)}");
    });
    var routeBuilder = new RouteBuilder(app, trackPackageRouteHandler);
    routeBuilder.MapRoute("Track Package Route", "package/{operation}/{id:int}");
    routeBuilder.MapGet("hello/{name}", context =>
    {
        var name = context.GetRouteValue("name");
        return context.Response.WriteAsync($"Hi, {name}!");
    });
    var routes = routeBuilder.Build();
    app.UseRouter(routes);
}

在上述代码的Configure方法中,首先创建了一个RouteHandler,即路由处理器,它会从请求的URL中获取路由信息,并将其输出;接着,创建了一个RouteBuilder,并使用它的MapRoute方法来添加路由信息,这些信息包括路由名称以及要匹配的URL模板,在上面的示例中,URL模板的值为package/{operation}/{id:int}。除了调用MapRoute外,后面还可以使用MapGet方法添加仅匹配GET方法的请求,最后调用IApplicationBuilder的UseRouter扩展方法来添加路由中间件。

表3-1列出了程序运行后每个请求的URL会得到的响应结果。

表3-1 请求与响应结果

以上是在ASP.NET Core中底层路由的创建方式。然而,通常情况下并不需要这么做,这种方式比较复杂,更主要的原因则是当使用MVC后,就只需将对应的URL路由到Controller与Action,这简化了路由规则,并且MVC中间件也封装了相关的逻辑,使基于MVC的路由更易于配置。

对于ASP.NET Core MVC,定义路由的方法有以下两种。

基于约定的路由:基于约定的路由会根据一些约定来创建路由,它要在应用程序的Startup类中来定义,事实上,上面的示例就是基于约定的路由。

特性路由:使用C#特性对Controller和Action指定其路由信息。

要使用基本约定的路由,首先定义一个或若干个路由约定,同时,只要保证所有定义的路由约定能够尽可能地满足不同形式的映射即可。前文曾提到,这些约定需要在Startup类中指明,具体来说,应在配置MVC中间件时来设置路由约定。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    …
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            template: "{controller}/{action}");
    });
}

在上述代码中,使用字符串设置了一个路由约定。所谓的路由约定,本质上是一个URL模板信息。其中,在大括号{}中的部分是路由参数,每一个参数都有一个名称,它们充当了占位符的作用,参数与参数之间以“/”分隔。对于路由参数名,controller与action是ASP.NET Core MVC特定的,它们分别用于匹配到Controller与Action。注意,任何一个MVC应用程序应该至少定义一个路由。

当创建了上面的路由,以下的URL都会满足这个约定。

http://localhost:5001/home/index

http://localhost:5001/account/register

它们分别会映射到HomeController的Index方法以及AccountController的Register方法。

在通常情况下,对于MVC应用程序,会设置如下的路由约定。

routes.MapRoute(
    "default", "{controller=Home}/{action=Index}/{id?}");

在这个约定里,为controller与action设置了默认值,分别为Home和Index,因此以下URL都满足这个约定。

http://localhost:5001:会映射到HomeContoller的Index方法。

http://localhost:5001/blog:会映射到BlogController的Index方法。

除了在URL模板中来指定默认值以外,还可以使用MapRoute方法的其他重载形式来指定默认值,因此上述代码也可写成如下这种形式:

routes.MapRoute(
    template: "{controller}/{action}/{id?}"),
    defaults: new { controller = "Home", action = "Index" }

对于模板中的最后一个参数id,则会向action所映射方法的同名参数传值,因此对于如下HomeController。

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return Ok("Hello");
    }
    public IActionResult Welcome(int id)
    {
        return Ok("Hello, Your Id: " + id);
    }
}

当请求URL为https://localhost:5001/Home/Welcome/1时,URL中的1将会传给Welcome方法的id参数;而在模板中,参数id后面有一个问号的标记,说明这个参数是可选的,因此,在URL中有无此项都可以。当URL中不包括相应参数的值时,那么在执行Action时,对应的参数将会使用该参数类型的默认值。注意,一个URL模板中只能有一个可选参数,并且只能放在最后。

在指定参数的同时,也可以为参数添加一些约束或限制。例如,如果希望上面的参数id的值为一个整型的数字,则应该这样定义路由:

routes.MapRoute(
     template: "{controller}/{action}/{id:int}");

{id:int}指明了URL中的这个参数必须为一个整型数字,否则URL不会映射到这个路由。除了int类型,还可以指定下列类型:bool、datetime、decimal、long、float和guid等。除了类型方面的限制外,使用length(min,max)、maxlength(value)和minlength(value)可以限制参数值的长度,使用range(min,max)可以限制参数值的范围;此外,还可以指定正则表达式,如{para:regex(^\d{15}|\d{18}$)}。

一个应用程序可以定义多个路由,当它收到客户端的请求后,将会对每个定义的路由信息进行匹配,直到找到对应的路由。

另一种实现路由的方法是使用特性路由,即RouteAttribute。它能够为每个Controller,甚至每个Action显式地设置路由信息,只要在Controller类或Action方法上添加[Route]特性即可,因此它要更为灵活。

public class HomeController: Controller
{
    [Route("")]
    [Route("Home/Index")]        
    [Route("AnotherOne")]
    public IActionResult Index()
    {
        return Ok("Hello from Index method of Home Controller");    
    }  
}

上例中,使用[Route]特性为HomeController的Index方法添加了3个路由,因此能使如下URL都能映射到这个Action上。

http://locahost:5001

http://localhost:5001/Home/Index

http://localhost:5001/AnotherOne

除了在Action上使用[Route]特性,也可以在Controller上添加,当一个Contoller中包括多个Action时,这会非常方便,因为在为Action配置路由特性时,就不需再指明其Controller了,如下例所示。

[Route("Home")]
public class HomeController : Controller
{
    [Route("")]
    [Route("Index")]
    public IActionResult Index()
    {
        …
    }
    [Route("Welcome")]
    public IActionResult Welcome()
    {
        …
    }
}

目前为止,我们在特性路由中都指定了固定的值,当重构代码时,例如Controller的类名或者Action的方法名改变了,它们相应的路由特性里的值也需要改变,否则,Controller类名或Action方法名与其相应的路由名称不一致,可能会引起混淆。对于这一问题,解决办法是使用[controller]与[action]来分别代替固定的值,它们分别表示当前的Controller与Action,可参考如下代码。

[Route("[controller]")]
public class HomeController : Controller
{
    [Route("")]
    [Route("[action]")]
    public IActionResult Index()
    {
        …
    }
    [Route("[action]")]
    public IActionResult Welcome()
    {
        …
    }
}

需要注意的是,尽管这样能够使Controller与Action的名称更为灵活,但对于Web API应用程序而言,URL作为接口应该尽量避免变动,因此仍建议写成固定值。

如果要为Action方法传递参数,与基于约定的路由一样,只要在[Route]特性中指定即可,具体如下。

[Route("[action]/{name?}")]
public IActionResult Welcome(string name)
{
    …
}

为Action设置路由时,除了使用[Route]特性外,更常见的是使用HTTP特性,特别是在开发Web API应用程序时,这些特性如表3-2所示。

表3-2 HTTP特性

由于每一个特性对应一个HTTP方法,因此如果要获取一个资源,可以这样使用:

[Route("api/[controller]")]
public class BlogsController : Controller
{
    [HttpGet("{id}")]
    public IActionResult Get(string id)
    {
        …
    }
}

[HttpGet]特性不仅指定了参数,并且它使它当前Action仅支持GET请求。另外,对于Web API应用程序,建议所有的Controller路由模板字符串以api开始,这样做的目的是明确指明它是一个API。

最后,需要说明的是,基本约定的路由与特性路由方式可以同时存在,但是如果已经为一个Action指定了特性路由,那么基本约定的路由在该Action上就不会起作用了。