ASP.NET Core OData 实践——Lesson8增删改查集合Property(C#)

在 OData API 设计中,集合属性(Collection-Valued Property)是指一个实体拥有的属性类型为集合(如 List<string>、List<Address>)。

支持的接口

Request MethodRoute 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,以便拥有如下能力:

  1. OData 路由支持
    继承 ODataController 后,控制器自动支持 OData 路由(如 /odata/Shapes(1)),可以直接响应 OData 标准的 URL 路径和操作。
  2. OData 查询参数支持
    可以使用 [EnableQuery] 特性,自动支持 $filter、$select、$orderby、$expand 等 OData 查询参数,无需手动解析。
  3. OData 响应格式
    返回的数据会自动序列化为 OData 标准格式(如 JSON OData),方便前端或其他系统消费。
  4. OData Delta 支持
    支持 Delta<T>、DeltaSet<T> 等类型,便于实现 PATCH、批量 PATCH 等 OData 特有的部分更新操作。
  5. 更丰富的 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 MethodRoute 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 MethodRoute Template说明
GET~/{entityset}/{key}/{property}查询基类类型Entity的属性值
GET~/{entityset}/{key}/{cast}/{property}查询派生类型Entity的属性值
GET~/{singleton}/{property}查询基类类型单例的属性值
GET~/{singleton}/{cast}/{property}查询派生类型单例的属性值
查询基类类型Entity的基础类型集合属性值
Request MethodRoute Template说明
GET~/{entityset}/{key}/{property}查询基类类型Entity的属性值
GET~/{singleton}/{cast}/{property}查询派生类型单例的属性值
  • Request
curl --location 'http://localhost:5119/odata/Customers(1)/ContactPhones'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Collection(Edm.String)",
    "value": [
        "761-116-1865"
    ]
}
查询派生类型Entity的基础类型集合属性值
Request MethodRoute Template说明
GET~/{singleton}/{property}查询基类类型单例的属性值
GET~/{singleton}/{cast}/{property}查询派生类型单例的属性值
  • Request
curl --location 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses",
    "value": [
        {
            "Street": "Street 3C"
        }
    ]
}

查询集合属性数量

Request MethodRoute 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 MethodRoute Template说明
GET~/{entityset}/{key}/{collectionvaluedproperty}/$count查询基类类型Entity的基础类型集合属性值的个数
GET~/{singleton}/{collectionvaluedproperty}/$count查询基类类型单例的基础类型集合属性值的个数
  • Request
curl --location 'http://localhost:5119/odata/Customers(1)/ContactPhones/$count'
  • Response
1
查询派生类型Entity的基础类型集合属性值的个数
Request MethodRoute Template说明
GET~/{entityset}/{key}/{cast}/{collectionvaluedproperty}/$count查询派生类型Entity的基础类型集合属性值的个数
GET~/{singleton}/{cast}/{collectionvaluedproperty}/$count查询派生类型单例的基础类型集合属性值的个数
  • Request
curl --location 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses/$count'
  • Response
1

创建(POST)

Request MethodRoute 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();
        }
  • Request
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();
        }
  • Request
curl --location 'http://localhost:5119/odata/Customers(3)/Lesson8.Models.EnterpriseCustomer/ShippingAddresses' \
--header 'Content-Type: application/json' \
--data '{
    "Street": "One Microsoft Way"
}'

完整更新(PUT)

Request MethodRoute Template说明
PUT~/{entityset}/{key}/{property}完整更新基类类型Entity的属性值
PUT~/{entityset}/{key}/{cast}/{property}完整更新派生类型Entity的属性值
PUT~/{singleton}/{property}完整更新基类类型单例的属性值
PUT~/{singleton}/{cast}/{property}完整更新派生类型单例的属性值

完整更新基类类型Entity的属性值

Request MethodRoute 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();
        }
  • Request
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 MethodRoute 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();
        }
  • Request
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 MethodRoute 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 MethodRoute 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();
        }
  • Request
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();
        }
  • Request
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();
        }
  • Request
curl --location --request DELETE 'http://localhost:5119/odata/Customers(1)/ContactPhones' \
--header 'Content-Type: application/json' \
--data '{
    "value": "798-507-2014"
}'

删除派生类型Entity的非空属性

Request MethodRoute 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();
        }
  • Request
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();
        }
  • Request
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();
        }
  • Request
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();

服务文档

  • 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

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值