本章包括三个小节 如果你输入了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目录
如果要自托管(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方法,这是通过以HttpGet、HttpPut、HttpPost或HttpDelete注解属性对动作方法进行修饰来实现的
在下列示例中,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 404 Not Found Content-Type: application/json; charset=utf-8 Date: Thu, 09 Aug 2012 23:27:18 GMT Content-Length: 51 { "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实例,然后创建一个包含该HttpError的HttpResponseMessage。
补充事例:将自定义“键-值”添加到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"] = 42; 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; } }