引言
在 OData 中,EDM(Entity Data Model) 代表“实体数据模型”,它是一种用于表示 Web API 中的结构化数据的格式。EDM 定义了可以由 OData 服务公开的数据类型、实体和关系。 EDM 也提供了一些规则来描述数据模型中的实体之间的关系,例如继承、关联和复合类型。EDM 是 OData 协议的核心组成部分之一,它允许客户端和服务器之间以一致的方式交换和操作数据。
EDM 与实体对象模型
我刚接触 EDM 时恰好是与 EF Core 一起使用,就非常不理解这个现象:明明已经在 EF Core 中已经定义了模型,为啥还需要单独配置一个 EDM?
其实,EDM 和实体框架(EF)Core 中的实体对象虽然都用于数据建模,但却是不同的概念:在 EF Core 中,实体对象表示数据库中的表或视图,而 EDM 定义了 OData 服务中的数据结构,包括实体、属性、导航属性等。可以理解实体对象是为数据库服务,而 EDM 是用于数据开放服务的。
虽然 EDM 和 EF Core 中的实体对象都具有一些相似之处,例如它们都有属性和关系,甚至你也可以直接返回实体模型用提供给 OData 让其动态自动生成 EDM 模型(Non-ODM 模式),但是依然建议使用 EDM 模型,主要是有几个方面:
- 实体框架中的实体类可能包含与 OData 服务定义不同的属性。例如,实体框架中的实体类可能包含用于持久化和跟踪状态的属性,而这些属性可能并不需要在 OData 服务中公开。
- 实体框架中的实体类可能使用与 OData 服务定义不同的命名约定。例如,实体框架中的实体类可能使用 PascalCase(首字母大写)命名约定,而在 OData 服务中的 EDM 可以使用 camelCase(首字母小写)命名约定。
- 实体框架中的实体类可能包含与 OData 服务定义不同的数据结构。例如,联合主键的实现用于 OData 查询较为麻烦,可以配置 EDM 使得 OData 对外服务不使用联合主键。
EDM 配置
在配置 OData 时,我们需要在代码中提供 EDM 对象。
.AddRouteComponents(AuthorizeHelper.PREFIX, EdmHelper.GetEdmModels());
GetEdmModels
函数返回一个 IEdmModel
对象。
public static IEdmModel GetEdmModels()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
var device = builder.EntitySet<DeviceInfo>("DeviceInfos").EntityType.HasKey(p => p.DeviceId);
device.Action("Upload");
builder.EnableLowerCamelCase();
return builder.GetEdmModel();
}
以上代码中对 DeviceInfo 类型定义了一个实体对象,其具有 DeviceId
作为主键,拥有一个名为 Upload
的 Action。并且对所有的 EDM 对象启用了 LowerCamelCase 支持。上面的感觉是挺简单的是吧,注意我们使用到了 ODataConventionModelBuilder
对象,这个对象帮助我们自动实现了很多配置内容。如果我们使用其他的方式就不那么简单了。实际上配置 EDM 总共有三种方式。
我一般只使用 Convention 的配置方法,因此这里引用官方网站的例子,详情请见 Introduction to the model builders - OData | Microsoft Learn
Explicit
如果模型通过 new EdmModel()
构建,那么构建的是无类型模型,相当于你不依赖现有的 CLR 类型凭空构建了一个模型。
public IEdmModel GetEdmModel()
{
EdmModel model = new EdmModel();
EdmEntityType customer = new EdmEntityType("WebApiDocNS", "Customer");
customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32));
customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true));
model.AddElement(customer);
EdmEntityType order = new EdmEntityType("WebApiDocNS", "Order");
order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32));
order.AddStructuralProperty("Token", EdmPrimitiveTypeKind.Guid);
model.AddElement(order);
return model;
}
Non-convention
如果模型是依赖 new ODataModelBuilder()
构建,那么构建模型时可以依据现有的 CLR 对象进行构建,不过依然需要配置每一个属性、操作等。
public static IEdmModel GetEdmModel()
{
var builder = new ODataModelBuilder();
var customer = builder.EntityType<Customer>();
customer.HasKey(c => c.CustomerId);
customer.ComplexProperty(c => c.Location);
customer.HasMany(c => c.Orders);
var order = builder.EntityType<Order>();
order.HasKey(o => o.OrderId);
order.Property(o => o.Token);
return builder.GetEdmModel();
}
相当于前一种方法已经有了很大改进,我们可以依赖现有的结构,而不再需要手动去命名了。
Convention
更进一步,我们可以使用惯例 ( Convention )方式,模型依赖 new ODataConventionModelBuilder ()
构建,这个是代码最少的,整个模型的配置按照 OData RESTful 惯例实现。
public static IEdmModel GetEdmModels()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
var device = builder.EntitySet<DeviceInfo>("DeviceInfos").EntityType;
return builder.GetEdmModel();
}
Convention 涉及的内容很多,有机会以后会详细解释惯例生成 EDM 这种模式。
常用 EDM 配置
EDM 配置项目繁多,我们常用的有:
- 配置实体(主键)
var device = builder.EntitySet<DeviceInfo>("DeviceInfos").EntityType;
- 配置(集合) Action
//对于实体上的Action
device.Action("Upload");
//对于集合上的Action
device.Collection.Action("Upload");
- 配置(集合) Function
//对于实体上的Function
device.Function("Data").Returns<string>();
//对于集合上的Function
device.Collection.Function("Data").Returns<string>();
对 Function 以及 Action 的详细介绍,请期待后续文章。
访问 EDM 模型
OData 服务提供了 $metadata
终结点,可以从服务的根 URL 后添加 $metadata
来访问。例如以下是一个可以直接访问的 EDM 模型,以 XML 形式提供:
http://services.odata.org/TripPinRESTierService/$metadata
此外,也可以使用某些工具(如 Microsoft 的 OData Connected Service)来自动生成客户端代码,并从服务中获取元数据。这些工具通常会从服务的根 URL 下载 $metadata 文件,并将其解析为客户端代码。