本篇体验实现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 ICollectionProducts { 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("Products");//创建EntityDataModel(EDM) builder.EntitySet ("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 IQueryableGet(){ return db.Products;}
当为某个action配置上[EnableQuery]特性后,就支持OData查询了。
● 根据Product的主键查询
[EnableQuery]public SingleResultGet([FromODataUri] int key){ IQueryable 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 SingleResultGetSupplier([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 TaskPost(Product product){ if(!ModelState.IsValid) { return BadRequest(ModelState); } db.Products.Add(product); await db.SaveChangesAsync(); return Created(product);}
以上,首先是验证,然后是添加,最后把新添加的Product放在Create方法中返回给前端。
● Product的部分更新
public async TaskPatch([FromODataUri] int key, Delta 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 TaskPut([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 TaskDelete([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的实体关系
////// 创建Product与Supplier的关系/// 如果为Product.Supplier创建关系,使用PUT请求/// 如果为Supplier.Products创建关系,使用POST请求/// /// Product的主键/// Product的导航属性/// ///[AcceptVerbs("POST", "PUT")]public async Task 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 (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(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 ()); var odataPath = request.ODataProperties().PathHandler.Parse( request.ODataProperties().Model, serviceRoot, uri.LocalPath); var keySegment = odataPath.Segments.OfType ().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的实体关系
////// 删除Product与Supplier的关系/// /// Product主键/// Product的导航属性/// Suppliers(1)的所在地址///[HttpDelete]public async Task 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 IQueryableGetProducts([FromODataUri] int key) { return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products); } [EnableQuery] public IQueryable Get() { return db.Suppliers; } [EnableQuery] public SingleResult Get([FromODataUri] int key) { IQueryable result = db.Suppliers.Where(s => s.Id == key); return SingleResult.Create(result); } /// /// 删除某个Supplier与某个Product之间的关系 /// DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1) /// /// Supplier的主键 /// Product的主键字符串 /// Supplier的导航属性 ///[HttpDelete] public async Task 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); }}