本篇體驗實現ASP.NET Web API基於OData的增刪改查,以及處理實體間的關系。
首先是比較典型的一對多關系,Supplier和Product。
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Category { get; set; } [ForeignKey("Supplier")] public int? SupplierId { get; set; } public virtual Supplier Supplier { get; set; } } public class Supplier { public int Id { get; set; } public string Name { get; set; } public ICollection<Product> Products { get; set; } }
Product有一個針對Supplier的外鍵SupplierId,可以為null。
Entity Framework的配置部分略去。
在WebApiConfig中有關OData的部分配置如下:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 配置和服務 // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); //有關OData //使用ODataConventionModelBuilder創建EDM使用了一些慣例 //如果要對創建EDM有更多的控制,使用ODataModelBuilder ODataModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Product>("Products");//創建EntityDataModel(EDM) builder.EntitySet<Supplier>("Suppliers"); config.MapODataServiceRoute( routeName: "ODataRoute", routePrefix: "odata", model:builder.GetEdmModel()); } }
有關ProductsController
public class ProductsController : ODataController { ProductsContext db = new ProductsContext(); private bool ProductExists(int key) { return db.Products.Any(p => p.Id == key); } protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); } ... }
和OData相關的,都要繼承ODataController這個基類。
● 獲取所有
[EnableQuery] public IQueryable<Product> Get() { return db.Products; }
當為某個action配置上[EnableQuery]特性后,就支持OData查詢了。
● 根據Product的主鍵查詢
[EnableQuery] public SingleResult<Product> Get([FromODataUri] int key) { IQueryable<Product> query = db.Products.Where(p => p.Id == key); return SingleResult.Create(query); }
→[FromODataUri] int key中的key值可以從如下uri中獲取:
GET http://localhost:63372/odata/Prodducts(11)
以上的11將賦值給key。
→ SingleResult可以接受0個或1個Entity。
● 根據Product的主鍵獲取其導航屬性Supplier
//GET /Products(1)/Supplier //相當於獲取Poduct的導航屬性Supplier //GetSupplier中的Supplier是導航屬性的名稱,GetSupplier和key的寫法都符合慣例 //[EnableQuery(AllowedQueryOptions =System.Web.OData.Query.AllowedQueryOptions.All)] [EnableQuery] public SingleResult<Supplier> GetSupplier([FromODataUri] int key) { var result = db.Products.Where(p => p.Id == key).Select(m => m.Supplier); return SingleResult.Create(result); }
以上,GetSupplier的語法符合慣例,Supplier和Product的導航屬性名稱保持一致。
● 添加Product
public async Task<IHttpActionResult> Post(Product product) { if(!ModelState.IsValid) { return BadRequest(ModelState); } db.Products.Add(product); await db.SaveChangesAsync(); return Created(product); }
以上,首先是驗證,然后是添加,最后把新添加的Product放在Create方法中返回給前端。
● Product的部分更新
public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product) { if(!ModelState.IsValid) { return BadRequest(ModelState); } var entity = await db.Products.FindAsync(key); if (entity == null) { return NotFound(); } product.Patch(entity); try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if(!ProductExists(key)) { return NotFound(); } else { throw; } } return Updated(entity); }
以上,Delta<Product>這個泛型類可以追蹤Product的變化,最后使用其實例方法Patch把變化告知實體entity, Patch成功就把Product放在Updated方法中返回給前端。
● 更新Product
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product product) { if(!ModelState.IsValid) { return BadRequest(ModelState); } if(key != product.Id) { return BadRequest(); } db.Entry(product).State = System.Data.Entity.EntityState.Modified; try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProductExists(key)) { return NotFound(); } else { throw; } } return Updated(product); }
這里,首先判斷實體的ModelState,然后判斷從前端傳來的Product主鍵key是否和前端傳來的Product的主鍵相等,在處理Entity Framwork單元提交變化的時候catch一個DbUpdateConcurrencyException異常,防止在更新的時候該Product剛好被刪除掉。最終,也把Product放在Updated方法返回給前端。
● 刪除Product
public async Task<IHttpActionResult> Delete([FromODataUri] int key) { var product = await db.Products.FindAsync(key); if(product==null) { return NotFound(); } db.Products.Remove(product); await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); }
● 創建Product與Supplier的實體關系
/// <summary> /// 創建Product與Supplier的關系 /// 如果為Product.Supplier創建關系,使用PUT請求 /// 如果為Supplier.Products創建關系,使用POST請求 /// </summary> /// <param name="key">Product的主鍵</param> /// <param name="navigationProperty">Product的導航屬性</param> /// <param name="link"></param> /// <returns></returns> [AcceptVerbs("POST", "PUT")] public async Task<IHttpActionResult> CreateRef([FromODataUri] int key, string navigationProperty, [FromBody] Uri link) { //現保證Product是存在的 var product = db.Products.SingleOrDefault(p => p.Id == key); if (product == null) return NotFound(); switch(navigationProperty) { case "Supplier": //獲取Supplier的主鍵 var supplierId = Helpers.GetKeyFromUri<int>(Request, link); var supplier = db.Suppliers.SingleOrDefault(s => s.Id == supplierId); if (supplier == null) return NotFound(); product.Supplier = supplier; break; default: return StatusCode(HttpStatusCode.NotImplemented); } await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); }
以上,如果創建Product的Supplier關系,就使用PUT請求,如果創建Supplier的Products關系,就使用POST請求。
前端發出PUT請求,uri為:http://localhost:54714/odata/Products(1)/Supplier/$ref
意思是說需要為編號為1的Product創建一個Supplier。
需要創建的Supplier來自哪里呢?需要從前端的body中傳遞過來,格式如下:
{"@odata.id":"http://localhost:54714/odata/Suppliers(2)"}
在CreateRef方法中,形參key用來接收這里的Product主鍵1, 形參navigationProperty用來接收Supplier,形參link用來接收來自body的有關一個具體Supplier的完整uri,即http://localhost:54714/odata/Suppliers(2)。
$ref放在Products(1)/Supplier/之后,表示現在處理的是編號為1的Product和某個Supplier之間的關系。
Helpers.GetKeyFromUri<int>方法用來取出http://localhost:54714/odata/Suppliers(2)中某個Supplier的主鍵2。
Helpers.GetKeyFromUri<T>方法如下:
//把uri split成segment,找到key的鍵值,並轉換成合適的類型 public static class Helpers { public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri) { if (uri == null) { throw new ArgumentNullException("uri"); } var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request); string serviceRoot = urlHelper.CreateODataLink( request.ODataProperties().RouteName, request.ODataProperties().PathHandler, new List<ODataPathSegment>()); var odataPath = request.ODataProperties().PathHandler.Parse( request.ODataProperties().Model, serviceRoot, uri.LocalPath); var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().FirstOrDefault(); if (keySegment == null) { throw new InvalidOperationException("The link does not contain a key."); } var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, Microsoft.OData.Core.ODataVersion.V4); return (TKey)value; } }
● 刪除Product與Supplier的實體關系
/// <summary> /// 刪除Product與Supplier的關系 /// </summary> /// <param name="key">Product主鍵</param> /// <param name="navigationProperty">Product的導航屬性</param> /// <param name="link">Suppliers(1)的所在地址</param> /// <returns></returns> [HttpDelete] public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, string navigationProperty, [FromBody] Uri link) { var product = db.Products.SingleOrDefault(p => p.Id == key); if (product == null) return NotFound(); switch(navigationProperty) { case "Supplier": product.Supplier = null; break; default: return StatusCode(HttpStatusCode.NotImplemented); } await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); }
前端發出DELETE請求:http://localhost:54714/odata/Products(1)/Supplier/$ref
DeleteRef方法中,形參key用來接收Product的主鍵1,形參navigationProperty用來接收Supplier。
SuppliersController,與Product類似
public class SuppliersController : ODataController { ProductsContext db = new ProductsContext(); [EnableQuery] public IQueryable<Product> GetProducts([FromODataUri] int key) { return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products); } [EnableQuery] public IQueryable<Supplier> Get() { return db.Suppliers; } [EnableQuery] public SingleResult<Supplier> Get([FromODataUri] int key) { IQueryable<Supplier> result = db.Suppliers.Where(s => s.Id == key); return SingleResult.Create(result); } /// <summary> /// 刪除某個Supplier與某個Product之間的關系 /// DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1) /// </summary> /// <param name="key">Supplier的主鍵</param> /// <param name="relatedKey">Product的主鍵字符串</param> /// <param name="navigationProperty">Supplier的導航屬性</param> /// <returns></returns> [HttpDelete] public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, [FromODataUri] string relatedKey, string navigationProperty) { var supplier = db.Suppliers.SingleOrDefault(p => p.Id == key); if (supplier == null) return NotFound(); switch(navigationProperty) { case "Products": var productId = Convert.ToInt32(relatedKey); var product = db.Products.SingleOrDefault(p => p.Id == productId); if (product == null) return NotFound(); product.Supplier = null; break; default: return StatusCode(HttpStatusCode.NotImplemented); } await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); } protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); } }