什么是OData

开放数据协议(Open Data Protocol,简称OData)是一种描述如何创建和访问Restful服务的OASIS标准。该标准由微软发起 [1] ,前三个版本1.0、2.0、3.0都是微软开放标准,遵循微软开放规范承诺书(Microsoft Open Specification Promise)。第四个版本4.0于2014年3月17日在OASIS投票通过成为开放工业标准

上面是百度的答案,我们可以很清楚的知道这是一个协议

使用OData开发

以.NetCoreAPI为例,创建一个API项目

添加ASP.NET Core ODataMicrosoft.EntityFrameworkCore.InMemoryNuget程序包

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>0d1b65c3-336b-4511-be78-68c012362630</UserSecretsId>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OData" Version="7.4.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.5" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
  </ItemGroup>

</Project>

ASP.NET Core OData程序包是为了更方便的去开发而引入的

Microsoft.EntityFrameworkCore.InMemory则方便数据引入

添加Model类

// Book
public class Book
{
  public int Id { get; set; }
  public string ISBN { get; set; }
  public string Title { get; set; }
  public string Author { get; set; }
  public decimal Price { get; set; }
  public Address Location { get; set; }
  public Press Press { get; set; }
}

// Press
public class Press
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string Email { get; set; }
  public Category Category { get; set; }
}

// Category
public enum Category
{
  Book,
  Magazine,
  EBook
}

// Address
public class Address
{
  public string City { get; set; }
  public string Street { get; set; }
}

在这里:

  • Book,Press将作为实体类型
  • Address将用作“复杂”类型。
  • Category将用作枚举类型。

建立EDM(Entity Data Model)模型

OData基于实体数据模型(EDM)来描述数据结构,在ASP.Net Core OData中,可以轻松的基于上述模型建立,在Startup类末尾加一个私有方法

private static IEdmModel GetEdmModel()
{
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<Book>("Books");
    builder.EntitySet<Press>("Presses");
    return builder.GetEdmModel();
}

在这里我们定义了两个名为BooksPresses 的实体集

通过依赖注入注册服务

ASP.NET Core OData需要预先注册一些服务才能提供其功能,该库提供了一个名为AddOData()的扩展方法,以通过内置的依赖项注入来注册所需要的OData服务,因此,修改如下代码到ConfigureServices中

public class Startup
{
  	// ...

      public void ConfigureServices(IServiceCollection services)
      {
            services.AddOData();
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication1", Version = "v1" });
            });
      }
}

注册OData端点

我们需要添加OData路由来注册OData端点,并调用GetEdmModel()绑定EDM模型到端点

public class Startup
{
  	// ...

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication1 v1"));
        }

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapODataRoute("odata", "odata", GetEdmModel());
        });
    }
}

8.0版本无需在Configure中注册:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<BookStoreContext>(opt => opt.UseInMemoryDatabase("BookLists"));
        services.AddControllers();
        services.AddOData(opt => opt.AddModel("odata", GetEdmModel()));
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

    private static IEdmModel GetEdmModel()
    {
        // …
    }
}

查询元数据

OData服务已经准备就绪,可以运行,并且提供基本的功能,例如查询元数据(EDM的XML表示),构建并允许项目,我们任何客户端工具发出请求:

GET http://localhost:5000/odata/$metadata

然后,可以获取如下xml所示的元数据

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
    <edmx:DataServices>
        <Schema Namespace="BookStore.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Book">
                <Key>
                    <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="ISBN" Type="Edm.String" />
                <Property Name="Title" Type="Edm.String" />
                <Property Name="Author" Type="Edm.String" />
                <Property Name="Price" Type="Edm.Decimal" Nullable="false" />
                <Property Name="Location" Type="BookStore.Models.Address" />
                <NavigationProperty Name="Press" Type="BookStore.Models.Press" />
            </EntityType>
            <EntityType Name="Press">
                <Key>
                    <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="Name" Type="Edm.String" />
                <Property Name="Email" Type="Edm.String" />
                <Property Name="Category" Type="BookStore.Models.Category" Nullable="false" />
            </EntityType>
            <ComplexType Name="Address">
                <Property Name="City" Type="Edm.String" />
                <Property Name="Street" Type="Edm.String" />
            </ComplexType>
            <EnumType Name="Category">
                <Member Name="Book" Value="0" />
                <Member Name="Magazine" Value="1" />
                <Member Name="EBook" Value="2" />
            </EnumType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityContainer Name="Container">
                <EntitySet Name="Books" EntityType="BookStore.Models.Book">
                    <NavigationPropertyBinding Path="Press" Target="Presses" />
                </EntitySet>
                <EntitySet Name="Presses" EntityType="BookStore.Models.Press" />
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

创建数据库上下文

现在,可以添加更多实际的功能,首先将数据库上下文引入Web程序,在Models文件夹中创建BookStoreContext

public class BookStoreContext : DbContext
{
    public BookStoreContext(DbContextOptions<BookStoreContext> options)
      : base(options)
    {
    }

    public DbSet<Book> Books { get; set; }
    public DbSet<Press> Presses { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>().OwnsOne(c => c.Location);
    }
}

OnModelCreating中的代码将Address映射为复杂类型

注册数据库上下文

通过依赖注入在服务中注册数据库上下文,求改Startup类中的ConfigureServices方法

public class Startup
{
  	// ...

      public void ConfigureServices(IServiceCollection services)
      {
            services.AddDbContext<BookStoreContext>(opt => opt.UseInMemoryDatabase("BookLists"));
            services.AddOData();
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication1", Version = "v1" });
            });
      }
}

模型数据

为了简单起见,我们简单生成一些内容

public static class DataSource
{
    private static IList<Book> _books { get; set; }

    public static IList<Book> GetBooks()
    {
        if (_books != null)
        {
            return _books;
        }

        _books = new List<Book>();

        // book #1
        Book book = new Book
        {
            Id = 1,
            ISBN = "978-0-321-87758-1",
            Title = "Essential C#5.0",
            Author = "Mark Michaelis",
            Price = 59.99m,
            Location = new Address { City = "Redmond", Street = "156TH AVE NE" },
            Press = new Press
            {
                Id = 1,
                Name = "Addison-Wesley",
                Category = Category.Book
            }
        };
        _books.Add(book);

        // book #2
        book = new Book
        {
            Id = 2,
            ISBN = "063-6-920-02371-5",
            Title = "Enterprise Games",
            Author = "Michael Hugos",
            Price = 49.99m,
            Location = new Address { City = "Bellevue", Street = "Main ST" },
            Press = new Press
            {
                Id = 2,
                Name = "O'Reilly",
                Category = Category.EBook,
            }
        };
        _books.Add(book);

        return _books;
    }
}

操作资源

构建Controller

创建一个Controller命名为BooksController

[ApiController]
[Route("[controller]")]
public class BooksController : ODataController
{
    private BookStoreContext _db;

    public BooksController(BookStoreContext context)
    {
        _db = context;
        if (context.Books.Count() == 0)
        {
            foreach (var b in DataSource.GetBooks())
            {
                context.Books.Add(b);
                context.Presses.Add(b.Press);
            }
            context.SaveChanges();
        }
    }

    [EnableQuery]
    public IActionResult Get()
    {
        return Ok(_db.Books);
    }

    [EnableQuery]
    public IActionResult Get([FromODataUri] int key)
    {
        return Ok(_db.Books.FirstOrDefault(c => c.Id == key));
    }
}

在上面定义的两个Action中:

  1. Get()返回所有内容
  2. Get(int Key)返回指定ID内容

检索资源

我们访问:GET http://localhost:5000/odata/Books 响应结果为:

{
    "@odata.context": "http://localhost:5000/odata/$metadata#Books",
    "value": [
        {
            "Id": 1,
            "ISBN": "978-0-321-87758-1",
            "Title": "Essential C#5.0",
            "Author": "Mark Michaelis",
            "Price": 59.99,
            "Location": {
                "City": "Redmond",
                "Street": "156TH AVE NE"
            }
        },
        {
            "Id": 2,
            "ISBN": "063-6-920-02371-5",
            "Title": "Enterprise Games",
            "Author": "Michael Hugos",
            "Price": 49.99,
            "Location": {
                "City": "Bellevue",
                "Street": "Main ST"
            }
        }
    ]
}

尝试发出:GET http://localhost:5000/odata/Books(2) 获取ID为2的书籍

创建资源

添加下列代码到Controller

[EnableQuery]
public IActionResult Post([FromBody] Book book)
{
    _db.Books.Add(book);
    _db.SaveChanges();
    return Created(book);
}

发出如下请求创建Book数据

*POST* http://localhost:5000/odata/Books

Content-Type: application/json

Content:

{
  "Id":3,"ISBN":"82-917-7192-5","Title":"Hary Potter","Author":"J. K. Rowling",
  "Price":199.99,
  "Location":{
     "City":"Shanghai",
     "Street":"Zhongshan RD"
   }
}

查询资源

需要在Startup.cs中添加如下代码启用所有OData查询选项,例如$filter,$orderby,$expand等。

public class Startup
{
  // ...
  
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication1 v1"));
        }
        
        //...

        app.UseEndpoints(endpoints =>
        {
            endpoints.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
            endpoints.MapODataRoute("odata", "odata", GetEdmModel());
        });
    }
}

基于$filter的示例:

GET http://localhost:5000/odata/Books?$filter=Price le 50

响应内容为:

{
    "@odata.context": "https://localhost:5001/odata/$metadata#Books",
    "value": [
        {
            "Id": 2,
            "ISBN": "063-6-920-02371-5",
            "Title": "Enterprise Games",
            "Author": "Michael Hugos",
            "Price": 49.99,
            "Location": {
                "City": "Bellevue",
                "Street": "Main ST"
            }
        }
    ]
}

它还支持更为复杂的查询选项,例如:

GET http://localhost:5000/odata/Books?$filter=Price le 50&$expand=Press($select=Name)&$select=ISBN

响应结果为:

{
    "@odata.context": "http://localhost:5000/odata/$metadata#Books(ISBN,Press(Name))",
    "value": [
        {
            "ISBN": "063-6-920-02371-5",
            "Press": {
                "Name": "O'Reilly"
            }
        }
    ]
}

更多的查询方式可以在官方网站查看,这些都是基于OData协议进行的

如果在Swagger中显示OData路由

使用NuGet安装OData.Swagger程序包

打开Startup.cs文件,在ConfigureService方法中添加如下服务

services.AddOdataSwaggerSupport();

重新启动程序即可

如果你的控制器是继承自ODataController,则需要添加[ApiExplorerSettings(IgnoreApi = false)]

[ApiController]
   [ApiExplorerSettings(IgnoreApi = false)]
   [Route("[controller]")]
public class BooksController : ODataController
   {
       //...
   }

本文参考:

OData参考:https://devblogs.microsoft.com/odata/asp-net-core-odata-now-available/

OData参考:https://devblogs.microsoft.com/odata/asp-net-odata-8-0-preview-for-net-5/

在Swagger中显示OData参考:https://github.com/KishorNaik/Sol_OData_Swagger_Support

OData官网:https://www.odata.org/