大纲
- 支持的接口
- 主要模型设计
- 控制器设计
- 数据源
- 查询(GET)
- 查询基础类型的原始类型属性
- 查询基类类型Entity的基础类型属性的值
- 查询基类类型Entity的派生类型属性的原始值
- 查询派生类型Entity的基础类型属性
- 查询派生类型Entity的属性值
- 查询派生类型Entity的派生类型属性的原始值
- 新增(POST)
- 完整更新(PUT)
- 完整更新基类类型Entity的属性值
- 完整更新派生类型Entity的属性值
- 局部更新(PATCH)
- 删除(DELETE)
- 删除基类类型Entity的非空属性
- 删除派生类型Entity的非空属性
- 主程序
- 服务文档
- 模型元文档
- 代码地址
- 参考资料
原始属性(Primitive Property)是 OData 实体中最基础的数据类型属性,如字符串、数字、布尔值等。本文将带你了解如何通过 OData API 查询、更新这些原始属性,并探讨它们在实际业务中的常见用法和注意事项。
支持的接口
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{property} | 查询基类类型Entity的属性值 |
GET | ~/{entityset}/{key}/{cast}/{property} | 查询派生类型Entity的属性值 |
GET | ~/{entityset}/{key}/{property}/$value | 查询基类类型Entity的派生类型属性的原始值 |
GET | ~/{entityset}/{key}/{cast}/{property}/$value | 查询派生类型Entity的派生类型属性的原始值 |
GET | ~/{singleton}/{property} | 查询基类类型单例的属性值 |
GET | ~/{singleton}/{cast}/{property} | 查询派生类型单例的属性值 |
GET | ~/{singleton}/{property}/$value | 查询基类类型单例的派生类型属性的原始值 |
GET | ~/{singleton}/{cast}/{property}/$value | 查询派生类型单例的派生类型属性的原始值 |
PUT | ~/{entityset}/{key}/{property} | 完整更新基类类型Entity的属性值 |
PUT | ~/{entityset}/{key}/{cast}/{property} | 完整更新派生类型Entity的属性值 |
PUT | ~/{singleton}/{property} | 完整更新基类类型单例的属性值 |
PUT | ~/{singleton}/{cast}/{property} | 完整更新派生类型单例的属性值 |
DELETE | ~/{entityset}/{key}/{nullableproperty} | 删除基类类型Entity的非空属性 |
DELETE | ~/{entityset}/{key}/{cast}/{nullableproperty} | 删除派生类型Entity的非空属性 |
DELETE | ~/{singleton}/{nullableproperty} | 删除基类类型单例的非空属性 |
DELETE | ~/{singleton}/{cast}/{nullableproperty} | 删除派生类型单例的非空属性 |
主要模型设计
在项目下新增Models文件夹,并添加Address、PostalAddress 、Customer和EnterpriseCustomer类。
namespace Lesson8.Models
{public class Address{public required string Street { get; set; }}
}
namespace Lesson8.Models
{public class PostalAddress : Address{public required string PostalCode { get; set; }}
}
using System.Net;namespace Lesson8.Models
{using System.Collections.Generic;public class Customer{public int Id { get; set; }public string? Name { get; set; }public Address? BillingAddress { get; set; }public List<string> ContactPhones { get; set; } = [];}
}
using System.Net;namespace Lesson8.Models
{using System.Collections.Generic;public class EnterpriseCustomer : Customer{public decimal? CreditLimit { get; set; }public Address? RegisteredAddress { get; set; }public List<Address> ShippingAddresses { get; set; } = new List<Address>();}
}
控制器设计
在项目中新增Controller文件夹,然后添加CompanyController类。该类注册于ODataController,以便拥有如下能力:
- OData 路由支持
继承 ODataController 后,控制器自动支持 OData 路由(如 /odata/Shapes(1)),可以直接响应 OData 标准的 URL 路径和操作。 - OData 查询参数支持
可以使用 [EnableQuery] 特性,自动支持 $filter、$select、$orderby、$expand 等 OData 查询参数,无需手动解析。 - OData 响应格式
返回的数据会自动序列化为 OData 标准格式(如 JSON OData),方便前端或其他系统消费。 - OData Delta 支持
支持 Delta<T>、DeltaSet<T> 等类型,便于实现 PATCH、批量 PATCH 等 OData 特有的部分更新操作。 - 更丰富的 OData 语义
继承后可方便实现实体集、实体、导航属性、复杂类型等 OData 语义,提升 API 的表达能力。
using Lesson8.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;namespace Lesson8.Controllers
{public class CustomersController: ODataController{}
}
下面我们在该类中填充逻辑。
数据源
private static List<Customer> customers = new List<Customer>{new Customer{Id = 1,Name = "Customer 1",ContactPhones = new List<string> { "761-116-1865" },BillingAddress = new Address { Street = "Street 1A" }},new Customer{Id = 2,Name = "Customer 2",ContactPhones = new List<string> { "835-791-8257" },BillingAddress = new PostalAddress { Street = "2A", PostalCode = "14030" }},new EnterpriseCustomer{Id = 3,Name = "Customer 3",ContactPhones = new List<string> { "157-575-6005" },BillingAddress = new Address { Street = "Street 3A" },CreditLimit = 4200,RegisteredAddress = new Address { Street = "Street 3B" },ShippingAddresses = new List<Address>{new Address { Street = "Street 3C" }}},new EnterpriseCustomer{Id = 4,Name = "Customer 4",ContactPhones = new List<string> { "724-096-6719" },BillingAddress = new Address { Street = "Street 4A" },CreditLimit = 3700,RegisteredAddress = new PostalAddress { Street = "Street 4B", PostalCode = "22109" },ShippingAddresses = new List<Address>{new Address { Street = "Street 4C" }}}};
查询(GET)
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{property} | 查询基类类型Entity的属性值 |
GET | ~/{entityset}/{key}/{cast}/{property} | 查询派生类型Entity的属性值 |
GET | ~/{entityset}/{key}/{property}/$value | 查询基类类型Entity的派生类型属性的原始值 |
GET | ~/{entityset}/{key}/{cast}/{property}/$value | 查询派生类型Entity的派生类型属性的原始值 |
GET | ~/{singleton}/{property} | 查询基类类型单例的属性值 |
GET | ~/{singleton}/{cast}/{property} | 查询派生类型单例的属性值 |
GET | ~/{singleton}/{property}/$value | 查询基类类型单例的派生类型属性的原始值 |
GET | ~/{singleton}/{cast}/{property}/$value | 查询派生类型单例的派生类型属性的原始值 |
查询基础类型的原始类型属性
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{property} | 查询基类类型Entity的属性值 |
GET | ~/{singleton}/{property} | 查询基类类型单例的属性值 |
[EnableQuery]public ActionResult<string> GetName([FromRoute] int key){var customer = customers.SingleOrDefault(d => d.Id.Equals(key));if (customer == null || customer.Name == null){return NotFound();}return customer.Name;}
查询基类类型Entity的基础类型属性的值
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{property}/$value | 查询基类类型Entity的派生类型属性的原始值 |
GET | ~/{singleton}/{property}/$value | 查询基类类型单例的派生类型属性的原始值 |
- Request
curl --location 'http://localhost:5119/odata/Customers(1)/Name'
- Response
{"@odata.context": "http://localhost:5119/odata/$metadata#Customers(1)/Name","value": "Customer 1"
}
查询基类类型Entity的派生类型属性的原始值
- Request
curl --location 'http://localhost:5119/odata/Customers(1)/Name/$value'
- Response
Customer 1
查询派生类型Entity的基础类型属性
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{cast}/{property} | 查询派生类型Entity的属性值 |
GET | ~/{entityset}/{key}/{cast}/{property}/$value | 查询派生类型Entity的派生类型属性的原始值 |
GET | ~/{singleton}/{cast}/{property} | 查询派生类型单例的属性值 |
GET | ~/{singleton}/{cast}/{property}/$value | 查询派生类型单例的派生类型属性的原始值 |
[EnableQuery]public ActionResult<decimal> GetCreditLimitFromEnterpriseCustomer([FromRoute] int key){var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));if (enterpriseCustomer == null || !enterpriseCustomer.CreditLimit.HasValue){return NotFound();}return enterpriseCustomer.CreditLimit.Value;}
查询派生类型Entity的属性值
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{cast}/{property} | 查询派生类型Entity的属性值 |
GET | ~/{singleton}/{cast}/{property} | 查询派生类型单例的属性值 |
[EnableQuery]public ActionResult<decimal> GetCreditLimit([FromRoute] int key){var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));if (enterpriseCustomer == null){return NotFound();}return enterpriseCustomer.CreditLimit;}
- Request
curl --location 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/CreditLimit'
- Response
{"@odata.context": "http://localhost:5119/odata/$metadata#Customers(3)/Lesson8.Models.EnterpriseCustomer/CreditLimit","value": 4200
}
查询派生类型Entity的派生类型属性的原始值
curl --location 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/CreditLimit/$value'
- Response
4200
新增(POST)
而原始属性(如 Name、CreditLimit)只是实体的一个字段,不是集合或导航属性,不能单独“新增”,所以不支持 POST 操作。
完整更新(PUT)
Request Method | Route Template | 说明 |
---|---|---|
PUT | ~/{entityset}/{key}/{property} | 完整更新基类类型Entity的属性值 |
PUT | ~/{entityset}/{key}/{cast}/{property} | 完整更新派生类型Entity的属性值 |
PUT | ~/{singleton}/{property} | 完整更新基类类型单例的属性值 |
PUT | ~/{singleton}/{cast}/{property} | 完整更新派生类型单例的属性值 |
完整更新基类类型Entity的属性值
public ActionResult PutToName([FromRoute] int key, [FromBody] string name){var customer = customers.SingleOrDefault(d => d.Id.Equals(key));if (customer == null){return NotFound();}customer.Name = name;return Ok();}
- Request
curl --location --request PUT 'http://localhost:5119/odata/Customers(1)/Name' \
--header 'Content-Type: application/json' \
--data '{"value": "Sue"
}'
完整更新派生类型Entity的属性值
public ActionResult PutToCreditLimitFromEnterpriseCustomer([FromRoute] int key, [FromBody] decimal creditLimit){var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));if (enterpriseCustomer == null){return NotFound();}enterpriseCustomer.CreditLimit = creditLimit;return Ok();}
- Request
curl --location --request PUT 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/CreditLimit' \
--header 'Content-Type: application/json' \
--data '{"value": 10
}'
局部更新(PATCH)
Patch操作支持单值复杂类型属性(single-valued complex properties),所以原始类型Property不支持Patch操作。
删除(DELETE)
Request Method | Route Template | 说明 |
---|---|---|
DELETE | ~/{entityset}/{key}/{nullableproperty} | 删除基类类型Entity的非空属性 |
DELETE | ~/{entityset}/{key}/{cast}/{nullableproperty} | 删除派生类型Entity的非空属性 |
DELETE | ~/{singleton}/{nullableproperty} | 删除基类类型单例的非空属性 |
DELETE | ~/{singleton}/{cast}/{nullableproperty} | 删除派生类型单例的非空属性 |
由于删除操作只能删除可设置为null的属性,所以我们需要在模型定义时,将需要操作的属性(Customer.Name和EnterpriseCustomer .CreditLimit )设置为nullable,否则请求找不到对应的路由函数。
using System.Net;namespace Lesson8.Models
{using System.Collections.Generic;public class Customer{public int Id { get; set; }public string? Name { get; set; }public Address? BillingAddress { get; set; }public List<string> ContactPhones { get; set; } = [];}
}
using System.Net;namespace Lesson8.Models
{using System.Collections.Generic;public class EnterpriseCustomer : Customer{public decimal? CreditLimit { get; set; }public Address? RegisteredAddress { get; set; }public List<Address> ShippingAddresses { get; set; } = new List<Address>();}
}
删除基类类型Entity的非空属性
public ActionResult DeleteToName([FromRoute] int key){var customer = customers.SingleOrDefault(d => d.Id.Equals(key));if (customer == null){return NotFound();}customer.Name = string.Empty; // Clear the namereturn NoContent();}
- Request
curl --location --request DELETE 'http://localhost:5119/odata/Customers(1)/Name'
删除派生类型Entity的非空属性
public ActionResult DeleteToCreditLimitFromEnterpriseCustomer([FromRoute] int key){var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));if (enterpriseCustomer == null){return NotFound();}enterpriseCustomer.CreditLimit = 0; // Reset the credit limitreturn NoContent();}
- Request
curl --location --request DELETE 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/CreditLimit'
主程序
using Lesson8.Models;
using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;
using Microsoft.OData.Edm;var builder = WebApplication.CreateBuilder(args);static IEdmModel GetEdmModel()
{var modelBuilder = new ODataConventionModelBuilder();modelBuilder.EntitySet<Customer>("Customers");return modelBuilder.GetEdmModel();
}builder.Services.AddControllers().AddOData(options =>options.Select().Filter().OrderBy().Expand().Count().SetMaxTop(null).AddRouteComponents("odata", GetEdmModel())
);var app = builder.Build();app.UseRouting();app.MapControllers();app.Run();
服务文档
- Request
curl --location 'http://localhost:5119/odata'
- Response
{"@odata.context": "http://localhost:5119/odata/$metadata","value": [{"name": "Customers","kind": "EntitySet","url": "Customers"}]
}
模型元文档
- Request
curl --location 'http://localhost:5119/odata/$metadata'
- Response
<?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="Lesson8.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="Customer"><Key><PropertyRef Name="Id" /></Key><Property Name="Id" Type="Edm.Int32" Nullable="false" /><Property Name="Name" Type="Edm.String" Nullable="false" /><Property Name="BillingAddress" Type="Lesson8.Models.Address" Nullable="false" /><Property Name="ContactPhones" Type="Collection(Edm.String)" /></EntityType><ComplexType Name="Address"><Property Name="Street" Type="Edm.String" Nullable="false" /></ComplexType><ComplexType Name="PostalAddress" BaseType="Lesson8.Models.Address"><Property Name="PostalCode" Type="Edm.String" Nullable="false" /></ComplexType><EntityType Name="EnterpriseCustomer" BaseType="Lesson8.Models.Customer"><Property Name="CreditLimit" Type="Edm.Decimal" Nullable="false" Scale="variable" /><Property Name="RegisteredAddress" Type="Lesson8.Models.Address" Nullable="false" /><Property Name="ShippingAddresses" Type="Collection(Lesson8.Models.Address)" /></EntityType></Schema><Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityContainer Name="Container"><EntitySet Name="Customers" EntityType="Lesson8.Models.Customer" /></EntityContainer></Schema></edmx:DataServices>
</edmx:Edmx>
代码地址
https://github.com/f304646673/odata/tree/main/csharp/Lesson/Lesson8
参考资料
- https://learn.microsoft.com/en-us/odata/webapi-8/fundamentals/property-routing?tabs=net60%2Cvisual-studio