/ dotnet

ASP.NET Web API 2 OData

开放数据协议 OData 是 web 数据访问协议,OData 提供了一套统一的方式对数据库 CRUD 的操作。
本文主要介绍基础配置方法和记录实际使用遇到的一些问题。

Install the OData Packages

Install-Package Microsoft.AspNet.Odata

Add a Model Class

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }
    }
}

Enable Entity Framework

OData 不需要依赖 EF ,你也可以通过其他方式读写数据库。

Install-Package EntityFramework

Web.config配置就不用多说了。

using System.Data.Entity;
namespace ProductService.Models
{
    public class ProductsContext : DbContext
    {
        public ProductsContext() 
                : base("name=ProductsContext")
        {
        }
        public DbSet<Product> Products { get; set; }
    }
}

Configure the OData Endpoint

_Start/WebApiConfig.cs中添加:

using ProductService.Models;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // New code:
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());
    }
}

以上注册方法的意义是:

  • 创建实体数据模型 (EDM)。
  • 添加一个路由。
    EDM 是一个抽象模型的数据。 EDM 用于创建服务元数据文档。

OData Controller

using ProductService.Models;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.OData;
namespace ProductService.Controllers
{
    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 Web API 就几分钟就配置好了。

增删改查

  • 查询

[EnableQuery]
public IQueryable<Product> Get()
{
    return db.Products;
}
[EnableQuery]
public SingleResult<Product> Get([FromODataUri] int key)
{
    IQueryable<Product> result = db.Products.Where(p => p.Id == key);
    return SingleResult.Create(result);
}
  • 增加

public async Task<IHttpActionResult> Post(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Created(product);
}
  • 更新

OData 支持用于更新实体,修补程序和 PUT 的两个不同的语义。
修补程序执行部分更新。 客户端指定只是要更新的属性。

  • PUT 会替换整个实体。
  • PUT 的缺点是客户端必须发送实体,包括未更改的值中的所有属性的值。 OData 规范规定修补程序是首选。
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);
}
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    if (key != update.Id)
    {
        return BadRequest();
    }
    db.Entry(update).State = EntityState.Modified;
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(update);
}
  • 删除

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);
}

复杂查询

具体的查询方法可以参考下面给出的文档,OData 的语法非常丰富,可以通过写灵活的参数配置来省去服务器控制器端的一些工作。
举个例子:

SELECT status, COUNT(DISTINCT id) AS total
FROM Delivery
GROUP BY status

用 OData 获取就是

/odata/deliveries?$apply=groupby((status),aggregate(id with countdistinct as total))

在使用某些查询的时候会出现迷之 40X 报错:

Now the default setting for WebAPI OData is : client can't apply $count, $orderby, $select, $top, $expand, $filter in the query, query like localhost\odata\Customers?$orderby=Name will failed as BadRequest, because all properties are not sort-able by default, this is a breaking change in 6.0.0,

只是 6.0.0 的异常内容。

修正

  • _Start/WebApiConfig.cs 中加上这句话:
config.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
  • 或者单独给对应的实体后面加
builder.EntityType<Product>().Filter("Id");

Reference

Supporting OData v4 in ASP.NET Web API
How to Use Web API OData to Build an OData V4 Service without Entity Framework
OData Extension for Data Aggregation Version 4.0
OData 無法使用 $count, $orderby, $select, $top, $expand, $filter
OData v4 groupby with $count