Pro ASP.NET Core MVC2(第7版)翻译

第16章:高级路由功能

作者:Adam Freeman 翻译:陈广 日期:2018-9-22


在上一章中,我向您展示了如何使用路由系统来处理传入 URL,但这只是故事的一部分。还需要能够使用您的 URL 架构来生成传出 URL,您可以将其嵌入到视图中,这样用户就可以单击链接并将表单提交回您的应用程序,从而以正确的控制器和 action 为目标。

本章我将向您展示生成传出 URL 的不同技术,如何通过替换标准的 MVC 路由实现类来定制路由系统,以及如何使用 MVC 区域 特性,它允许您将一个庞大而复杂的 MVC 应用程序分解为可管理的块。在完成本章时,我将提供一些关于 MVC 应用程序中 URL 架构的最佳实践建议。表16-1为高级路由特性简介。

表 16-1:高级路由特性简介

问题 回答
它是什么? 路由系统提供的功能不仅仅是匹配 HTTP 请求的 URL,还支持在视图中生成 URL,用自定义类替换内置路由功能,并将应用程序构造为独立的部分。
它有什么用? 每个特性都有不同的用途。能够生成 URL 并可以轻松地更改 URL 架构,而无需更新所有视图;能够使用自定义类来根据您的需要定制路由系统;能够构造应用程序以便更容易地构建复杂的项目。
它是如何使用的? 查看本章相关章节获取详细信息
它有什么缺陷和限制? 复杂应用程序的路由配置很难管理。
有没有其它选择? 没有,路由系统是 ASP.NET 的一个组成部分。

表 16-2 是本章摘要

表 16-2:本章摘要

问题 解决方案 清单
生成带有 URL 的锚元素 使用asp-actionasp-controller属性 2-5
为路由段提供值 使用带asp-route-前缀的属性 6-7
生成完全限定 URL 使用asp-procotolasp-hostasp-fragmen属性 8
选择生成 URL 的路由 使用asp-route属性 9-10
生成一个没有 HTML 元素的 URL 在视图或 action 方法中使用Url.Action助手方法 11-12
定制路由系统 Startup类中使用Configure方法 13
创建自定义路由类 实现IRouter接口 14-21
将应用程序分解为功能部分 创建区域并使用Area特性 22-28

准备示例项目

我准备继续使用上一章的 UrlsAndRoutes 项目。所需的唯一更改是在Startup类中,我用具有相同效果的显式路由替换了UseMvcWithDefaultRoute方法,如清单16-1所示。

清单 16-1:UrlsAndRoutes 文件夹下的 Startup.cs 文件,更改路由配置

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

如果启动应用程序,浏览器将请求默认 URL,该 URL 将被发送到 Home 控制器的Index action,如图16-1所示。

图16-1 运行示例应用程序

在视图中生成传出 URL

在几乎所有 MVC 应用程序中,您都希望允许用户从一个视图导航到另一个视图,这通常依赖于在第一个视图中包含一个链接,该链接的目标是生成第二个视图的 action 方法。很容易添加一个静态元素(称为锚元素),其href属性以 action 方法为目标,如下所示:

<a href="/Home/CustomVariable">This is an outgoing URL</a>

假设应用程序正在使用默认的路由配置,这个 HTML 元素将创建一个链接,该链接将针对 Home 控制器上的CustomVariable action 方法。象这样手动定义的 URL 是非常快速而简单的。它们也非常危险,当您更改应用程序的 URL 架构时,将破坏所有硬编码的 URL。然后,您必须遍历应用程序中的所有视图,并更新对控制器和 action 方法的所有引用,这是一个繁琐、容易出错和难以测试的过程。另一种更好的选择是使用路由系统生成传出 URL,这将确保 URL 架构用于动态生成 URL,并以保证反映应用程序的 URL 架构的方式进行。

生成传出链接

在视图中生成传出 URL 的最简单方法是使用 anchor 标签助手,它将为 HTML a 元素生成href属性,如清单16-2所示,清单16-2显示了我对 /Views/Shared/Result.cshtml 视图的添加。

提示:我在第23章详细解释了标签助手是如何工作的。

清单 16-2:Views/Shared 文件夹下的 Result.cshtml 文件,使用 Anchor 标签助手

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-action="CustomVariable">This is an outgoing URL</a>
</body>
</html>

asp-action属性用于指定href属性中 URL 应该指向的 action 方法的名称。您可以通过启动应用程序查看结果,如图16-2所示。

图16-2 使用标签助手生成链接

标记助手使用当前的路由配置在 a 元素上设置href属性。如果检查发送到浏览器的 HTML,您将看到它包含以下元素:

<a href="/Home/CustomVariable">This is an outgoing URL</a>

这似乎是重新创建我前面向您展示的手动定义的 URL,但是这种方法的好处是它会自动响应路由配置中的更改。为了演示,我向 Startup.cs 文件添加了一个新的路由,如清单16-3所示。

清单 16-3:UrlsAndRoutes 文件夹下的 Startup.cs 文件,添加路由

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "NewRoute",
                    template: "App/Do{action}",
                    defaults: new { controller = "Home" });

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

新路由更改了针对 Home 控制器的请求的 URL 架构。如果启动应用程序,您将看到此更改反映在ActionLink HTML 助手方法生成的 HTML 中,如下所示:

<a href="/App/DoCustomVariable">This is an outgoing URL</a>

使用标签助手生成链接解决了一个重要的维护问题。我能够更改路由架构,并使视图中的传出链接自动反映更改,而不必手动编辑应用程序中的视图。

单击该链接时,将使用传出 URL 创建传入 HTTP 请求,然后使用相同的路由来针对将处理请求的 action 方法和控制器,如图16-3所示。

图16-3 单击链接的效果是将传出 URL 转换为传入请求


理解出站 URL 路由匹配

您已经看到,更改定义 URL 架构的路由是如何改变生成传出 URL 的方式的。应用程序通常会定义几个路由,了解如何选择它们来生成 URL 是很重要的。路由系统按照定义的顺序处理路由,并依次检查每个路由是否匹配,这要求满足以下三个条件:

  • 必须为 URL 模式中定义的每个段变量提供一个值。要查找每个段变量的值,路由系统首先查找您提供的值(使用匿名类型的属性),然后查找当前请求的变量值,最后查看路由中定义的默认值。(我将在本章稍后返回这些值的第二个源)。
  • 为段变量提供的任何值都不可能与路由中定义的默认惟一变量不一致。这些变量提供了默认值但不在 URL 模式中出现。例如,在以下路由定义中,myVar是一个默认惟一变量:
routes.MapRoute("MyRoute", "{controller}/{action}",
    new { myVar = "true" });

为了使这个路由匹配,我必须注意不要为myvar提供一个值,或者确保我所提供的值与默认值相匹配。

  • 所有段变量的值必须满足路由约束。请参阅前一章中的《约束路由》一节,以获得不同类型约束的示例。

要明确的是,路由系统并不试图找到提供最佳匹配的路由,它只找到了第一个匹配点,此时它使用该路由生成 URL;任何后续路由被将被忽略。由于这个原因,你应该首先定义最具体的路由。检查传出 URL 生成非常重要。如果您试图生成一个无法找到匹配路由的 URL,将会创建一个包含空href属性的链接,如下所示:

<a href="">This is an outgoing URL</a>`

该链接将在视图中正确渲染,但当用户单击该链接时将无法正常运行。如果只生成 URL(我将在本章稍后向您展示),则结果将为null,它将渲染为视图中的空字符串。您可以通过使用命名路由来控制路由匹配。有关详细信息,请参阅本章后面的《从指定路由生成 URL》一节。


针对其它控制器

当您在元素上指定asp-action属性时,标签助手假定您希望所针的 action 在导致视图渲染的那个控制器中。要创建针对不同控制器的传出 URL,可以使用 asp-controller 属性,如清单16-4所示。

清单 16-4:Views/Shared 文件夹下的 Result.cshtml 文件,针对不同的控制器

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Admin" asp-action="Index">
        This targets another controller
    </a>
</body>
</html>

当您渲染视图,将会看到生成以下 HTML:

<a href="/Admin">This targets another controller</a>

标签助手将针对 Admin 控制器上Index action 方法的 URL 请求表示为 /Admin。路由系统知道,应用程序中定义的路由默认使用Index action 方法,允许它省略不需要的段。

在决定如何针对给定的 action 方法时,路由系统会包含使用Route特性定义的路由。在清单16-5中,asp-controller属性的目标是 Customer 控制器中的Index action,在第15章中已经应用了Route特性。

清单 16-5:Views/Shared 文件夹下的 Result.cshtml 文件,针对一个 Action

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Customer" asp-action="Index">This is an outgoing URL</a>
</body>
</html>

链接生成如下 HTML:

<a href="/app/Customer/actions/Index">This is an outgoing URL</a>

在15章中我针对 Customer 控制器应用的Route特性为:

...
[Route("app/[controller]/actions/[action]/{id:weekday?}")]
public class CustomerController : Controller {
...

传递额外值

您可以将段变量的值传递给路由系统,方法是定义其名称以asp-route-开头的属性,后面跟着段名,以使asp-route-id用于设置id段的值,如清单16-6所示。

清单 16-6:Views/Shared 文件夹下的 Result.cshtml 文件,为段变量提供值

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Home" asp-action="Index" asp-route-id="Hello">
        This is an outgoing URL
    </a>
</body>
</html>

我为一个名为id的段变量提供了一个值。如果应用程序使用清单16-6所示的路由,那么视图中将呈现以下 HTML:

<a href="/App/DoIndex?id=Hello">This is an outgoing URL</a>

注意,段值已作为查询字符串的一部分添加,以适应路由描述的 URL 模式。这是因为在那个路由中没有对应于id的段变量。为了解决这个问题,我在 Startup.cs 文件中编辑了路由,只留下一个有id段的路由,如清单16-7所示。

清单 16-7:UrlsAndRoutes 文件夹下的 Startup.cs 文件,编辑路由

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                //routes.MapRoute(
                //    name: "NewRoute",
                //    template: "App/Do{action}",
                //    defaults: new { controller = "Home" });

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

再次运行应用程序,您将看到标签助手生成以下 HTML 元素,其中id属性值包含在 URL 段中:

<a href="/Home/Index/Hello">This is an outgoing URL</a>

理解段变量重用

当我描述路由与出站 URL 匹配的方式时,我解释说,当试图为路由的 URL 模式中的每个段变量找到值时,路由系统将查看来自当前请求的值。这是一种混淆许多程序员的行为,可能导致长时间的调试会话。、

假设应用程序只有一个路由,如下所示:

...
app.UseMvc(routes => {
    routes.MapRoute(name: "MyRoute",
        template: "{controller}/{action}/{color}/{page}");
});
...

现在假设一个用户当前位于URL /Home/Index/Red/100,我渲染一个链接如下:

...
<a asp-controller="Home" asp-action="Index" asp-route-page="789">
This is an outgoing URL
</a>
...

您可能认为路由系统无法匹配路由,因为我没有为color段变量提供一个值,并且没有定义默认值。但是,您可能会出错。路由系统将与我定义的路由匹配。它将生成以下 HTML:

<a href="/Home/Index/Red/789">This is an outgoing URL</a>

路由系统热衷于与路由进行匹配,以便在生成传出 URL 时重用来自传入 URL 的段变量值。本例中,我以color变量的Red值结束,因为我的假想用户是从这个 URL 开始的。

这不是最终的行为。路由系统将应用这一技术作为其对路由的定期评估的一部分,即使存在匹配的后续路由,也不需要重用当前请求中的值。

我强烈建议您不要依赖此行为,而为 URL 模式中的所有段变量提供值。依赖这种行为不仅会使您的代码更难阅读,而且您最终会对用户发出请求的顺序做出假设,这将在应用程序进入维护时最终刺痛您。


生成完全限定 URL

到目前为止生成的所有链接都包含相对 URL,但是锚元素标签助手也可以生成完全限定的 URL,如清单16-8所示。

清单 16-8:Views/Shared 文件夹下的 Result.cshtml 文件,生成完全限定 URL

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Home" asp-action="Index" asp-route-id="Hello"
       asp-protocol="https" asp-host="myserver.mydomain.com"
       asp-fragment="myFragment">
        This is an outgoing URL
    </a>
</body>
</html>

asp-protocolasp-hostasp-fragment属性用于指定协议(清单中为 https),服务器名称(myserver.mydomain.com)和 URL 段(myFragment)。这些值与路由系统的输出相结合以创建完全限定 URL,如果运行应用程序您可以看到发送到浏览器的 HTML。

<a href="https://myserver.mydomain.com/Home/Index/Hello#myFragment">
    This is an outgoing URL
</a>

在使用完全限定 URL 时要小心,因为它们会创建对应用程序基础结构的依赖关系,当基础结构发生变化时,您必须记住对 MVC 视图进行相应的更改。

从指定路由生成 URL

在前面的示例中,路由系统选择了将用于生成 URL 的路由。如果以特定格式生成 URL 很重要,则可以指定用于生成传出 URL 的路由。为了演示它是如何工作的,我在 Startup.cs 文件中添加了一个新的路由,以便在示例应用程序中有两个路由,如清单16-9所示。

清单 16-9:UrlsAndRoutes 文件夹下的 Startup.cs 文件,添加路由

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                //routes.MapRoute(
                //    name: "NewRoute",
                //    template: "App/Do{action}",
                //    defaults: new { controller = "Home" });

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

                routes.MapRoute(
                    name: "out",
                    template: "outbound/{controller=Home}/{action=Index}");
            });
        }
    }
}

清单16-10中所示的视图包含两个锚元素,每个锚元素指定相同的控制器和 action。不同之处在于,第二个