本篇体验实现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); } }