Web API路由与动作(三)

时间:2022-01-13 07:11:42

本章包括三个小节  如果你输入了mvc的路由规则 这个可以粗略过一遍即可  内容说明有点繁琐

原文地址:http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api

3.1ASP.NET Web API中的路由

  本节描述ASP.NET Web API如何将HTTP请求路由到控制器

  如果你熟悉ASP.NET MVC,Web API路由与MVC路由十分类似。主要差别是Web API使用HTTP方法而不是URI路径来选择动作。你也可以在Web API中使用MVC风格的路由。本文不假设你具备ASP.NET MVC的任何知识

  

一:路由表

  在ASP.NET Web API中,一个控制器是处理HTTP请求的一个类。控制器的public方法称为动作方法action methods)或简称为动作action)。当Web API框架接收到一个请求时,它将这个请求路由到一个动作

  为了确定调用哪一个动作,框架使用了一个路由表routing table)。Visual Studio中Web API的项目模板会创建一个默认路由

  

  

routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

 

 这条路由是在WebApiConfig.cs文件中定义的,该文件位于App_Start目录

  Web API路由与动作(三)

  

  如果要自托管(self-host )Web API,你必须直接在HttpSelfHostConfiguration对象上设置路由表

  路由表中的每一个条目都包含一个路由模板route template)。Web API的默认路由模板是“api/{controller}/{id}”。在这个模板中,“api”是一个文字式路径片段,而{controller}和{id}则是占位符变量

  当Web API框架接收一个HTTP请求时,它会试图根据路由表中的一个路由模板来匹配其URI。如果无路由匹配,客户端会接收到一个404(未找到)错误。例如,以下URI与这个默认路由的匹配:

  

  • /api/contacts
  • /api/contacts/1
  • /api/products/gizmo1

  然而,以下URI不匹配,因为它缺少“api”片段  

  • /contacts/1

  注:在路由中使用“api”的原因是为了避免与ASP.NET MVC的路由冲突。通过这种方式,可以用“/contacts”进入一个MVC控制器,而“/api/contacts”进入一个Web API控制器。当然,如果你不喜欢这种约定,可以修改这个默认路由表

  

  一旦找到了匹配路由,Web API便会选择相应的控制和动作:

  1.为了找到控制器,Web API会把“控制器”加到{controller}变量的值

  2.为了找到动作,Web API会考查HTTP方法,然后寻找一个名称以HTTP方法名开头的动作。例如,对于一个GET请求,Web API会查找一个以“Get…”开头的动作,如“GetContact”或“GetAllContacts”等。这种约定仅运用于GET、POST、PUT和DELETE方法。通过把注解属性运用于控制器,你可以启用其它HTTP方法。后面会看到一个例子

  3.路由模板中的其它占位变量,如{id},被映射成动作参数

  

  让我们考察一个例子。假设你定义了以下控制器:

   

public class ProductsController : ApiController
{
public void GetAllProducts() { }
public IEnumerable<Product> GetProductById(int id) { }
public HttpResponseMessage DeleteProduct(int id){ }
}

  以下是一些可能的HTTP请求,及其将要被调用的动作:

  

  HTTP方法     URI路径     动作         参数
     GET        api/products   GetAllProducts   (无)
  GET        api/products/4   GetProductById   4
  DELETE      api/products/4   DeleteProduct      4
  POST       api/products     (不匹配)

  注意,URI的{id}片段如果出现,会被映射到动作的id参数。在这个例子中,控制器定义了两个GET方法,其中一个带有id参数,而另一个不带参数

  另外要注意,POST请求是失败的,因为该控制器未定义“Post…”方法

二:路由变异

  1.HTTP方法

  替代用于HTTP方法的命名约定,可以明确地为一个动作指定HTTP方法,这是通过以HttpGetHttpPutHttpPostHttpDelete注解属性对动作方法进行修饰来实现的

  在下列示例中,FindProduct方法被映射到GET请求

  

public class ProductsController : ApiController
{
[HttpGet]
public Product FindProduct(id) {}
}

  要允许一个动作有多个HTTP方法,或允许对GET、PUT、POST和DELETE以外的其它HTTP方法,需使用AcceptVerbs(接收谓词)注解属性,它以HTTP方法列表为参数

  

public class ProductsController : ApiController
{
[AcceptVerbs("GET", "HEAD")] // 指示该动作接收HTTP的GET和HEAD方法 — 译者注
public Product FindProduct(id) { } // WebDAV method
// WebDAV方法(基于Web的分布式著作与版本控制的HTTP方法,是一个扩展的HTTP方法 — 译者注)
[AcceptVerbs("MKCOL")] // MKCOL是隶属于WebDAV的一个方法,它在URI指定的位置创建集合
public void MakeCollection() { }
}

  2.通过路由名称

  利用默认的路由模板,Web API使用HTTP方法来选择动作。然而,也可以创建在URI中包含动作名的路由

  

routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);

  在这个路由模板中,{action}参数命名了控制器上的动作方法。采用这种风格的路由,需要使用注解属性来指明所允许的HTTP方法。例如,假设你的控制器有以下方法

  

public class ProductsController : ApiController
{
[HttpGet]
public string Details(int id);
}

  在这个例子中,一个对“api/products/details/1”的GET请求会映射到这个Details方法。这种风格的路由类似于ASP.NET MVC,而且可能与RPC式的API相接近

  可以通过使用ActionName注解属性来覆盖动作名。在以下例子中,有两个动作映射到“api/products/thumbnail/id”。一个支持GET,而另一个支持POST

  

public class ProductsController : ApiController
{
[HttpGet]
[ActionName("Thumbnail")]
public HttpResponseMessage GetThumbnailImage(int id); [HttpPost]
[ActionName("Thumbnail")]
public void AddThumbnailImage(int id);
}

  3.非动作

  为了防止一个方法被作为一个动作所请求,要使用NonAction注解属性。它对框架发出信号:该方法不是一个动作,即使它可能与路由规则匹配

  

// Not an action method.
// 不是一个动作方法
[NonAction]
public string GetPrivateData() { ... }

  

 3.2路由与动作选择

  该节描述ASP.NET Web API如何把一个HTTP请求路由到控制器的一个特定的方法上

  本文考察路由过程的细节。如果你创建了一个Web API项目,并发现有些请求并未按你期望的方式被路由,希望这篇文章对你会有所帮助

  路由有三个主要阶段:

  1.将URI匹配到一个路由模板

  2.选择一个控制器

  3.选择一个动作

  你可以用自己的自定义行为来替换这一过程的某些部分。在本节中,我会描述默认行为。最后,我会注明可以在什么地方自定义行为

一:路由模板

  路由模板看上去类似于一个URI路径,但它可以具有占位符,这是用花括号来指示的

  

"api/{controller}/public/{category}/{id}"

  当创建一条路由时,可以为某些或所有占位符提供默认值:

  

defaults: new { category = "all" }

  也可以提供约束,它限制URI片段如何与占位符匹配:

  

constraints: new { id = @"\d+" }   // Only matches if "id" is one or more digits.
// 用正则表达式限制片段的取值,上语句表明,id片段的值必须是一个或多个数字。
// 因此,URI中id片段必须是数字才能与这条路由匹配

  框架会试图把URI路径的片段与该模板进行匹配。模板中的文字必须严格匹配。占位符可以匹配任意值,除非你指定了约束。框架不会匹配URI的其它部分,如主机名或查询字符串。框架会选择路由表中与URI匹配的第一条路由

  有两个特殊的占位符:“{controller}”和“{action}”

  1.“{controller}”提供控制器名。

  2.“{action}”提供动作名。在Web API中,通常的约定是忽略“{action}”的

二:默认值

  

  如果提供默认值,该路由将能够匹配缺少这些片段的URI。例如:

  

routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{category}",
defaults: new { category = "all" }
);

  URI“http://localhost/api/products”与这条路由是匹配的。“{category}”片段被赋成了默认值“all”

 

三:路由字典

  

  如果框架为一个URI找到一个匹配,它会创建一个字典,其中包含了每个占位符的值。(字典的内容是一些“键-值”对 — 译者注)。其键是不带花括号的占位符名称。其值取自URI路径或默认值。该字典被存储在IHttpRouteData对象中

  在路由匹配阶段,“{controller}”和“{action}”占位符的处理与其它占位符的处理是一样的。只是把它们简单地用值存储在字典中

  在默认值中可以使用特殊的RouteParameter.Optional值。如果一个占位符被赋予了这个值,则该值不会被添加到路由字典。例如

  

routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{category}/{id}",
defaults: new { category = "all", id = RouteParameter.Optional }
);

  对于URI路径“api/products”,路由字典将含有

  1.controller: "products"

  2.category: "all"

  由于这条URI路径中不包含id,因此,id的值将采用默认的RouteParameter.Optional,所以,路由字典中不会包含id片段的键值对

  然而,对于“api/products/toys/123”,路由字典将含有

  1.controller: "products"
  2.category: "toys"
  3.id: "123"

  默认值也可以包含未出现在路由模板中的值。若这条路由匹配,则该值会被存储在路由字典中。例如:

  

routes.MapHttpRoute(
name: "Root",
routeTemplate: "api/root/{id}",
defaults: new { controller = "customers", id = RouteParameter.Optional }
);

  

  如果URI路径是“api/root/8”,字典将含有两个值  

  1.controller: "customers"
  2.id: "8"

四:选择控制器

  控制器选择是由IHttpControllerSelector.SelectController方法来处理的。这个方法以HttpRequestMessage实例为参数,并返回HttpControllerDescriptor。其默认实现是由DefaultHttpControllerSelector类提供的。这个类使用了一种很直接的算法:

  1.查找路由字典的“controller”键

  2.取得这个键的值,并附加字符串“Controller”,以得到控制器的类型名

  3.用这个类型名查找Web API控制器

  例如,如果路由字典的键-值对为“controller”=“products”,那么,控制器类型便为“ProductsController”。如果没有匹配类型,或有多个匹配,框架会给客户端返回一条错误

  DefaultHttpControllerSelector使用IHttpControllerTypeResolver接口以获得Web API控制器类型的列表。IHttpControllerTypeResolver的默认实现会返回所有符合以下条件的public类:

  (a)实现IHttpController的类

  (b)是非抽象类,

  且(c)名称以“Controller”结尾的类

五:动作选择

  选择了控制器之后,框架会通过调用IHttpActionSelector.SelectAction方法来选择动作。这个方法以HttpControllerContext为参数,并返回HttpActionDescriptor

  默认实现是由ApiControllerActionSelector类提供的。为了选择一个动作,会查找以下方面

  1.请求的HTTP方法

  2.路由模板中的“{action}”占位符(如果有)

  3.控制器中动作的参数

  在查找选择算法之前,我们需要理解控制器动作的一些事情

  控制器中的哪些方法被看成为是“动作”?当选择一个动作时,框架只考察控制器的public实例方法。而且,它会排除“special name"特殊名称”的方法(构造器、事件、操作符重载等等),以及继承于ApiController类的方法。

  这里按原文的含义似乎是要排除API控制器中的public方法,但译者认为,框架会把API控制器中的public方法看成是动作 — 译者注

   

  HTTP方法。框架只会选择与请求的HTTP方法匹配的动作,确定如下:

  1.你可以用注解属性AcceptVerbs、HttpDelete、HttpGet、HttpHead、HttpOptions、HttpPatch、HttpPost、或HttpPut来指定HTTP方法(这段文字说明,你可以在方法上用这些注解属性进行标注,以指定该方法用于处理哪一种HTTP请求。通过这种标注,方法的命名可以不遵循下一条的约定 — 译者注)

  2.否则,如果控制器方法名称以“Get”、“Post”、“Put”、“Delete”、“Head”、“Options”、或“Patch”开头,那么,按照约定,该动作支持相应的HTTP方法

  3.如果以上都不是(即,既未用第一条的办法进行标注,又未用第二条的方法命名约定 — 译者注),则该方法支持POST

  参数绑定Parameter Bindings参数绑定是指Web API如何创建参数值。以下是参数绑定的默认规则:

  1.简单类型取自URI

  2.复合类型取自请求体

  

  简单类型包括所有“.NET 框架简单类型”,另外还有,DateTime、Decimal、Guid、String和TimeSpan。对于每一个动作,最多只有一个参数可以读取请求体

  

六:扩展点

  Web API为路由过程的某些部分提供了扩展点。

  

  接口以及描述

  1.IHttpControllerSelector --- 选择控制器。

  2.IHttpControllerTypeResolver---获取控制器类型列表。DefaultHttpControllerSelector从该列表选择控制器

  3.IAssembliesResolver---获取项目程序集列表。IHttpControllerTypeResolver接口用该列表查找控制器类型。

  4.IHttpControllerActivator---创建控制器新实例。

  5.IHttpActionSelector---选择动作。

  6.IHttpActionInvoker---调用动作。

3.3 ASP.NET Web API中的异常处理

  本节描述ASP.NET Web API中的错误与异常处理:

  1.HttpResponseException

  2.Exception Filters 异常过滤器

  3.Registering Exception Filters 注册异常过滤器

  4.HttpError

  

一: HttpResponseException

  如果一个Web API控制器抛出一个未捕捉异常,会发生什么?默认地,大多数异常都会被转化成一个带有状态码“500 – 内部服务器错误”的HTTP响应

  HttpResponseException(HTTP响应异常)类型是一种特殊的情况。这种异常会返回你在异常构造器中指定的任何HTTP状态码。例如,在以下方法中,如果id参数非法,会返回“404 — 未找到”

  

public Product GetProduct(int id)
{
Product item = repository.Get(id);
if (item == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return item;
}

  为了对响应进行更多控制,你也可以构造整个响应消息,并用HttpResponseException来包含它

  

public Product GetProduct(int id)
{
Product item = repository.Get(id);
if (item == null)
{
var resp = new HttpResponseMessage(HttpStatusCode.NotFound)
{
Content = new StringContent(string.Format("No product with ID = {0}", id)),
ReasonPhrase = "Product ID Not Found"
}
throw new HttpResponseException(resp);
}
return item;
}

  

二:Exception Filters

  通过编写一个异常过滤器(exception filter),你可以定制Web API如何处理异常。当一个控制器抛出一个非HttpResponseException异常的未处理异常时,会执行一个异常过滤器。HttpResponseException类型一个特殊的情况,因为它是专门设计用来返回一个HTTP响应的

  异常过滤器实现System.Web.Http.Filters.IExceptionFilter接口。编写异常过滤器最简单的方式是通过System.Web.Http.Filters.ExceptionFilterAttribute类进行派生,并重写其OnException方法

  ASP.NET Web API中的异常过滤器要比ASP.NET MVC中的简单些。然而,这两者是在不同的命名空间中声明的,且是功能独立的。特别是MVC中使用的HandleErrorAttribute类不会处理Web API控制器中抛出的异常。

  以下是将NotImplementedException异常转换成HTTP状态码“501 — 未实现”的一个过滤器

  

namespace ProductStore.Filters
{
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http.Filters; public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is NotImplementedException)
{
context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
}
}
}
}

  HttpActionExecutedContext对象的Response属性含有将发送给客户端的HTTP响应消息。

三:Registering Exception Filters  注册异常过滤器

  

  以下是注册Web API异常过滤器的几种方式:

    1.由动作注册

    2.由控制器注册

    3.全局注册

  要把过滤运用于特定的动作,在动作上添加该过滤器的注解属性

  

public class ProductsController : ApiController
{
[NotImplExceptionFilter]
public Contact GetContact(int id)
{
throw new NotImplementedException("This method is not implemented");
}
}

  

  要把过滤器运用于一个控制器的所有动作,在控制器上添加该过滤器的注解属性 NotImplExceptionFilter

  要全局性地把过滤器运用于所有Web API控制器,将该过滤器的一个实例添加到GlobalConfiguration.Configuration.Filters集合。这个集合中的异常过滤器会运用于任何Web API控制器动作

  

GlobalConfiguration.Configuration.Filters.Add(
new ProductStore.NotImplExceptionFilterAttribute());

  如果用的是“ASP.NET MVC 4 Web应用程序”项目模板创建的项目,要把你的Web API配置代码被放在WebApiConfig类中,它位于App_Start文件夹

  

public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute()); // Other configuration code(其它配置代码)...
}
}

  

四:HttpError

  HttpError对象为在响应体中返回错误消息提供了相应的方式。以下示例演示了如何用HttpError在响应体中返回HTTP状态码“404 — 未找到”

  

public HttpResponseMessage GetProduct(int id)
{
Product item = repository.Get(id);
if (item == null)
{
var message = string.Format("Product with id = {0} not found", id);
HttpError err = new HttpError(message);
return Request.CreateResponse(HttpStatusCode.NotFound, err);
}
else
{
return Request.CreateResponse(HttpStatusCode.OK, item);
}
}

  

  在这个例子中,如果该方法成功,它会在HTTP响应中返回产品。但如果所请求的产品未找到,则HTTP响应会在请求体中包含一个HttpError。该响应看上去大致像这样:

  

HTTP/1.1  Not Found
Content-Type: application/json; charset=utf-
Date: Thu, Aug :: GMT
Content-Length: {
"Message": "Product with id = 12 not found"
}

  注意,在这个例子中,HttpError会被序列化成JSON。使用HttpError的一个好处是,与其它强类型模型一样,会进行同样的“内容协商”(本系列化教程的第6.2小节 — 译者注)和序列化过程

  替代直接创建HttpError对象的一种办法是,你可以使用CreateErrorResponse方法:

  

public HttpResponseMessage GetProduct(int id)
{
Product item = repository.Get(id);
if (item == null)
{
var message = string.Format("Product with id = {0} not found", id);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
}
else
{
return Request.CreateResponse(HttpStatusCode.OK, item);
}
}

  

  CreateErrorResponse是在System.Net.Http.HttpRequestMessageExtensions类中定义的一个扩展方法。本质上,CreateErrorResponse会创建一个HttpError实例,然后创建一个包含该HttpErrorHttpResponseMessage

  

  补充事例:将自定义“键-值”添加到HttpError

  HttpError类实际上是一个“键-值”集合(它派生于Dictionary<string, object>),因此你可以添加自己的“键-值”对:

  

public HttpResponseMessage GetProduct(int id)
{
Product item = repository.Get(id); if (item == null)
{
var message = string.Format("Product with id = {0} not found", id);
var err = new HttpError(message);
err["error_sub_code"] = ;
return Request.CreateErrorResponse(HttpStatusCode.NotFound, err);
}
else
{
return Request.CreateResponse(HttpStatusCode.OK, item);
}
}

  

 以HttpResponseException来使用HttpError

  前面的例子是从控制器动作返回一个HttpResponseMessage消息,但你也可以使用HttpResponseException来返回一个HttpError。这让你能够在正常成功情况下返回强类型模型,而在有错误时,仍返回HttpError

  

public Product GetProduct(int id)
{
Product item = repository.Get(id);
if (item == null)
{
var message = string.Format("Product with id = {0} not found", id);
throw new HttpResponseException(
Request.CreateErrorResponse(HttpStatusCode.NotFound, message));
}
else
{
return item;
}
}