本文是針對ASP.NET WepApi 2 的筆記。
Web API 可返回的結果:
1.void
2.HttpResponseMessage
3.IHttpActionResult
4.其他類型
返回類型 | Web API創建響應的方式 |
void | HTTP204(無內容) |
HttpResponseMessage | 轉換為HTTP響應消息 |
IHttpActionRsult | 調用ExecuteAsync來創建HttpResponseMessage,然后轉換為HTTP響應消息 |
其他類型 | 將序列化的返回值寫到響應正文中,返回HTTP200 |
路由
路由表:Web API的默認路由模板"api/{controller}/{id}",此模板中,"api"是文本路徑段,{controller}和{id}是占位符變量。
路由變體:可以顯示指定Action的HTTP方法,如HttpGet,HttpPost。
public class ProductsController : ApiController { [HttpGet] public Product FindProduct(id) {} }
若要允許多個HTTP方法或GET,POST,PUT,DELETE以外的HTTP方法,可以顯示使用AcceptVerbs屬性:
public class ProductsController : ApiController { [AcceptVerbs("GET", "HEAD")] public Product FindProduct(id) { } // WebDAV method [AcceptVerbs("MKCOL")] public void MakeCollection() { } }
可以通過ActionName屬性重寫Action名稱:
public class ProductsController : ApiController { [HttpGet] [ActionName("Thumbnail")] public HttpResponseMessage GetThumbnailImage(int id); [HttpPost] [ActionName("Thumbnail")] public void AddThumbnailImage(int id); }
也可以使用NonAction屬性指定Action不參與路由匹配:
// Not an action method. [NonAction] public string GetPrivateData() { ... }
Web API路由過程的某些部分提供了擴展點:
接口 | 描述 |
IHttpControllerSelector | 選擇控制器 |
IHttpControllerTypeResolver | 獲取控制器類型的列表。DefaultHttpControllerSelector從此列表中選擇控制器類型 |
IAssembliesResolver | 獲取項目的程序集的列表。IHttpControllerTypeResolver接口使用此列表找到控制器的類型 |
IHttpControllerActivator | 創建新的控制器實例 |
IHttpActionSelector | 選擇Action |
IHttpActionInvoker | 調用Action |
若要為這些接口提供自己的實現,在HttpConfiguration對象上操作,HttpConfigutation在App_Start文件夾下的WebApiConfig.cs文件下能找到,也可以自己獲取:
var config = GlobalConfiguration.Configuration; config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));
屬性路由:Route是API 2支持的一種新類型的路由,顧名思義,它可以使用屬性定義路由:
[Route("customers/{customerId}/orders")] public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
你可能需要先啟用,才能使用屬性路由:
using System.Web.Http; namespace WebApplication { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API routes config.MapHttpAttributeRoutes(); // Other Web API configuration not shown. } } }
路由名稱:
在Web API 中,每個路由都有一個名稱。路由名稱可用於生成鏈接,以便可以在HTTP響應中包含一個鏈接。
Route可以設置名稱屬性,下面的代碼演示如何設置路由名稱,以及如何使用根據路由名稱生成的鏈接:
public class BooksController : ApiController { [Route("api/books/{id}", Name="GetBookById")] public BookDto GetBook(int id) { // Implementation not shown... } [Route("api/books")] public HttpResponseMessage Post(Book book) { // Validate and add book to database (not shown) var response = Request.CreateResponse(HttpStatusCode.Created); // Generate a link to the new book and set the Location header in the response. string uri = Url.Link("GetBookById", new { id = book.BookId }); response.Headers.Location = new Uri(uri); return response; } }
路由順序:屬性路由還可以指定路由匹配時的順序,RouteOrder,默認順序是0,首先計算較低的值。
媒體格式化
媒體類型,也稱MIME類型。在HTTP中,媒體類型描述消息正文的格式。例如:
- text/html
- image/png
- application/json
當HTTP消息包含實體正文時,Content-type標頭指定消息正文的格式。
例如,如果HTTP響應包含PNG圖像,響應可能包含以下標頭:
HTTP/1.1 200 OK Content-Length: 95267 Content-Type: image/png
當客戶端發送請求時,它可以包含Accept標頭。Accept標頭指示客服端想要從服務器獲取的響應媒體類型。例如:
Accept: text/html,application/xhtml+xml,application/xml
此標頭指示客戶端希望獲得HTML、XHTML或XML。
媒體類型可以確定Web API 如何序列化和反序列化HTTP消息正文。可以通過編寫自己的媒體類型達到擴展Web API序列化和反序列化的目的。
若要創建媒體格式化程序,需要實現以下類:
- MediaTypeFormatter。 此類使用異步的方式讀取和寫入。
- BufferedMediaTypeFormatter。此類派生自MediaTypeFormatter,但是使用同步的方式讀取和寫入。
下面的實例演示的媒體類型可以序列化以逗號分割值(CSV)格式的產品對象。下面是Product對象的定義:
namespace ProductStore.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
若要實現CSV格式化程序,定義一個類,派生自BufferedMediaTypeFormatter:
namespace ProductStore.Formatters using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; using ProductStore.Models; namespace ProductStore.Formatters { public class ProductCsvFormatter : BufferedMediaTypeFormatter { } }
在構造函數中,添加格式化程序支持的媒體類型。在此示例中,支持類型"text/csv":
public ProductCsvFormatter() { // Add the supported media type. SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv")); }
重寫CanWriteType方法,以指示該類型格式化程序可以序列化:
public override bool CanWriteType(System.Type type) { if (type == typeof(Product)) { return true; } else { Type enumerableType = typeof(IEnumerable<Product>); return enumerableType.IsAssignableFrom(type); } }
在此示例中,格式化程序可以序列化單個Product對象的集合、Product對象。
同樣,重寫CanReadType方法,以指示該類型格式化程序可以反序列化。在此示例中,格式化程序不支持反序列化,因此,此方法返回false:
public override bool CanReadType(Type type) { return false; }
最后,重寫WriteToStream方法。此方法通過向流寫入序列化程序。如果支持反序列化,還可以重寫ReadFromStream方法。
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content) { using (var writer = new StreamWriter(writeStream)) { var products = value as IEnumerable<Product>; if (products != null) { foreach (var product in products) { WriteItem(product, writer); } } else { var singleProduct = value as Product; if (singleProduct == null) { throw new InvalidOperationException("Cannot serialize type"); } WriteItem(singleProduct, writer); } } } // Helper methods for serializing Products to CSV format. private void WriteItem(Product product, StreamWriter writer) { writer.WriteLine("{0},{1},{2},{3}", Escape(product.Id), Escape(product.Name), Escape(product.Category), Escape(product.Price)); } static char[] _specialChars = new char[] { ',', '\n', '\r', '"' }; private string Escape(object o) { if (o == null) { return ""; } string field = o.ToString(); if (field.IndexOfAny(_specialChars) != -1) { // Delimit the entire field with quotes and replace embedded quotes with "". return String.Format("\"{0}\"", field.Replace("\"", "\"\"")); } else return field; }
將自定義的媒體格式化程序添加到Web API管道
public static void ConfigureApis(HttpConfiguration config) { config.Formatters.Add(new ProductCsvFormatter()); }
WebAPI 提供了對JSON和XML的媒體類型格式化程序。
JSON格式由JsonMediaTypeFormatter類處理,默認情況下JsonMediaTypeFormatter使用Json.NET庫以執行序列化。
如果您願意,可以配置JsonMediaTypeFormatter,以使用DataContractJsonSerializer而不是Json.NET。若要執行此操作,設置UseDataContractJsonSerializer屬性true:
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter; json.UseDataContractJsonSerializer = true;
默認情況下,所有公共屬性和字段會參與序列化JSON。若要忽略屬性或字段,給它裝飾JsonIgnore屬性。
public class Product { public string Name { get; set; } public decimal Price { get; set; } [JsonIgnore] public int ProductCode { get; set; } // omitted }
你也可以使用DataContract屬性裝飾你的類,存在此屬性,則將忽略所有成員,除非它們具有DataMember(JsonProperty)屬性。此外,可以使用DataMember序列化私有成員。
[DataContract] public class Product { [DataMember] public string Name { get; set; } [DataMember] public decimal Price { get; set; } public int ProductCode { get; set; } // omitted by default }
默認情況下,只讀屬性參與序列化。
默認情況下,Json.NET將日期將采用ISO 8601格式。使用"Z"后綴編寫日期以 UTC (協調世界時)。 以本地時間日期包含時區偏移量。 例如:
2012-07-27T18:51:45.53403Z // UTC 2012-07-27T11:51:45.53403-07:00 // Local
默認情況下,Json.NET 保留時區。 您可以通過設置 DateTimeZoneHandling 屬性覆蓋此:
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter; json.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;
如果想要使用Microsoft JSON 日期格式("\/Date(ticks)\/"
) 設置而不是 ISO 8601 DateFormatHandling上序列化程序設置屬性:
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter; json.SerializerSettings.DateFormatHandling = Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;
編寫縮進的JSON,可以如下設置:
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter; json.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
JSON屬性名稱使用駝峰式,無需修改數據模型,設置CamelCasePropertyNamesContractResolver序列化程序:
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter; json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
可以返回一個匿名對象。
XML媒體類型格式化程序
默認XmlMediaTypeFormatter類。默認使用DataContractSerializer來執行序列化。
如果您願意,可以配置XmlMediaTypeFormatter若要使用XmlSerializer而不是DataContractSerializer。 若要執行此操作,設置UseXmlSerializer屬性設置為true:
var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter; xml.UseXmlSerializer = true;
XmlSerializer類支持一組更窄的類型,而不DataContractSerializer,但提供了更好地控制生成的 XML。 請考慮使用XmlSerializer如果你需要匹配現有 XML 架構。
介紹下DataContractSerializer的序列化行為:
- 所有公共的屬性和字段進行序列化。若要忽略,使用IgnoreDataMember屬性。
- 不序列私有和受保護成員。
- 只讀屬性不會序列化(但是,只讀集合屬性的內容進行序列化)。
- 類和成員名稱是用XML類聲明中顯示完成相同的。
- 使用默認XML命名空間。
如果需要更好地控制序列化,您可以修飾的類DataContract屬性。 當存在此屬性時,類被序列,如下所示:
- "選擇加入"方法: 屬性和字段,默認情況下不序列化。 要序列化的屬性或字段,給它裝飾DataMember屬性。
- 要序列化的私有或受保護成員,給它裝飾DataMember屬性。
- 只讀屬性不會序列化。
- 若要更改類名稱在 XML 中的顯示方式,請設置名稱中的參數DataContract屬性。
- 若要更改成員名稱在 XML 中的顯示方式,請設置名稱中的參數DataMember屬性。
- 若要更改的 XML 命名空間,請設置Namespace中的參數DataContract類。
刪除JSON或XML格式化程序
如果您不想要使用它們,可以從列表中的格式化程序,刪除 JSON 格式化程序或 XML 格式化程序。 若要執行此操作的主要原因是:
- 若要限制為特定媒體類型將 web API 響應。 例如,你可能決定以支持僅將 JSON 響應,並刪除 XML 格式化程序。
- 若要使用自定義格式化程序替換默認的格式化程序。 例如,可以使用您自己的 JSON 格式化程序的自定義實現來替換 JSON 格式化程序。
下面的代碼演示如何刪除默認格式化程序。 調用此成員,從你應用程序_啟動Global.asax 中定義的方法。
void ConfigureApi(HttpConfiguration config) { // Remove the JSON formatter config.Formatters.Remove(config.Formatters.JsonFormatter); // or // Remove the XML formatter config.Formatters.Remove(config.Formatters.XmlFormatter); }
ASP.NET Web API 中的模型驗證
當客戶端將數據發送到 web API 時,通常要執行任何處理之前驗證數據。
數據注釋
在 ASP.NET Web API 中,可以使用中的屬性System.ComponentModel.DataAnnotations命名空間來對模型設置屬性的驗證規則。 請考慮以下模型:
using System.ComponentModel.DataAnnotations; namespace MyApi.Models { public class Product { public int Id { get; set; } [Required] public string Name { get; set; } public decimal Price { get; set; } [Range(0, 999)] public double Weight { get; set; } } }
如果已在 ASP.NET MVC 中使用模型驗證,這應該很熟悉。 Required屬性指出Name
屬性不能為 null。 Range屬性指出Weight
必須是介於 0 和 999 之間。
假設客戶端發送 POST 請求,其中以下 JSON 表示形式:
{ "Id":4, "Price":2.99, "Weight":5 }
您可以看到客戶端未包括Name
屬性,它被標記為Required。 當 Web API 將轉換為 JSONProduct
實例,它會驗證Product
針對驗證特性。 在你的控制器操作,可以檢查模型是否有效:
using MyApi.Models; using System.Net; using System.Net.Http; using System.Web.Http; namespace MyApi.Controllers { public class ProductsController : ApiController { public HttpResponseMessage Post(Product product) { if (ModelState.IsValid) { // Do something with the product (not shown). return new HttpResponseMessage(HttpStatusCode.OK); } else { return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } } } }
可以創建一個Action篩選器來調用控制器操作前的模型檢查:
using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using System.Web.Http.ModelBinding; namespace MyApi.Filters { public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { if (actionContext.ModelState.IsValid == false) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, actionContext.ModelState); } } } }
如果模型驗證失敗,此篩選器將返回 HTTP 響應,其中包含驗證錯誤。 在這種情況下,不會調用控制器操作。
若要將此篩選器應用於所有的 Web API 控制器,將添加到篩選器的實例HttpConfiguration.Filters在配置期間的集合:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Filters.Add(new ValidateModelAttribute()); // ... } }
另一個選項是設置篩選器作為特性上各個控制器或控制器操作:
public class ProductsController : ApiController { [ValidateModel] public HttpResponseMessage Post(Product product) { // ... } }
ASP.NET Web API 中的參數綁定
Web API 在控制器上調用方法時,必須設置參數的值,此過程稱為“綁定”。
默認情況下,Web API 使用以下規則進行參數綁定:
- 如果參數為“簡單”類型,Web API 會嘗試從 URI 中獲取值。 簡單類型包括 .NET基元類型(int、 bool、 double等),以及 TimeSpan、DateTime、Guid、decimal 和 string,此外還有**借助類型轉換器即可從字符串進行轉換的類型。 (稍后介紹有關類型轉換器的詳細信息。)
- 對於復雜類型,Web API 嘗試使用媒體類型格式化程序從消息正文中讀取值。
例如,下面是典型的 Web API 控制器方法:
HttpResponseMessage Put(int id, Product item) { ... }
Id 參數是"簡單"類型,因此 Web API 嘗試從請求 URI 中獲取值。 item 參數是復雜類型,因此 Web API 使用媒體類型格式化程序從請求正文中讀取值。
使用 [FromUri]
若要強制 Web API 從 URI 讀取復雜類型,請向參數添加 [FromUri] 特性。 下面的示例定義 GeoPoint
類型,以及從 URI 獲取 GeoPoint
的控制器方法。
public class GeoPoint { public double Latitude { get; set; } public double Longitude { get; set; } } public ValuesController : ApiController { public HttpResponseMessage Get([FromUri] GeoPoint location) { ... } }
使用 [FromBody]
若要強制 Web API 從請求正文讀取簡單類型,請向參數添加 [FromBody] 特性:
public HttpResponseMessage Post([FromBody] string name) { ... }
ASP.NET Web API 中的異常處理
HttpResponseException
如果 Web API 控制器引發未捕獲的異常,則會發生什么情況? 默認情況下,大多數異常將轉換為狀態代碼 500 內部服務器錯誤的 HTTP 響應。
HttpResponseException類型是一種特殊情況。 此異常將返回異常構造函數中指定任何 HTTP 狀態代碼。 例如,以下方法將返回 404,找不到,如果id參數無效。
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; }
異常篩選器
您可以自定義 Web API 通過編寫處理異常的方式異常篩選器。 當控制器方法引發任何未處理的異常時執行異常篩選器,除了 HttpResponseException異常。 HttpResponseException類型是一種特殊情況,因為它專為返回的 HTTP 響應。
異常篩選器實現System.Web.Http.Filters.IExceptionFilter接口。 編寫異常篩選器的最簡單方法是從派生System.Web.Http.Filters.ExceptionFilterAttribute類並重寫OnException方法。
下面是將轉換的篩選器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對象包含將發送到客戶端的 HTTP 響應消息。
注冊異常篩選器
有幾種方法來注冊 Web API 異常篩選器:
- 由Action
- 由控制器
- 全局
篩選器應用到特定的Action,請將作為屬性的篩選器添加到操作:
public class ProductsController : ApiController { [NotImplExceptionFilter] public Contact GetContact(int id) { throw new NotImplementedException("This method is not implemented"); } }
篩選器應用到所有控制器上的操作,將篩選器作為屬性添加到控制器類:
[NotImplExceptionFilter] public class ProductsController : ApiController { // ... }
若要對 Web API 的所有控制器全局應用篩選器,將添加到篩選器的實例GlobalConfiguration.Configuration.Filters集合。 在此集合中的異常篩選器適用於任何 Web API 控制器操作。
GlobalConfiguration.Configuration.Filters.Add( new ProductStore.NotImplExceptionFilterAttribute());
如果使用"ASP.NET MVC 4 Web 應用程序"項目模板來創建你的項目,將 Web API 配置代碼內的放WebApiConfig
類,該類在應用程序位於_開始文件夾:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute()); // Other configuration code... } }
HttpError
HttpError對象提供一致的方法來響應正文中返回的錯誤信息。 下面的示例演示如何返回 HTTP 狀態代碼 404 (找不到) 與HttpError響應正文中。
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實例,然后創建HttpResponseMessage ,其中包含HttpError.
HttpError 和模型驗證
public HttpResponseMessage PostProduct(Product item) { if (!ModelState.IsValid) { return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } // Implementation not shown... }
此示例中可能會返回以下響應:
HTTP/1.1 400 Bad Request Content-Type: application/json; charset=utf-8 Content-Length: 320 { "Message": "The request is invalid.", "ModelState": { "item": [ "Required property 'Name' not found in JSON. Path '', line 1, position 14." ], "item.Name": [ "The Name field is required." ], "item.Price": [ "The field Price must be between 0 and 999." ] } }
使用 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; } }
全局錯誤處理 ASP.NET Web API 2 中
如今,在Web API中沒有簡單的方式記錄或處理全局錯誤。可以通過異常篩選器過濾某些未經處理的異常,但也有一部分異常篩選器不能處理。例如:
- 從控制器的構造函數引發的異常。
- 從消息處理程序引發的異常。
- 在路由期間引發的異常。
- 響應內容序列化期間引發的異常。
我們可以使用異常篩選器,消息處理程序(后面會提到),但是它們都很難處理上面的問題,所以微軟提供了兩個類:IExceptionLogger和IExceptionHandler。
我們該怎么使用:
- IExceptionLogger用於查看WebAPI捕獲到的所有未經過處理的異常。
- IExceptionHandler用於自定義所有可能的響應未經處理WebAPI捕獲的異常。
- IExceptionFilter是Controller或者Action下捕捉未經處理的異常的最簡單的解決方案。
public interface IExceptionLogger { Task LogAsync(ExceptionLoggerContext context, CancellationToken cancellationToken); } public interface IExceptionHandler { Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken); }