大纲 支持的接口 主要模型设计 控制器设计 数据源 查询(GET) 查询集合属性的值 查询基类类型Entity的基础类型集合属性值 查询派生类型Entity的基础类型集合属性值 查询集合属性数量 查询基类类型Entity的基础类型集合属性值的个数 查询派生类型Entity的基础类型集合属性值的个数 创建(POST) 新增基类类型Entity的基础类型集合属性值 新增派生类型Entity的基础类型集合属性值 完整更新(PUT) 完整更新基类类型Entity的属性值 完整更新派生类型Entity的属性值 局部更新(PATCH) 删除(DELETE) 删除基类类型Entity的非空属性 删除集合中一个值 删除派生类型Entity的非空属性 删除集合中一个值 主程序 代码地址 参考资料
在 OData API 设计中,集合属性(Collection-Valued Property)是指一个实体拥有的属性类型为集合(如 List<string>、List<Address>)。
支持的接口
Request Method Route Template 说明 GET ~/{entityset}/{key}/{property} 查询基类类型Entity的属性值 GET ~/{entityset}/{key}/{cast}/{property} 查询派生类型Entity的属性值 GET ~/{singleton}/{property} 查询基类类型单例的属性值 GET ~/{singleton}/{cast}/{property} 查询派生类型单例的属性值 GET ~/{entityset}/{key}/{collectionvaluedproperty}/$count 查询基类类型Entity的基础类型集合属性值的个数 GET ~/{entityset}/{key}/{cast}/{collectionvaluedproperty}/$count 查询派生类型Entity的基础类型集合属性值的个数 GET ~/{singleton}/{collectionvaluedproperty}/$count 查询基类类型单例的基础类型集合属性值的个数 GET ~/{singleton}/{cast}/{collectionvaluedproperty}/$count 查询派生类型单例的基础类型集合属性值的个数 POST ~/{entityset}/{key}/{collectionvaluedproperty} 新增基类类型Entity的基础类型集合属性值 POST ~/{entityset}/{key}/{cast}/{collectionvaluedproperty} 新增派生类型Entity的基础类型集合属性值 POST ~/{singleton}/{collectionvaluedproperty} 新增基类类型单例的基础类型集合属性值 POST ~/{singleton}/{cast}/{collectionvaluedproperty} 新增派生类型单例的基础类型集合属性值 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 ~/{singleton}/{property} 查询基类类型单例的属性值 GET ~/{singleton}/{cast}/{property} 查询派生类型单例的属性值 GET ~/{entityset}/{key}/{collectionvaluedproperty}/$count 查询基类类型Entity的基础类型集合属性值的个数 GET ~/{entityset}/{key}/{cast}/{collectionvaluedproperty}/$count 查询派生类型Entity的基础类型集合属性值的个数 GET ~/{singleton}/{collectionvaluedproperty}/$count 查询基类类型单例的基础类型集合属性值的个数 GET ~/{singleton}/{cast}/{collectionvaluedproperty}/$count 查询派生类型单例的基础类型集合属性值的个数
对于基类类型Entity的属性,我们选用Customer.BillingAddress。
[ EnableQuery ] public ActionResult< IEnumerable< string > > GetContactPhones ( [ FromRoute ] int key) { var customer = customers. SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( customer == null ) { return NotFound ( ) ; } return customer. ContactPhones; }
对于派生类型Entity的属性,我们选用EnterpriseCustomer.ShippingAddresses。
[ EnableQuery ] public ActionResult< IEnumerable< Address> > GetShippingAddressesFromEnterpriseCustomer ( [ FromRoute ] int key) { var enterpriseCustomer = customers. OfType < EnterpriseCustomer> ( ) . SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( enterpriseCustomer == null ) { return NotFound ( ) ; } return enterpriseCustomer. ShippingAddresses; }
查询集合属性的值
Request Method Route Template 说明 GET ~/{entityset}/{key}/{property} 查询基类类型Entity的属性值 GET ~/{entityset}/{key}/{cast}/{property} 查询派生类型Entity的属性值 GET ~/{singleton}/{property} 查询基类类型单例的属性值 GET ~/{singleton}/{cast}/{property} 查询派生类型单例的属性值
查询基类类型Entity的基础类型集合属性值
Request Method Route Template 说明 GET ~/{entityset}/{key}/{property} 查询基类类型Entity的属性值 GET ~/{singleton}/{cast}/{property} 查询派生类型单例的属性值
curl --location 'http://localhost:5119/odata/Customers(1)/ContactPhones'
{ "@odata.context" : "http://localhost:5119/odata/$metadata#Collection(Edm.String)" , "value" : [ "761-116-1865" ]
}
查询派生类型Entity的基础类型集合属性值
Request Method Route Template 说明 GET ~/{singleton}/{property} 查询基类类型单例的属性值 GET ~/{singleton}/{cast}/{property} 查询派生类型单例的属性值
curl --location 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses'
{ "@odata.context" : "http://localhost:5119/odata/$metadata#Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses" , "value" : [ { "Street" : "Street 3C" } ]
}
查询集合属性数量
Request Method Route Template 说明 GET ~/{entityset}/{key}/{collectionvaluedproperty}/$count 查询基类类型Entity的基础类型集合属性值的个数 GET ~/{entityset}/{key}/{cast}/{collectionvaluedproperty}/$count 查询派生类型Entity的基础类型集合属性值的个数 GET ~/{singleton}/{collectionvaluedproperty}/$count 查询基类类型单例的基础类型集合属性值的个数 GET ~/{singleton}/{cast}/{collectionvaluedproperty}/$count 查询派生类型单例的基础类型集合属性值的个数
查询基类类型Entity的基础类型集合属性值的个数
Request Method Route Template 说明 GET ~/{entityset}/{key}/{collectionvaluedproperty}/$count 查询基类类型Entity的基础类型集合属性值的个数 GET ~/{singleton}/{collectionvaluedproperty}/$count 查询基类类型单例的基础类型集合属性值的个数
curl --location 'http://localhost:5119/odata/Customers(1)/ContactPhones/$count'
1
查询派生类型Entity的基础类型集合属性值的个数
Request Method Route Template 说明 GET ~/{entityset}/{key}/{cast}/{collectionvaluedproperty}/$count 查询派生类型Entity的基础类型集合属性值的个数 GET ~/{singleton}/{cast}/{collectionvaluedproperty}/$count 查询派生类型单例的基础类型集合属性值的个数
curl --location 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses/$count'
1
创建(POST)
Request Method Route Template 说明 POST ~/{entityset}/{key}/{collectionvaluedproperty} 新增基类类型Entity的基础类型集合属性值 POST ~/{entityset}/{key}/{cast}/{collectionvaluedproperty} 新增派生类型Entity的基础类型集合属性值 POST ~/{singleton}/{collectionvaluedproperty} 新增基类类型单例的基础类型集合属性值 POST ~/{singleton}/{cast}/{collectionvaluedproperty} 新增派生类型单例的基础类型集合属性值
新增基类类型Entity的基础类型集合属性值
public ActionResult PostToContactPhones ( [ FromRoute ] int key, [ FromBody ] string contactPhone) { var customer = customers. SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( customer == null ) { return NotFound ( ) ; } customer. ContactPhones. Add ( contactPhone) ; return Accepted ( ) ; }
curl --location 'http://localhost:5119/odata/Customers(1)/ContactPhones' \
--header 'Content-Type: application/json' \
--data '{"value": "798-507-2014"
}'
新增派生类型Entity的基础类型集合属性值
public ActionResult PostToShippingAddressesFromEnterpriseCustomer ( [ FromRoute ] int key, [ FromBody ] Address address) { var enterpriseCustomer = customers. OfType < EnterpriseCustomer> ( ) . SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( enterpriseCustomer == null ) { return NotFound ( ) ; } enterpriseCustomer. ShippingAddresses. Add ( address) ; return Accepted ( ) ; }
curl --location 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses' \
--header 'Content-Type: application/json' \
--data '{"Street": "One Microsoft Way"
}'
完整更新(PUT)
Request Method Route Template 说明 PUT ~/{entityset}/{key}/{property} 完整更新基类类型Entity的属性值 PUT ~/{entityset}/{key}/{cast}/{property} 完整更新派生类型Entity的属性值 PUT ~/{singleton}/{property} 完整更新基类类型单例的属性值 PUT ~/{singleton}/{cast}/{property} 完整更新派生类型单例的属性值
完整更新基类类型Entity的属性值
Request Method Route Template 说明 PUT ~/{entityset}/{key}/{property} 完整更新基类类型Entity的属性值 PUT ~/{singleton}/{property} 完整更新基类类型单例的属性值
public ActionResult PutToContactPhones ( [ FromRoute ] int key, [ FromBody ] IEnumerable< string > contactPhones) { var customer = customers. SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( customer == null ) { return NotFound ( ) ; } customer. ContactPhones = contactPhones?. ToList ( ) ?? [ ] ; return Ok ( ) ; }
curl --location --request PUT 'http://localhost:5119/odata/Customers(1)/ContactPhones' \
--header 'Content-Type: application/json' \
--data '{"value": ["804-855-4049", "491-9198-476"]
}'
完整更新派生类型Entity的属性值
Request Method Route Template 说明 PUT ~/{entityset}/{key}/{cast}/{property} 完整更新派生类型Entity的属性值 PUT ~/{singleton}/{cast}/{property} 完整更新派生类型单例的属性值
public ActionResult PutToShippingAddressesFromEnterpriseCustomer ( [ FromRoute ] int key, [ FromBody ] IEnumerable< Address> shippingAddresses) { var enterpriseCustomer = customers. OfType < EnterpriseCustomer> ( ) . SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( enterpriseCustomer == null ) { return NotFound ( ) ; } enterpriseCustomer. ShippingAddresses = shippingAddresses?. ToList ( ) ?? [ ] ; return Ok ( ) ; }
curl --location --request PUT 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses' \
--header 'Content-Type: application/json' \
--data '{"value": [{"Street": "One Microsoft Way"},{"Street": "One Google Way"}]
}'
局部更新(PATCH)
在 OData 标准中,PATCH 方法主要用于部分更新实体的单值属性(如字符串、数字、复杂类型对象),而不直接支持集合属性(collection-valued property)的部分更新。 也就是说,OData PATCH 不能像 PATCH 单个对象那样,直接对集合属性(如 List<Address>、List<string>)进行“增量”或“部分”修改。
删除(DELETE)
Request Method Route Template 说明 DELETE ~/{entityset}/{key}/{nullableproperty} 删除基类类型Entity的非空属性 DELETE ~/{entityset}/{key}/{cast}/{nullableproperty} 删除派生类型Entity的非空属性 DELETE ~/{singleton}/{nullableproperty} 删除基类类型单例的非空属性 DELETE ~/{singleton}/{cast}/{nullableproperty} 删除派生类型单例的非空属性
删除操作只能支持非空的原始(primitive) 或者复杂(complex)property。本例中删除的Customer.ContactPhones以及EnterpriseCustomer.ShippingAddresses都是List类型,其值可以是null。
不管是“删除基类类型Entity的非空属性”,还是“删除派生类型Entity的非空属性“,下面三种删除方法不可以同时存在,否则请求可以被路由到错误的地址。 只有集合属性(如 ContactPhones)才支持带 Body 的 DELETE(用于删除集合中的某项),其他类型(原始(primitive)或者单值)的删除操作不能带Body。
删除基类类型Entity的非空属性
Request Method Route Template 说明 DELETE ~/{entityset}/{key}/{nullableproperty} 删除基类类型Entity的非空属性 DELETE ~/{singleton}/{nullableproperty} 删除基类类型单例的非空属性
清空集合
public ActionResult DeleteToContactPhones ( [ FromRoute ] int key) { var customer = customers. SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( customer == null ) { return NotFound ( ) ; } customer. ContactPhones. Clear ( ) ; return NoContent ( ) ; }
curl --location --request DELETE 'http://localhost:5119/odata/Customers(1)/ContactPhones'
删除一组集合值
public ActionResult DeleteToContactPhones ( [ FromRoute ] int key, [ FromBody ] IEnumerable< string > contactPhones) { var customer = customers. SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( customer == null ) { return NotFound ( ) ; } foreach ( var contactPhone in contactPhones) { customer. ContactPhones. Remove ( contactPhone) ; } return NoContent ( ) ; }
curl --location --request DELETE 'http://localhost:5119/odata/Customers(1)/ContactPhones' \
--header 'Content-Type: application/json' \
--data '{"value": ["804-855-4049", "491-9198-476"]
}'
删除集合中一个值
public ActionResult DeleteToContactPhones ( [ FromRoute ] int key, [ FromBody ] string contactPhone) { var customer = customers. SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( customer == null ) { return NotFound ( ) ; } if ( ! customer. ContactPhones. Remove ( contactPhone) ) { return NotFound ( ) ; } return NoContent ( ) ; }
curl --location --request DELETE 'http://localhost:5119/odata/Customers(1)/ContactPhones' \
--header 'Content-Type: application/json' \
--data '{"value": "798-507-2014"
}'
删除派生类型Entity的非空属性
Request Method Route Template 说明 DELETE ~/{entityset}/{key}/{cast}/{nullableproperty} 删除派生类型Entity的非空属性 DELETE ~/{singleton}/{cast}/{nullableproperty} 删除派生类型单例的非空属性
清空集合
public ActionResult DeleteToShippingAddressesFromEnterpriseCustomer ( [ FromRoute ] int key) { var enterpriseCustomer = customers. OfType < EnterpriseCustomer> ( ) . SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( enterpriseCustomer == null ) { return NotFound ( ) ; } enterpriseCustomer. ShippingAddresses. Clear ( ) ; return NoContent ( ) ; }
curl --location --request DELETE 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses'
删除一组集合值
public ActionResult DeleteToShippingAddressesFromEnterpriseCustomer ( [ FromRoute ] int key, [ FromBody ] IEnumerable< Address> shippingAddresses) { var enterpriseCustomer = customers. OfType < EnterpriseCustomer> ( ) . SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( enterpriseCustomer == null ) { return NotFound ( ) ; } foreach ( var address in shippingAddresses) { enterpriseCustomer. ShippingAddresses. Remove ( address) ; } return NoContent ( ) ; }
curl --location --request DELETE 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses' \
--header 'Content-Type: application/json' \
--data '{"value": [{"Street": "One Microsoft Way"},{"Street": "One Google Way"}]
}'
删除集合中一个值
public ActionResult DeleteToShippingAddressesFromEnterpriseCustomer ( [ FromRoute ] int key, [ FromBody ] Address address) { var enterpriseCustomer = customers. OfType < EnterpriseCustomer> ( ) . SingleOrDefault ( d => d. Id. Equals ( key) ) ; if ( enterpriseCustomer == null ) { return NotFound ( ) ; } if ( ! enterpriseCustomer. ShippingAddresses. Remove ( address) ) { return NotFound ( ) ; } return NoContent ( ) ; }
curl --location --request DELETE 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses' \
--header 'Content-Type: application/json' \
--data '{"Street": "One Microsoft Way"
}'
主程序
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 ( ) ;
服务文档
curl --location 'http://localhost:5119/odata'
{ "@odata.context" : "http://localhost:5119/odata/$metadata" , "value" : [ { "name" : "Customers" , "kind" : "EntitySet" , "url" : "Customers" } ]
}
模型元文档
curl --location 'http://localhost:5119/odata/$metadata'
< ? 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