65.9K
CodeProject 正在变化。 阅读更多。
Home

Convention Routing VS Attribute Routing

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (9投票s)

2018年5月31日

CPOL

14分钟阅读

viewsIcon

24324

本文将详细介绍基于约定的路由和属性路由。欢迎提出您的反馈。

引言

路由是ASP.NET MVC管道中的第一个也是最重要的现象。在这里,您将学习路由、其工作原理及其变体。

背景

上周,我的一位朋友问了这个问题:“我们能创建带有某些约束条件的自定义路由吗?什么是属性路由?”我将本文献给他。希望他会喜欢。

将要涵盖的主题是

  1. 路由及其工作原理
    1. 路由的组成部分
    2. 默认路由和自定义路由
    3. IgnoreRoute()方法的目的是什么?
    4. 如何为自定义路由应用约束?
  2. 什么是属性路由?
    1. 默认参数和可选参数
    2. 默认操作和RoutePrefix
    3. Name和Order属性
    4. 属性路由(包括约束)

路由

路由是MVC管道中的第一步,它是URL中具体、物理文件的替代。换句话说,路由是一种现象,其中控制器和操作执行,而不是具体的物理文件。

为什么我们需要路由?

传统上,浏览器的URL代表物理文件。文件可以是HTML文件、ASPX页面等。让我们来理解这种传统方式

www.abcd.com/images/john.aspx?id=2

此URL显示首先转到“images”文件夹,然后访问id为2的“john.aspx”文件。因此,将您的宝贵信息暴露给浏览器URL并不是一个好方法,因为通过这些URL,站点可能被黑客攻击。因此,路由开始发挥作用,解决了这种传统基于文件系统的问。路由的实现从路由表开始。路由表是所有可能的、正确的路由的集合,这些路由可用于映射HTTP请求URL。让我们详细了解RouteTable和路由的工作原理。

路由是一种系统,通过使用MapRoute方法,将URL模式与RouteTable中定义的URL模式进行匹配。当应用程序启动时,路由过程开始发挥作用,首先将路由注册到RouteTable。在RouteTable中注册路由至关重要,因为这是路由引擎如何处理请求URL的唯一依据。路由在应用程序的App_Start文件中的RouteConfig类中注册。让我们看一下

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

Global.asax文件的样子是

protected void Application_Start()
{
    //Some other code is removed for clarity.
    RouteConfig.RegisterRoutes(RouteTable.Routes);
}

该图说明了路由的工作原理,并展示了路由引擎如何处理URL请求并向浏览器提供响应。

路由引擎是负责处理HTTP请求的模块。当HTTP请求到来时,UrlRoutingModule开始在RouteTable中匹配最完美的URL模式,一旦成功找到,路由引擎会将请求转发给RouteHandler。在RouteHandler中,其接口IRouteHandler发挥作用,并调用其GetHttpHandler方法。该方法如下所示

public interface IRouteHandler
{
    IHttpHandler GetHttpHandler(RequestContext requestContext);
}

当上述所有过程成功完成后,将调用ProcessRequest()方法,如上图所示;否则,如果请求的URL与RoutTable中的任何模式都不匹配,用户将被重定向到HTTP 404错误页面。

路由的组成部分

当您要注册路由时,必须在RouteConfig类中使用重载的MapRoute()方法。MapRoute()方法有6个重载版本,最后一个包含所有参数的方法将在下面解释

public static class RouteCollectionExtensions
{
    public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
}

上面的MapRoute()方法包含以下参数

  • 路由的名称
  • 路由的URL模式
  • 路由的默认值
  • 路由的约束
  • 路由的命名空间

让我们一一了解!

路由名称

首先,我想说,社区中关于路由名称的观点非常模糊。因此,我强烈建议仔细阅读本节,并请留下您的评论,因为我是在自学。所以,如果有错误,请理性地纠正我。

RouteTable中注册的路由必须有一个唯一的名称,以便与其他路由区分开来。此名称引用RouteTable中的特定URL模式。最重要的是,此名称仅用于URL生成。因此,我得出结论,路由不是基于名称发生的,而是基于其URL模式发生的。事实上,URL模式告诉UrlRoutingModule如何处理请求,而不是路由的名称。那么问题来了,为什么它必须是唯一的呢?它之所以应该是唯一的,是因为它是创建URL生成唯一性的东西。让我们实际理解它。

  • 创建一个包含两个操作方法的控制器。
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
    public ActionResult Student()
    {
        return View();
    }
}
  • 创建用于理解路由名称的路由。
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    // Specific Route
    routes.MapRoute(
        name: "SecondRoute",
        url: "Home/Student",
        defaults: new { controller = "Home", action = "Student" }
    );
    //Generic Route
    routes.MapRoute(
        name: "FirstRoute",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
}
  • 现在我们需要创建视图。

Student.cshtml

@{
    ViewBag.Title = "Second Route";
}
<h2>Second Route</h2>
This is our 2nd Route.

Index.cshtml

@{
    ViewBag.Title = "Second Route";
}
<h2>First Route</h2>
<a href="@Url.RouteUrl("SecondRoute")">This URL will direct you to Second Route.</a>
  • 现在,当我们执行应用程序时,默认的通用路由将开始生效,如图所示

  • 当我们点击URL时,它将重定向到第二个路由的视图,如图所示

正如您所看到的,我们在锚标签中传递了路由名称“Second Route”,当我们点击它时,它将重定向到名称为该路由的路由。

因此,结论是,路由名称用于URL生成。现在的问题是,为什么路由名称必须是唯一的?所以,如果路由名称不唯一,UrlRoutingModule将如何知道Second Route,它将抛出一个运行时错误,如下所示

路由的URL模式:路由的URL模式由文字值和变量占位符组成。这些变量也称为URL参数。所有这些文字和占位符都由斜杠(/)分隔,称为段(Segments)。

文字(Literal)表示“固定”值,变量占位符(variable placeholder)表示“值的替换”。在URL模式中,您可以使用大括号{}来定义占位符。您可以通过文字在段中定义多个占位符。例如,请看下图!

因此,如果您想在一个段中添加多个占位符,您必须在这些占位符之间使用一个文字。

路由的默认值

当您定义一个路由时,您可以为URL的每个段分配默认值。当URL中没有提供参数时,这些默认值将生效。您可以通过创建RouteValueDictionary类的匿名对象来设置路由的默认值。

路由的约束:

简单来说,约束是对路由参数的限制。当您为路由设置任何约束时,如果URL参数不满足约束要求,则该路由将不起作用。然后请求将转到具有默认参数的路由。您添加约束到路由是为了确保它能根据您的应用程序需求工作。您将在本主题中看到更多详细信息。

路由中的命名空间

您可以为Web应用程序设置自己的命名空间。可以通过创建RouteValueDictionary类的对象将命名空间添加到路由中。当您想将URL重定向到具有特定命名空间的特定控制器时,将使用此参数。

默认路由和自定义路由

默认路由

MVC中的默认路由是一个通用路由,它以“defaults”的名称保存在RouteTable中。有趣的是,默认路由只有三个由斜杠分隔的段

{controller}/{action}/{id}

图示了一个路由将要注册到RouteCollectionroutes.RouteTable)中,它有一个唯一的名称“Defaults”,一个URL模式,其中controller、action、id都是占位符,然后有一个defaults属性,负责在请求URL中不包含controller和action时自动初始化它们。

自定义路由

让我们创建一个自定义路由来理解路由。我们有一个HomeContoller和一个Index操作方法。让我们创建另一个名为Student的操作方法

public class HomeController : Controller
{
    public string Index()
    {
        return "This is Index action method";
    }
    public string Student(int rollno)
    {
        return $"Roll Number is {rollno}”;
        }
}

现在我们需要在RoutConfig类中创建一个自定义路由

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
             name: "FirstRoute",
             url: "Home/{action}/{rollno}",
             defaults: new { controller = "Home", action = "Student", rollno = UrlParameter.Optional }
             );
    routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
}

在执行应用程序之前,我想告诉您关于路由一个非常重要的事情,那就是:“始终从具体到通用定义您的路由”。在上面的代码中,默认路由是最通用的,所以它应该在RouteTable的最后。而我们最具体的路由是我们之前定义的第一个路由。路由流程如下

现在,当您执行应用程序并输入如下URL时,输出如下

表格清晰地展示了URL模式及其输出。第二个URL中的错误是由于URL中缺少参数值。另一方面,当您在第三个URL中传递参数值时,该路由将生效并显示输出。这种在URL中传递数据的方式称为查询字符串方法。

查询字符串在其URL中有一个问号(?)。但这种传递数据的方式并不是一个好方法,因为通过这种方法,您的数据可能会暴露给黑客或渗透者。比查询字符串更好的传递数据方式是这样的

https://Server/Home/Student/12345

IgnoreRoute()方法的目的是什么?

在上面的RouteConfig类代码中,您可以看到IgnoreRoute()方法,如下所示

如您所知,URL模式{controller}/{action}/{id}默认由UrlRoutingModule处理。但有趣的是,如果您真的希望RoutingModule处理上述模式,则必须将RouteCollectionRouteExistingFiles属性设置为true。

public class RouteCollection : Collection<RouteBase>
{
    //other code is removed for clarity
    public bool RouteExistingFiles { get; set; }
}

您可以在上面的代码中看到,RouteExistingFiles属性的数据类型是bool,所以它可以是true或false。当它为true时,RoutingModule将处理所有与RouteConfig类中定义的URL模式匹配的请求。否则,当此属性设置为false时,UrlRoutingModule将永远不会处理任何请求。

现在,问题是,当您将RouteExistingFiles属性设置为true时,路由模块会处理所有请求,包括您的Web资源文件。所以,当您想阻止浏览器访问某些路由时,您该如何做?

这可以通过使用IgnoreRoute()方法来实现。IgnoreRoute方法将阻止对IgnoreRoute参数中定义的路由的访问。方法如下

public static void IgnoreRoute(string url);

这将从RouteTable中的可用路由列表中忽略指定的URL路由。

那么,在RouteConfig类中,以下代码是什么意思?

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

上面的IgnoreRoute()代码表明resource.axd文件被禁止访问。最后一点是,这些.axd文件不在我们的项目中,它们是为HTTP处理保留的。

如何为自定义路由应用约束?

约束是对路由参数的限制。当您为路由设置任何约束时,如果URL包含不满足约束要求的参数值,则该路由将不起作用。

让我们以实际方式来理解它,假设您想将学生的卷号限制为只有5位数,那么您该如何做?

这可以通过约束来实现。让我们来试试。

routes.MapRoute(
                name: "FirstRoute",
                url: "Home/{action}/{rollno}",
                defaults: new { controller = "Home", action = "Student", rollno = UrlParameter.Optional },
                constraints: new { rollno = @"\d{5}" }
                );

如上所示,卷号数字必须是5。当我们执行以下URL时

https://Home/Student/12345

输出:卷号是12345

“\d{5}”有一个@符号作为前缀,这被称为逐字字面量(verbatim literal)。它的反面且糟糕的方式是使用转义序列(Escape sequences)。转义序列及其用法定义在下面的图中

让我们了解两者的区别

使用转义序列:“C:\\Computer\\Lectures\\ASP.NET”

使用逐字字面量:@“C:\Computer\Lectures\ASP.NET”

正如我们所见,逐字字面量提供了更好的可读性。因此,通过这种方式,您可以为自定义路由创建自己的约束。

什么是属性路由?

属性路由是ASP.NET MVC 5中的一种新型路由。根据名称属性路由,人们可以认为这种方法将使用属性来定义路由。是的!让我们详细了解。

为什么我们需要属性路由?

首先,看看我们之前的自定义路由

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
             name: "FirstRoute",
             url: "Home/{action}/{rollno}",
             defaults: new { controller = "Home", action = "Student", rollno = UrlParameter.Optional }
             );
    routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
}

我们需要属性路由是因为

  1. 在上面的代码中,只有两个路由是使用MapRoute定义的。但是当我们的应用程序足够大,有超过两个路由时,单独创建每个路由将变得非常复杂。对于每个路由,您需要写5行代码,这会占用您的时间并干扰您的开发时间。
  2. 您需要同时关注RouteConfig类中的URL模式以及处理请求的Controller。因此,在这些文件夹之间来回移动会给您带来复杂性。
  3. 这会减少出错的可能性,因为在RouteConfig类中,创建新自定义路由时可能会出现任何错误。
  4. 它易于使用,并有助于将同一个操作方法映射到两个路由。
  5. 您不必关心路由流程,即从最具体到最通用。所有这些都在您在操作方法上方使用的属性。

现在,让我们开始使用属性路由。

首先,您必须启用属性路由的访问。因此,RouteConfig类将变成

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        // below line of code will enable the attribute routing.
        routes.MapMvcAttributeRoutes();
    }
}

现在,让我们进入控制器,看看它的工作原理。[Route]属性用于操作方法的顶部,如下所示

public class HomeController : Controller
{
    [Route("Home/Student/{rollno}")]
    public string Student(int rollno)
    {
        return $"Name is {rollno}";
    }
}

输出是

如何在属性路由中创建可选参数并设置参数的默认值?

如果您想在属性路由中使参数可选,只需在参数末尾加上问号即可,如下所示

public class HomeController : Controller
{
    [Route("Home/Student/{rollno ?}")]
    public string Student(int rollno)
    {
        return $"Name is {rollno}";
    }
}

如果您想为任何参数设置默认值,您必须使用此模式

ParameterName = Value,如下所示

public class HomeController : Controller
{
    [Route("Home/Student/{rollno = 23}")]
    public string Student(int rollno)
    {
        return $"Name is {rollno}";
    }
}

什么是RoutePrefix?

您可能会看到很多路由从开头部分相同,意味着它们的prefix是相同的。例如

Home/Student

Home/Teacher

以上两个URL都有相同的prefix,即Home。

所以,我们使用RoutePrefix属性而不是一遍又一遍地输入相同的prefix。此属性设置在控制器级别。如下所示

[RoutePrefix("Home")]
public class HomeController : Controller
{
    [Route("Student/{rollno= 23}")]
    public string Student(int rollno)
    {
        return $"Roll Number {rollno}";
    }
    [Route("Teacher")]
    public string Teacher()
    {
        return "Teacher’s method";
    }
}

在上面的代码中,RoutePrefix设置在控制器级别,在操作方法中,我们不必反复使用Home prefix。输出如下

RoutePrefix控制着整个控制器,所有操作方法都绑定使用该prefix。但是,当您想执行一个不应将prefix作为控制器prefix的操作时,该怎么办?

因此,当您想覆盖路由prefix时,请使用~符号,如下所示

[RoutePrefix("Home")]
public class HomeController : Controller
{
    [Route("Student/{rollno= 23}")]
    public string Student(int rollno)
    {
        return $"Roll Number is {rollno}";
    }
    [Route("~/Home2/Teacher")]
    public string Teacher()
    {
        return "Teacher’s method";
    }
}

正如您在基于约定的路由中看到的,我们可以设置默认操作方法,现在问题来了,

我们可以在属性路由中设置默认操作方法吗?

是的!您可以通过在控制器顶部使用[Route]属性来实现。请看下面的代码

[RoutePrefix("Home")
[Route("{action = Teacher}")]
public class HomeController : Controller
{
    public string Teacher()
    {
        return "Teacher’s method";
    }
}

控制器顶部的第二个Route属性为操作方法设置了默认值。

正如您在路由部分所看到的,我们可以为路由设置名称。所以,

我们可以在属性路由中为特定URL设置名称吗?

是的!在这里,名称也仅用于URL生成。我们可以这样设置名称

语法

[Route("URLPath", Name = "RouteName")]
[Route("Home/Student", Name = "StudentRoute")]
public ActionResult Student()
{
    return View();
}

上面的代码显示了如何在属性路由中设置路由名称。在阅读了基于约定的路由的路由名称部分后,您可以在此Route属性中重复URL生成的过程。

正如您在路由部分所看到的,我们应该从更具体到更通用的流程创建我们的路由。那么,如何在属性路由中做到这一点?

我们可以在属性路由中设置路由的优先级吗?

是的!您需要使用路由属性中的Order参数。假设我们有一个控制器,其中有两个操作方法,分别命名为First和Second。它们分别设置了Order参数为2和1。

为操作方法设置顺序后,操作方法将按照FCFS(先来先服务)执行,其值可以是从-2,147,483,648到2,147,483,647(int的大小)。

public class HomeController : Controller
{
    [Route(Order = 2)]
    public string First()
    {
        return "This is First action method having order 2";
    }
    [Route(Order = 1)]
    public string Second()
    {
        return "This is Second action method having order 1";
    }
}

 当我们运行此代码时,Second操作方法将执行,因为它具有更高的顺序。

但是,如果在Route属性中未指定顺序,则操作方法将按照路由在RouteTable中注册的顺序执行。

正如您在基于约定的路由部分所看到的,我们可以为它设置约束。

我们可以在属性路由中设置约束吗?如果可以,如何设置?

是的!您可以在属性路由中设置约束。语法如下

[Route(URLPath/{parameterName: constraint})]

首先,让我们看看可以在属性路由中使用的不同约束。

让我们看一个实际的例子。

public class HomeController : Controller
{
    [Route("Home/Student/{rollno:maxlength(2)}")]
    public string Student(int rollno)
    {
        return $"Roll Number's length is {rollno}";
    }
}

当我们传入URL时

https://:50656/Home/Student/12

输出如下

当我们传入卷号长度为3时,如下所示

https://:50656/Home/Student/123

输出如下

我们也可以同时使用多个约束

[Route("Home/Student/{rollno:maxlength(2):range(9,10)}")]
public string Student(int rollno)
{
    return $"Roll Number's length is {rollno}";
}

因此,这些是使用属性路由对我们的路由的限制。

结论

这些是关于路由和属性路由的首要内容,我希望本文能帮助您理解所有概念。如果您有任何疑问,请随时在评论中与我联系。同时,请提供反馈,无论是正面的还是负面的,它将帮助我写出最好的文章,并增加我分享知识的热情。

© . All rights reserved.