.NET 中 ODate 协议介绍
OData(Open Data Protocol)
是一个开放的 Web
协议,用于查询和更新数据。在 .NET
生态系统中,OData
被广泛支持和使用。
主要特性
1. 统一的数据访问方式
- 提供标准化的查询语法
- 支持
CRUD
操作 - 支持元数据描述
2. 查询能力
标准查询选项支持:
- 过滤数据(
$filter
) - 排序(
$orderby
) - 分页(
$top, $skip
) - 投影,选择字段(
$select
) - 扩展关联数据(
$expand
)
.NET 9
增强功能:
- 性能改进:查询执行和序列化性能优化
- 更好的路由集成:与
ASP.NET Core
路由系统更紧密集成 - 端点(
Endpoint
)路由支持:完全支持现代端点路由范式
3. 格式支持
JSON
格式(默认,推荐)XML
格式Atom
格式
版本支持
.NET
支持多个 OData
版本:
OData v4
(当前最新版本,推荐使用)OData v3
(较旧版本)
安全考虑
- 支持授权和认证
- 查询复杂度限制
- 防止拒绝服务攻击的机制
最佳实践
- 正确设置查询限制(
SetMaxTop
) - 使用
EnableQuery
特性控制查询行为 - 实现适当的错误处理
- 考虑使用
DTO
避免暴露内部模型 - 启用适当的缓存策略
优势
- 标准化:遵循开放标准,便于与其他系统集成
- 灵活性:客户端可以构建复杂的查询
- 性能优化:支持服务端(
SSE
,ef core
服务端评估)分页和投影字段选择(CSE
,ef core
客户端评估) - 工具支持:
Visual Studio
等工具提供良好支持
OData
遵循的国际标准:
- 核心标准
### OASIS 标准
- **OData Version 4.0**: 由 OASIS 组织发布的开放标准
- **OData JSON Format Version 4.0**: 定义 JSON 格式的数据交换规范
- **OData Common Schema Definition Language (CSDL) Version 4.0**: 实体数据模型定义语言
### ISO/IEC 标准
- **ISO/IEC 20802-1:2016**: Information technology - Open Data Protocol (OData) - Part 1: Core
- **ISO/IEC 20802-2:2016**: Information technology - Open Data Protocol (OData) - Part 2: URL Conventions
- **ISO/IEC 20802-3:2016**: Information technology - Open Data Protocol (OData) - Part 3: Common Schema Definition Language (CSDL)
- 相关
Web
标准
### HTTP 标准
- **RFC 7231**: HTTP/1.1 Semantics and Content
- **RFC 7230-7237**: HTTP 协议系列标准
### URI 标准
- **RFC 3986**: Uniform Resource Identifier (URI): Generic Syntax
- 数据格式标准
- **ECMA-404**: The JSON Data Interchange Format
- **RFC 7493**: The I-JSON Message Format
- 其他相关标准
- **Atom Publishing Protocol (AtomPub)**: RFC 5023
- **OData Extension for Data Aggregation**: OASIS 标准扩展
这些标准确保了 OData
协议在全球范围内的互操作性和标准化实施。
使用场景
- 构建
RESTful API
- 数据分析和报表系统
- 移动应用后端服务
- 微服务间的数据交互
·OData
协议为 .NET
开发者提供了一种强大而标准化的方式来构建数据服务 API
。
实现方式
.csproj
项目添加 nuget
相关包:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="9.3.2" />
<PackageReference Include="Microsoft.OData.ModelBuilder" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.7" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
</ItemGroup>
</Project>
说明:此处使用
EF Core
内存模式,模拟DB
数据库,并添加OData
相关包。
目录结构
完整目录结构如下:
├─Controllers
├─Datatabase
│ ├─Entities
│ ├─Mappers
│ └─Repositories
├─Models
│ └─Dtos
├─Properties
└─Services
架构层次说明
这种实现保持了清晰的分层架构:
Controller 层:ProductsController
- 负责处理
HTTP
请求和响应 - 负责请求
DTO
的数据验证 - 使用
OData
特性进行查询支持 - 依赖服务层进行业务处理
Database 层:
- 数据库相关的核心数据访问层
- 包含实体、映射和仓储模式的实现
- Database/Entities
- 存放数据实体类,包含数据结构定义,通常对应数据库表结构
- 表示业务领域中的核心数据对象
- Database/Mappers
- 数据映射器,负责
Entity
(实体对象)与领域DTO
之间的转换
- 数据映射器,负责
- Database/Repositories
- 仓储模式实现,提供数据访问的抽象接口
- 封装数据访问逻辑,提供
CRUD
操作
Service 层:ProductService
- 实现业务逻辑
- 协调多个仓储操作
- 处理业务规则和验证
- 依赖仓储层进行数据访问
Models 层:领域模型
- 包含数据结构定义
- 领域
DTO
模型 - 业务领域层中间转换模型
这种架构模式的优势:
- 关注点分离:每层职责明确
- 可测试性:各层可以独立进行单元测试
- 可维护性:修改某一层不会(最小化)影响其他层
- 可扩展性:可以轻松添加新的业务逻辑或数据源
- 复用性:服务和仓储可以在多个控制器中复用
详细示例
- 创建数据库表实体模型
namespace ODataDemo.Database.Entities;
/// <summary>
/// 分类领域实体
/// </summary>
public class Category
{
public required string Id { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
public DateTime CreatedDate { get; set; }
public bool IsActive { get; set; }
// 导航属性
public ICollection<Product> Products { get; set; } = [];
// 领域方法
public void Activate() => IsActive = true;
public void Deactivate()
{
IsActive = false;
foreach (var product in Products)
{
product.UpdateStock(-product.StockQuantity); // 清空库存
}
}
public bool CanDelete() => !Products.Any(p => p.IsInStock());
}
/// <summary>
/// 产品领域实体
/// </summary>
public sealed class Product
{
public required string Id { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; }
// 导航属性
public string CategoryId { get; set; } = string.Empty;
public Category? Category { get; set; }
// 领域方法
public void UpdateStock(int quantity)
{
if (quantity < 0 && Math.Abs(quantity) > StockQuantity)
{
throw new InvalidOperationException("库存不足");
}
StockQuantity += quantity;
UpdatedDate = DateTime.UtcNow;
}
public bool IsInStock() => StockQuantity > 0;
public void ApplyDiscount(decimal discountPercentage)
{
if (discountPercentage < 0 || discountPercentage > 100)
{
throw new ArgumentException("折扣百分比必须在0-100之间");
}
Price = Price * (1 - discountPercentage / 100);
UpdatedDate = DateTime.UtcNow;
}
}
- 创建
DTO
对象
using System.Text.Json.Serialization;
namespace ODataDemo.Models.Dtos;
//#######################################
// 分类数据传输对象,用于 OData API
//#######################################
public class CategoryRequstDto
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("description")]
public required string Description { get; set; }
[JsonPropertyName("is_active")]
public bool IsActive { get; set; }
}
public sealed class CategoryResponeDto : CategoryRequstDto
{
[JsonPropertyName("id")]
public required string Id { get; set; }
[JsonPropertyName("created_date")]
public DateTime CreatedDate { get; set; }
[JsonPropertyName("product_count")]
public int ProductCount { get; set; }
}
//############################################
// 产品数据传输对象,用于 OData API
//############################################
public class ProductRequstDto
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("description")]
public required string Description { get; set; }
[JsonPropertyName("price")]
public decimal Price { get; set; }
[JsonPropertyName("stock_quantity")]
public int StockQuantity { get; set; }
[JsonPropertyName("category_id")]
public string CategoryId { get; set; } = string.Empty;
[JsonPropertyName("category_name")]
public string CategoryName { get; set; } = string.Empty;
[JsonPropertyName("is_in_stock")]
public bool IsInStock { get; set; }
}
public sealed class ProductResponeDto : ProductRequstDto
{
[JsonPropertyName("id")]
public required string Id { get; set; }
[JsonPropertyName("created_date")]
public DateTime CreatedDate { get; set; }
[JsonPropertyName("updated_date")]
public DateTime UpdatedDate { get; set; }
}
Entity
映射DTO
处理
using ODataDemo.Database.Entities;
using ODataDemo.Models.Dtos;
namespace ODataDemo.Database.Mappers;
public static class CategoryMapper
{
public static Category From(this CategoryRequstDto dto)
{
return new Category()
{
Id = Guid.CreateVersion7().ToString(),
Name = dto.Name,
Description = dto.Description,
CreatedDate = DateTime.UtcNow,
IsActive = dto.IsActive,
};
}
public static CategoryResponeDto ToModel(this Category entity)
{
return new CategoryResponeDto
{
Id = entity.Id,
Name = entity.Name,
Description = entity.Description,
IsActive = entity.IsActive,
CreatedDate = entity.CreatedDate,
ProductCount = entity.Products.Count,
};
}
}
public static class ProductMapper
{
public static Product From(this ProductRequstDto dto)
{
return new Product()
{
Id = Guid.CreateVersion7().ToString(),
Name = dto.Name,
Description = dto.Description,
Price = dto.Price,
StockQuantity = dto.StockQuantity,
CreatedDate = DateTime.UtcNow,
UpdatedDate = DateTime.UtcNow,
CategoryId = dto.CategoryId
};
}
public static ProductResponeDto ToModel(this Product entity)
{
return new ProductResponeDto
{
Id = entity.Id,
Name = entity.Name,
Description = entity.Description,
Price = entity.Price,
StockQuantity = entity.StockQuantity,
CreatedDate = entity.CreatedDate,
UpdatedDate = entity.UpdatedDate,
CategoryId = entity.CategoryId,
CategoryName = entity.Category?.Name ?? string.Empty,
IsInStock = entity.IsInStock()
};
}
}
- 定义仓储规范
说明:在仓储层,只出现数据库表对应的实体对象
Entity
。
using ODataDemo.Database.Entities;
namespace ODataDemo.Database.Repositories;
public interface IDataRepository
{
#region Product
IQueryable<Product> GetProducts();
Task<Product?> GetProductByIdAsync(string id);
Task<Product> AddProductAsync(Product product);
Task<Product> UpdateProductAsync(Product product);
Task DeleteProductAsync(string id);
Task<bool> ExistsProductAsync(string id);
#endregion
#region Category
IQueryable<Category> GetCategorys();
Task<Category?> GetCategoryByIdAsync(string id);
Task<Category> AddCategoryAsync(Category category);
Task<Category> UpdateCategoryAsync(Category category);
Task DeleteCategoryAsync(string id);
Task<bool> ExistsCategoryAsync(string id);
#endregion
}
- 定义服务规范
说明:服务层只出现
DTO
对象,在实现内部处理Entity
和DTO
的转换。
using ODataDemo.Models.Dtos;
namespace ODataDemo.Services;
public interface ICategoryService
{
IQueryable<CategoryResponeDto> GetAllCategories();
Task<CategoryResponeDto?> GetCategoryByIdAsync(string id);
Task<CategoryResponeDto> CreateCategoryAsync(CategoryRequstDto category);
Task<CategoryResponeDto> UpdateCategoryAsync(string id, CategoryRequstDto category);
Task DeleteCategoryAsync(string id);
Task ActivateCategoryAsync(string id);
Task DeactivateCategoryAsync(string id);
}
public interface IProductService
{
IQueryable<ProductResponeDto> GetProducts();
Task<ProductResponeDto?> GetProductByIdAsync(string id);
Task<ProductResponeDto> CreateProductAsync(ProductRequstDto dto);
Task<ProductResponeDto> UpdateProductAsync(string id, ProductRequstDto dto);
Task DeleteProductAsync(string id);
Task ApplyDiscountAsync(string productId, decimal discountPercentage);
Task UpdateStockAsync(string productId, int quantity);
}
- 配置
EDM
模型
说明:
EDM
配置是关键,更多信息请查看相关资料,篇幅有限不再详述。
相关资料:
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using ODataDemo.Models.Dtos;
namespace ODataDemo.Database;
public static class EdmModelConfig
{
// 配置 EDM 模型
public static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
// 只注册 DTO 类型
var productDto = builder.EntityType<ProductResponeDto>();
productDto.HasKey(p => p.Id);
productDto.Property(p => p.Name);
productDto.Property(p => p.Description);
productDto.Property(p => p.Price);
productDto.Property(p => p.StockQuantity);
productDto.Property(p => p.CategoryId);
productDto.Property(p => p.CreatedDate);
productDto.Property(p => p.IsInStock);
productDto.Property(p => p.CreatedDate);
productDto.Property(p => p.UpdatedDate);
var categoryDto = builder.EntityType<CategoryResponeDto>();
categoryDto.HasKey(c => c.Id);
categoryDto.Property(c => c.Name);
categoryDto.Property(c => c.Description);
categoryDto.Property(p => p.IsActive);
categoryDto.Property(p => p.CreatedDate);
categoryDto.Property(p => p.ProductCount);
// 使用 DTO 创建实体集
builder.EntitySet<ProductResponeDto>("Products");
builder.EntitySet<CategoryResponeDto>("Categories");
return builder.GetEdmModel();
}
}
- 配置
AppDbContext
说明:在此处添加一些初始化的种子数据。
using Microsoft.EntityFrameworkCore;
using ODataDemo.Database.Entities;
namespace ODataDemo.Database;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
string id1 = Guid.CreateVersion7().ToString();
string id2 = Guid.CreateVersion7().ToString();
string id3 = Guid.CreateVersion7().ToString();
// Seed data
modelBuilder.Entity<Category>().HasData(
new Category { Id = id1, Name = "Electronics", Description = "Electronic devices" },
new Category { Id = id2, Name = "Books", Description = "Books and literature" },
new Category { Id = id3.ToString(), Name = "Clothing", Description = "Apparel and accessories" }
);
modelBuilder.Entity<Product>().HasData(
new Product { Id = Guid.CreateVersion7().ToString(), Name = "Laptop", Price = 1200.00m, StockQuantity = 50, CategoryId = id1, Description = "High-performance laptop" },
new Product { Id = Guid.CreateVersion7().ToString(), Name = "Mouse", Price = 25.00m, StockQuantity = 100, CategoryId = id1, Description = "Wireless mouse" },
new Product { Id = Guid.CreateVersion7().ToString(), Name = "Keyboard", Price = 75.00m, StockQuantity = 75, CategoryId = id1, Description = "Mechanical keyboard" },
new Product { Id = Guid.CreateVersion7().ToString(), Name = "C# Programming Guide", Price = 45.00m, StockQuantity = 30, CategoryId = id2, Description = "Comprehensive C# guide" },
new Product { Id = Guid.CreateVersion7().ToString(), Name = "T-Shirt", Price = 20.00m, StockQuantity = 200, CategoryId = id3, Description = "Cotton t-shirt" }
);
base.OnModelCreating(modelBuilder);
}
}
说明:添加两个
Swagger
和OData
相关的处理
- 支持
OData
查询参数显示
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
namespace ODataDemo.Filters;
public class SwaggerDefaultValues : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
// 检查是否标记为过时
var isDeprecated = context.MethodInfo.GetCustomAttribute<ObsoleteAttribute>() != null;
if (isDeprecated)
{
operation.Deprecated = true;
}
// 添加默认响应
if (operation.Parameters == null)
{
operation.Parameters = [];
}
// 为每个参数添加默认值和描述
foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions
.First(p => p.Name == parameter.Name);
parameter.Description ??= description.ModelMetadata?.Description;
if (parameter.Schema.Default == null &&
description.DefaultValue != null &&
description.DefaultValue is not DBNull &&
description.ModelMetadata?.ModelType != null)
{
var json = System.Text.Json.JsonSerializer.Serialize(description.DefaultValue);
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
}
parameter.Required |= description.IsRequired;
}
// 为 OData 操作添加通用参数说明
if (context.ApiDescription.HttpMethod == "GET")
{
AddODataQueryParameters(operation);
}
}
private void AddODataQueryParameters(OpenApiOperation operation)
{
var odataParameters = new List<(string name, string description)>
{
("$filter", "Filters the results based on a Boolean condition"),
("$orderby", "Sorts the results"),
("$top", "Returns only the first n results"),
("$skip", "Skips the first n results"),
("$select", "Selects which properties to include in the response"),
("$expand", "Expands related entities inline"),
("$count", "Includes a count of the total number of items")
};
foreach (var (name, description) in odataParameters)
{
if (!operation.Parameters.Any(p => p.Name == name))
{
operation.Parameters.Add(new OpenApiParameter
{
Name = name,
In = ParameterLocation.Query,
Description = description,
Schema = new OpenApiSchema
{
Type = "string"
},
Required = false
});
}
}
}
}
- 处理
OData
特定类型的序列化问题
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace ODataDemo.Filters;
public class ODataSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
// 处理 OData 特定类型的序列化问题
if (context.Type.IsGenericType &&
(context.Type.GetGenericTypeDefinition() == typeof(SingleResult<>) ||
context.Type.GetGenericTypeDefinition() == typeof(Delta<>)))
{
// 对于 SingleResult<T> 和 Delta<T>,使用泛型参数类型
var genericType = context.Type.GetGenericArguments()[0];
// 可以根据需要自定义 schema
}
}
}
- 控制器实现
注意:控制器继承
ODataController
,没有[ApiController]
和[Route]
特性
//[Route(“api/[controller]”)]
//[ApiController]
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using ODataDemo.Models.Dtos;
using ODataDemo.Services;
namespace ODataDemo.Controllers;
public class ProductsController(IProductService productService) : ODataController
{
// GET /odata/Products
[EnableQuery]
public IActionResult Get()
{
return Ok(productService.GetProducts());
}
// GET /odata/Products(1) - 使用标准命名和路由
[EnableQuery]
public SingleResult<ProductResponeDto> Get([FromODataUri] string key)
{
var result = productService.GetProducts().Where(p => p.Id == key);
return SingleResult.Create(result);
}
// POST /odata/Products
public async Task<IActionResult> Post([FromBody] ProductRequstDto dto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var result = await productService.CreateProductAsync(dto);
return Created(result);
}
// PUT /odata/Products(1)
public async Task<IActionResult> Put([FromRoute] string key, [FromBody] ProductRequstDto dto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var result = await productService.UpdateProductAsync(key, dto);
return Updated(result);
}
// PATCH /odata/Products(1)
public async Task<IActionResult> Patch([FromRoute] string key, [FromBody] Delta<ProductRequstDto> delta)
{
var product = await productService.GetProductByIdAsync(key);
if (product == null)
{
return NotFound();
}
delta.Patch(product);
var result = await productService.UpdateProductAsync(key, product);
return Updated(result);
}
// DELETE /odata/Products(1)
public async Task<IActionResult> Delete([FromRoute] string key)
{
await productService.DeleteProductAsync(key);
return NoContent();
}
}
Program.cs
代码示例:
using Microsoft.AspNetCore.OData;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using ODataDemo.Data.Repositories;
using ODataDemo.Database;
using ODataDemo.Database.Repositories;
using ODataDemo.Filters;
using ODataDemo.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// 添加 API 探索器(必需)
builder.Services.AddEndpointsApiExplorer();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
//builder.Services.AddOpenApi();
// 添加 Swagger 生成器(必需)
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "OData API",
Description = "An ASP.NET Core OData API"
});
// 支持 OData 查询参数显示
options.OperationFilter<SwaggerDefaultValues>();
// 处理 OData 类型序列化问题
options.SchemaFilter<ODataSchemaFilter>();
options.CustomSchemaIds(type => type.ToString());
});
// 添加控制器和 OData 服务
builder.Services.AddControllers()
.AddOData(options =>
{
// 启用分页相关查询选项
options.Select().Filter().OrderBy().Expand().Count()
.SetMaxTop(100); // 设置每页最大记录数
// 使用 Convention-based 路由
options.AddRouteComponents("odata", EdmModelConfig.GetEdmModel());
// 关键配置:启用大小写不敏感
options.RouteOptions.EnablePropertyNameCaseInsensitive = true;
options.RouteOptions.EnableControllerNameCaseInsensitive = true;
options.RouteOptions.EnableActionNameCaseInsensitive = true;
});
// 添加数据库上下文
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("ODataDemo"));
// 注册db仓储服务
builder.Services.AddScoped<IDataRepository, DataRepository>();
// 注册业务服务
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IProductService, ProductService>();
var app = builder.Build();
// 初始化数据库
using (var scope = app.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.Database.EnsureCreated();
}
// Configure the HTTP request pipeline.
// 启用路由
app.UseRouting();
if (app.Environment.IsDevelopment())
{
// 添加错误处理中间件
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (Exception ex)
{
if (context.Request.Path.StartsWithSegments("/swagger"))
{
Console.WriteLine("Swagger Error:");
}
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
await context.Response.WriteAsync(ex.Message);
}
});
// 使用 swagger
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "OData API v1");
options.RoutePrefix = "swagger";
});
}
app.UseAuthorization();
app.MapControllers();
await app.RunAsync();
启动项目
启动项目,显示如下页面,其中包含两个接口,分别是产品和分类,另外还有一个源数据信息接口。
使用 Apipost
工具,访问源数据接口:
http://localhost:5108/odata/
其他接口类似,此处就不再详述。
最后附上完整示例截图,感兴趣的小伙伴欢迎点赞关注哟。
总结
ASP.NET Core WebAPI
和 OData
的集成提供了一种标准化、高性能的方式来构建数据驱动的 REST API
,大大简化了复杂查询接口的开发工作。
可以增强扩展统一的数据响应格式,比如 ApiResponse
,统一的异常数据格式等。