# 四、路由系统

## 基本路由

所有的 HTTP 请求都通过路由系统，路由系统决定什么来管理请求。路由系统的主要任务是决定应该调用哪个控制器的哪个动作来管理实际的请求。为了做出这个决定，路由系统解析 HTTP 请求(特别是动词和 URI)，并获得一系列令牌，这些令牌与包含所有可能路由的路由表相匹配。

正如我们在[第 2 章](02.html#_Chapter_2_)中看到的，当我们创建一个新的 Web API 应用程序时，模板会为我们生成一个默认路由:

```cs
  using System.Web.Http;

  namespace HelloWebApi
  {
      public static class WebApiConfig
      {

  public static void Register(HttpConfiguration config)

  {

  config.Routes.MapHttpRoute(

  name: "DefaultApi",

  routeTemplate: "api/{controller}/{id}",

  defaults: new { id = RouteParameter.Optional }

  );

  }
      }
  }

```

这种情况下的方法`MapHttpRoute`采用三个参数:

*   路线名称(`DefaultApi`)。
*   一个路由模板:一个带有文字(`api`)和两个占位符(`controller`和`id`)的模板，将被当前请求段替换。
*   默认值:在这种情况下，我们是说`id`在请求中不是强制性的。

如果你用过 ASP.NET MVC，你会发现几个相似之处。然而，在 MVC 中，您应该有操作，而在这里操作不存在。这是两种路由系统的主要区别。在网络应用编程接口中，动作由 HTTP 方法决定，我们将在后面看到。

`MapHttpRoute`方法只是在存储所有路线的字典中添加一个新条目。鉴于我们已经定义了一个`PostsController`，我们刚刚定义的路由将响应这些请求:

*   /API/post
*   /api/Posts/42
*   /API/post/Syncfusion

如果路由系统没有找到正确的匹配，它将向呼叫者返回 HTTP 状态`404 Not Found`。

为了决定应该调用哪个动作，路由系统必须分析 HTTP 方法。如果我们向服务器发出`GET`请求，动作应该类似于 **`Get`** `SomeResource` (…)。如果我们放置一个`POST`，动作应该类似于 **`Post`** `SomeOtherResource` (…)。默认方法的一般规则是动作必须以动作名开始，所以如果我们考虑一个`GET` HTTP 请求，那么`Get(…)`、`GetPosts(…)`和`GetSomething(…)`都是有效的动作。

上一条路由的完整路由表如下:

表 2:路由表

| HTTP 方法 | 上呼吸道感染 | 控制器 | 行动 | 参数 |
| 得到 | /API/post | 后置控制器 | Get() | 不适用的 |
| 得到 | /api/Posts/42 | 后置控制器 | Get(int id) | forty-two |
| 邮政 | /API/post | 后置控制器 | 员额(c 员额) | 从身体里 |
| 删除 | /api/Posts/42 | 后置控制器 | 删除(int id) | forty-two |

Visual Studio 为我们定义的路线可以更改，我们不会被迫使用它，即使这是每个 REST 服务的一般最佳实践。

修改或添加新路由时，请考虑按照添加路由的顺序评估路由表。所以将使用第一个匹配。

考虑这个例子:

```cs
  using System.Web.Http;

  namespace HelloWebApi
  {
      public static class WebApiConfig
      {

  public static void Register(HttpConfiguration config)

  {
              config.Routes.MapHttpRoute(

  name: "PostByDate",

  routeTemplate: "api/{controller}/{year}/{month}/{day}",

  defaults: new { month = RouteParameter.Optional, day =  RouteParameter.Optional }

  );

  config.Routes.MapHttpRoute(

  name: "DefaultApi",

  routeTemplate: "api/{controller}/{id}",

  defaults: new { id = RouteParameter.Optional }

  );

  }
      }
  }

```

这里，我们在默认路由之前定义了一个新路由；这意味着名为`PostByDate`的路线被优先评估。这个新路线有四个占位符:一个用于控制器名称，三个用于定义日期(年、月和日)。在`defaults`值参数中，我们指定月和日是可选的，但年仍然是强制的。

为了让这个例子起作用，我们定义了一个新的`PostsController`，如下所示:

```cs
  using System.Web.Http;

  namespace HelloWebApi
  {
      public class PostsController : ApiController
      {

  public IQueryable<Post> Get(int year, int month = 0, int day = 0)

  {

  // Do
  something to load the posts that match the date.

  }
      }
  }

```

`Get`方法取三个参数:一年(必选)，一个月和一天是可选的(如果没有给定，它们将包含给定类型的默认值，在我们的例子中是零)。

该请求将被解析和分解，以便能够用其参数调用`Get`动作:

表 3:请求 URI 和参数值

| 上呼吸道感染 | 年 | 月 | 一天 |
| GET/API/post/2010/04/11 | Two thousand and ten | four | Eleven |
| GET/API/post/1977/11 | One thousand nine hundred and seventy-seven | Eleven | Zero |
| Get /api/Posts/1973 | One thousand nine hundred and seventy-three | Zero | Zero |

如您所见，通过这个简单的路线，我们定义了一个 REST 接口来根据日期查询帖子，日期可以是精确的一天、特定的一个月或一整年。

我们必须定义另外两件事来确保一切都是正确的。

首先，像这样的路由通常使用单个控制器。很奇怪，每个资源都可以用日期来查询，所以最好在路线和控制器之间建立一个紧密的链接。这可以通过以下方式实现:

```cs
  // ...
  config.Routes.MapHttpRoute(
      name: "PostByDate",
      routeTemplate: "api/Posts/{year}/{month}/{day}",
      defaults: new
          {
              controller = "Posts",
              month = RouteParameter.Optional,
              day = RouteParameter.Optional
          }
      );
  // ...

```

我们可以简单地删除控制器占位符，并使用文字“`Posts`”，这样只有当请求包含 posts 字符串(不区分大小写)时，路由才会匹配。

另一个问题是，这条路线没有对参数施加任何约束，所以这个 URI:

`/api/Posts/2013/May`

因为月份是一个字符串，没有任何东西管理转换。

为了确保参数是数字，我们可以使用如下代码所示的路由约束:

```cs
  // ...
  config.Routes.MapHttpRoute(
      name: "PostByDate",
      routeTemplate: "api/Posts/{year}/{month}/{day}",
      defaults: new
          {
              controller = "Posts",
              month = RouteParameter.Optional,
              day = RouteParameter.Optional
          },

      constraints: new
          {
              month = @"\d{0,2}", day = @"\d{0,2}"
          }

```

);

// ...

constraints 参数使用正则表达式来确保月和日是两个数字(由零、一或两位数字组成)。

如前所述，必须执行的控制器操作是由 HTTP 方法选择的，因此我们只有七个操作可用。如果我想要更多的操作或不以获取、发布等开头的自定义操作名称，会发生什么？

实现这种行为的一种方法是在路线定义中使用占位符`action`:

```cs
  // ...
  config.Routes.MapHttpRoute(
      name:
  "PostsCustomAction",

  routeTemplate: "api/{controller}/{action}/{id}",

  defaults: new { id = RouteParameter.Optional }
      );
  // ...

```

给定这个路由定义，动作将被显式地写入 URL，因此我们不会被迫使用七个默认动作。但是我们可以使用如下网址:

`/api/Posts/Category/10`

将由`Posts`控制器中的动作`Category`管理，但该成员必须具有指定其支持的方法的属性:

```cs
  public class PostsController : ApiController
  {
      [HttpGet]
      public string Category(int id)
      {
          // ...
      }
  }

```

`HttpGet`属性告诉运行时，动作`Category`应该只对一个 HTTP `GET`方法调用。

如果我们试图`POST`到这个网址，我们会得到以下错误:

*请求的资源不支持 http 方法‘POST’*

匹配所有 HTTP 方法的可能属性有:

*   http get(http get)
*   http post(http post)
*   http put(http put)
*   HttpOptions
*   HttpPatch
*   HttpDelete
*   HttpHead

## 总结

有了所有这些选项，我们就有了定义满足所有请求的完整路由表所需的一切。设计路由表时，请始终牢记代表用户界面重要部分的网址的可读性。