大纲
本文是一个基于 ASP.NET Core OData 的 Web API 示例,主要演示了如何对员工(Employee)和经理(Manager)进行绩效、奖金等业务相关的服务端计算和查询。
Function 是一种用于封装服务端自定义逻辑的机制,允许客户端通过标准化的 URL 调用复杂计算、查询或业务操作。Function 通常用于实现那些无法简单归类为 CRUD(创建、读取、更新、删除) 的操作,例如计算折扣价格、执行聚合统计或调用外部服务。
核心概念
- 幂等性(Idempotency)
Function 必须是 幂等的,即多次调用产生相同结果,且不会修改资源状态。例如,计算税费或生成报表属于幂等操作。 - 参数与返回值
参数:通过 URL 查询字符串传递(如 ?param=value)。
返回值:必须返回数据(如实体、集合、基本类型),不可为无返回值操作。 - 绑定类型
绑定函数(Bound Function):针对特定实体或集合,例如 Products(1)/CalculateTax。
非绑定函数(Unbound Function):独立操作,例如 GetTopSellingProducts()。
特点
- 封装复杂逻辑
将服务端的复杂计算或业务规则封装为单一操作,简化客户端调用。 - 提高性能
对于需要多表连接或复杂计算的场景,Function 可在服务端高效执行,减少数据传输。 - 保持数据一致性
通过服务端统一处理业务逻辑,确保数据操作的一致性和安全性。 - 增强 API 表达力
使 API 能够表达更丰富的操作语义,超越简单的 CRUD。
支持的接口
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}|{singleton}/{function} | 调用绑定到EntitySet或者Singleton的Function |
GET | ~/{entityset}|{singleton}/{cast}/{function} | 调用绑定到EntitySet或者Singleton的特定派生类型的Function |
GET | ~/{entityset}/{key}/{function} | 通过指定Key的Entity使用Function |
GET | ~/{entityset}/{key}/{cast}/{function} | 通过指定Key的特定派生类型Entity使用Function |
GET | ~/{function}(param1={value},param2={value}) | 调用非绑定Function |
主要模型设计
在项目下新增Models文件夹,并添加Employee和Manager类。
Employee类的属性如下
- Id
- 类型:int
- 说明:员工的唯一标识符。通常作为主键,用于区分不同的员工对象。
- Name
- 类型:required string
- 说明:员工姓名。required 关键字表示在创建 Employee 实例时必须赋值,保证数据完整性。
- PerfRating
- 类型:int
- 说明:绩效评分。用于记录员工的绩效考核分数,通常用于业务分析、排序或筛选。
namespace Lesson5.Models
{
public class Employee
{
public int Id { get; set; }
public required string Name { get; set; }
public int PerfRating { get; set; }
}
}
Manager 类继承自 Employee 基类,意味着 Manager 拥有 Employee 的所有属性(如 Id、Name、PerfRating)。同时它也新增了自己的特有属性:
- Bonus
- 类型:decimal
- 说明:表示经理的奖金。该属性是 Manager 独有的,普通员工没有此属性。
- 用途:可用于 OData 查询、奖金统计、业务逻辑处理等。
namespace Lesson5.Models
{
public class Manager : Employee
{
public decimal Bonus { get; set; }
}
}
控制器设计
在项目中新增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 Lesson5.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;
namespace Lesson5.Controllers
{
public class EmployeesController : ODataController
{
}
}
下面我们在该类中填充逻辑。
数据源
private static List<Employee> employees = new()
{
new Employee { Id = 1, Name = "Employee 1", PerfRating = 8 },
new Employee { Id = 2, Name = "Employee 2", PerfRating = 7 },
new Employee { Id = 3, Name = "Employee 3", PerfRating = 5 },
new Employee { Id = 4, Name = "Employee 4", PerfRating = 3 },
new Manager { Id = 5, Name = "Employee 5", PerfRating = 7, Bonus = 2900 },
new Manager { Id = 6, Name = "Employee 6", PerfRating = 9, Bonus = 3700 }
};
调用Function(GET)
调用绑定的Function
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}|{singleton}/{function} | 调用绑定到EntitySet或者Singleton的Function |
GET | ~/{entityset}|{singleton}/{cast}/{function} | 调用绑定到EntitySet或者Singleton的特定派生类型的Function |
GET | ~/{entityset}/{key}/{function} | 通过指定Key的Entity使用Function |
GET | ~/{entityset}/{key}/{cast}/{function} | 通过指定Key的特定派生类型Entity使用Function |
bound function(绑定函数)在 OData 中有以下特点:
- 与实体或实体集绑定
- bound function 必须绑定到某个实体类型(如 Employee)或实体集(如 Employees)。
- 例如:GetRating 绑定到单个 Employee,GetHighestRating 绑定到 Employee 集合。
- 调用时需指定绑定对象
- 路由中必须包含实体或集合。例如:
- 单实体:/odata/Employees(1)/Default.GetRating()
- 实体集:/odata/Employees/Default.GetHighestRating()
- 第一个参数为绑定类型
- 在 EDMX 元数据中,bound function 的第一个参数是 bindingParameter,类型为绑定的实体或集合。
- 常用于与特定数据相关的操作
- 适合实现“对某个对象/集合做某事”的业务逻辑,如统计、聚合、分析等。
- OData 路由自动识别
- OData 框架会根据函数绑定类型自动生成对应的路由和元数据。
调用绑定到基类类型EntitySet的Function
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}|{singleton}/{function} | 调用绑定到EntitySet或者Singleton的Function |
下面的代码是一个 GET 请求的 Web API 方法,用于获取所有员工的最高绩效评分(PerfRating)。
employees.Select(d => d.PerfRating)
是从所有员工对象中提取 PerfRating 字段,得到所有绩效分的集合。
.OrderByDescending(d => d).First()
将绩效分降序排列,取第一个(即最大值)。
public ActionResult<decimal> GetHighestRating()
{
if (employees.Count < 1)
{
return NoContent();
}
return employees.Select(d => d.PerfRating).OrderByDescending(d => d).First();
}
- Request
curl --location 'http://localhost:5119/odata/Employees/GetHighestRating()'
- Response
{
"@odata.context": "http://localhost:5119/odata/$metadata#Edm.Decimal",
"value": 9
}
调用绑定到基类类型Entity的Function
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{function} | 通过指定Key的Entity使用Function |
下面的代码用于根据员工的唯一标识(Id)查询并返回该员工的绩效评分(PerfRating)。
var employee = employees.SingleOrDefault(d => d.Id.Equals(key));
是在内存员工列表中查找 Id 等于 key 的员工对象。
[HttpGet]
public ActionResult<decimal> GetRating([FromRoute] int key)
{
var employee = employees.SingleOrDefault(d => d.Id.Equals(key));
if (employee == null)
{
return NotFound();
}
return employee.PerfRating;
}
- Request
curl --location 'http://localhost:5119/odata/Employees(1)/GetRating()'
- Response
{
"@odata.context": "http://localhost:5119/odata/$metadata#Edm.Decimal",
"value": 8
}
调用绑定到特定派生类类型EntitySet的Function
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}|{singleton}/{cast}/{function} | 调用绑定到EntitySet或者Singleton的特定派生类型的Function |
下面的代码用于获取所有经理(Manager)对象中的最大奖金(Bonus)。
var managers = employees.OfType<Manager>().ToArray();
是从员工集合中筛选出所有 Manager 类型的对象,转换为数组。这样可以只对经理对象进行后续操作。
return managers.Select(d => d.Bonus).OrderByDescending(d => d).First();
提取所有经理的 Bonus 字段,按降序排列,取第一个(即最大奖金)。
[HttpGet]
public ActionResult<decimal> GetHighestBonusOnCollectionOfManager()
{
var managers = employees.OfType<Manager>().ToArray();
if (managers.Length < 1)
{
return NoContent();
}
return managers.Select(d => d.Bonus).OrderByDescending(d => d).First();
}
- Request
curl --location 'http://localhost:5119/odata/Employees/Lesson5.Models.Manager/GetHighestBonus()'
- Response
{
"@odata.context": "http://localhost:5119/odata/$metadata#Edm.Decimal",
"value": 3700
}
调用绑定到特定派生类类型Entity的Function
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{cast}/{function} | 通过指定Key的特定派生类型Entity使用Function |
下面的代码用于根据经理(Manager)的唯一标识(Id)查询并返回该经理的奖金(Bonus)。
var manager = employees.OfType<Manager>().SingleOrDefault(d => d.Id.Equals(key));
是从员工集合中筛选出所有 Manager 类型的对象,并查找 Id 等于 key 的经理。
[HttpGet]
public ActionResult<decimal> GetBonusOnManager([FromRoute] int key)
{
var manager = employees.OfType<Manager>().SingleOrDefault(d => d.Id.Equals(key));
if (manager == null)
{
return NotFound();
}
return manager.Bonus;
}
- Request
curl --location 'http://localhost:5119/odata/Employees(5)/Lesson5.Models.Manager/GetBonus()'
- Response
{
"@odata.context": "http://localhost:5119/odata/$metadata#Edm.Decimal",
"value": 2900
}
调用未绑定的Function
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{function}(param1={value},param2={value}) | 调用非绑定函数 |
unbound function(非绑定函数)在 OData 中有以下特点:
- 不依赖于实体或实体集
- unbound function 不是挂在某个实体类型或集合上的,而是全局可用。
- 例如:GetSalary(hourlyRate, hoursWorked),直接通过 /odata/GetSalary(…) 调用。
- 调用方式简单
- 直接通过 OData 路由访问,无需指定实体或集合。
- 例:GET /odata/GetSalary(hourlyRate=100,hoursWorked=8)
- 参数和返回值自定义
- 可以有任意参数和返回类型,不受实体模型约束,适合实现全局计算、工具类服务等。
- 在 EDMX 中有 FunctionImport
- 在元数据(EDMX)中,unbound function 会通过 <FunctionImport> 节点暴露,区别于 bound function 的绑定参数。
为了区别之前的绑定Function,我们新建一个DefaultController类用来承载未绑定Function。
下面的代码用于根据传入的时薪(hourlyRate)和工时(hoursWorked)计算工资,并返回结果。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Routing.Controllers;
namespace Lesson5.Controllers
{
public class DefaultController : ODataController
{
[HttpGet("odata/GetSalary(hourlyRate={hourlyRate},hoursWorked={hoursWorked})")]
public ActionResult<decimal> GetSalary(decimal hourlyRate, int hoursWorked)
{
return hourlyRate * hoursWorked;
}
}
}
- Request
curl --location 'http://localhost:5119/odata/GetSalary(hourlyRate=17,hoursWorked=40)'
- Response
{
"@odata.context": "http://localhost:5119/odata/$metadata#Edm.Decimal",
"value": 680
}
主程序
using Lesson5.Models;
using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;
using Microsoft.OData.Edm;
var builder = WebApplication.CreateBuilder(args);
// 提取 OData EDM 模型构建为方法,便于维护和扩展
static IEdmModel GetEdmModel()
{
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Employee>("Employees");
var employeeEntityType = modelBuilder.EntitySet<Employee>("Employees").EntityType;
employeeEntityType.Collection.Function("GetHighestRating").Returns<int>();
employeeEntityType.Function("GetRating").Returns<int>();
var managerEntityType = modelBuilder.EntityType<Manager>();
managerEntityType.Collection.Function("GetHighestBonus").Returns<decimal>();
managerEntityType.Function("GetBonus").Returns<decimal>();
var getSalaryFunction = modelBuilder.Function("GetSalary");
getSalaryFunction.Parameter<decimal>("hourlyRate");
getSalaryFunction.Parameter<int>("hoursWorked");
getSalaryFunction.Returns<decimal>();
return modelBuilder.GetEdmModel();
}
// 添加 OData 服务和配置
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();
employeeEntityType.Collection.Function("GetHighestRating").Returns<int>();
是将GetHighestRating绑定到Employee的EntitySet上。
employeeEntityType.Function("GetRating").Returns<int>();
是将GetRating绑定到Employee的Entity上。
managerEntityType.Collection.Function("GetHighestBonus").Returns<decimal>();
是将GetHighestBonus绑定到Manager的EntitySet上。
managerEntityType.Function("GetBonus").Returns<decimal>();
是将GetBonus绑定到Manager的Entity上。
getSalaryFunction
是一个未绑定的Function,我们需要定义其出入参名称和类型。
服务文档
- Request
curl --location 'http://localhost:5119/odata'
- Response
{
"@odata.context": "http://localhost:5119/odata/$metadata",
"value": [
{
"name": "Employees",
"kind": "EntitySet",
"url": "Employees"
}
]
}
模型元文档
- 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="Lesson5.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Employee">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" Nullable="false" />
<Property Name="PerfRating" Type="Edm.Int32" Nullable="false" />
</EntityType>
<EntityType Name="Manager" BaseType="Lesson5.Models.Employee">
<Property Name="Bonus" Type="Edm.Decimal" Nullable="false" Scale="variable" />
</EntityType>
</Schema>
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<Function Name="GetHighestRating" IsBound="true">
<Parameter Name="bindingParameter" Type="Collection(Lesson5.Models.Employee)" />
<ReturnType Type="Edm.Int32" Nullable="false" />
</Function>
<Function Name="GetRating" IsBound="true">
<Parameter Name="bindingParameter" Type="Lesson5.Models.Employee" />
<ReturnType Type="Edm.Int32" Nullable="false" />
</Function>
<Function Name="GetHighestBonus" IsBound="true">
<Parameter Name="bindingParameter" Type="Collection(Lesson5.Models.Manager)" />
<ReturnType Type="Edm.Decimal" Nullable="false" Scale="variable" />
</Function>
<Function Name="GetBonus" IsBound="true">
<Parameter Name="bindingParameter" Type="Lesson5.Models.Manager" />
<ReturnType Type="Edm.Decimal" Nullable="false" Scale="variable" />
</Function>
<Function Name="GetSalary">
<Parameter Name="hourlyRate" Type="Edm.Decimal" Nullable="false" Scale="variable" />
<Parameter Name="hoursWorked" Type="Edm.Int32" Nullable="false" />
<ReturnType Type="Edm.Decimal" Nullable="false" Scale="variable" />
</Function>
<EntityContainer Name="Container">
<EntitySet Name="Employees" EntityType="Lesson5.Models.Employee" />
<FunctionImport Name="GetSalary" Function="Default.GetSalary" IncludeInServiceDocument="true" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
其组成和解释如下:
- 实体类型定义
- Employee
- <EntityType Name=“Employee”>:定义了 Employee 实体。
- <Key><PropertyRef Name=“Id” /></Key>:主键为 Id。
- <Property Name=“Id” Type=“Edm.Int32” Nullable=“false” />:Id,int 类型,必填。
- <Property Name=“Name” Type=“Edm.String” Nullable=“false” />:Name,string 类型,必填。
- <Property Name=“PerfRating” Type=“Edm.Int32” Nullable=“false” />:PerfRating,绩效评分,int 类型,必填。
- Manager
- :Manager 继承自 Employee。
- :Bonus,奖金,decimal 类型,必填。
- 函数定义(Function)
- GetHighestRating
- 绑定到 Employee 集合(IsBound=“true”,Type=“Collection(Lesson5.Models.Employee)”)。
- 返回所有员工中的最高绩效评分(int)。
- GetRating
- 绑定到单个 Employee。
- 返回该员工的绩效评分(int)。
- GetHighestBonus
- 绑定到 Manager 集合。
- 返回所有经理中的最高奖金(decimal)。
- GetBonus
- 绑定到单个 Manager。
- 返回该经理的奖金(decimal)。
- GetSalary
- 非绑定函数(IsBound 缺省),即全局函数。
- 参数:hourlyRate(decimal),hoursWorked(int)。
- 返回工资(decimal),即时薪乘以工时。
- 通过 FunctionImport 显式暴露在服务文档中,便于直接调用。
- 实体容器与实体集
- <EntityContainer Name=“Container”>:OData 服务的根容器。
- <EntitySet Name=“Employees” EntityType=“Lesson5.Models.Employee” />:定义了 Employees 实体集,类型为 Employee(包含 Manager)。
- <FunctionImport Name=“GetSalary” … />:将 GetSalary 函数作为服务操作暴露,允许通过 /odata/GetSalary(…) 直接调用。
代码地址
https://github.com/f304646673/odata/tree/main/csharp/Lesson/Lesson5