大纲
原始属性(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 name
return 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 limit
return 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