项目

定制应用模块:扩展实体

在某些情况下,您可能希望为依赖模块中定义的实体添加一些额外的属性(以及数据库字段)。本节将介绍几种不同的实现方法。

额外属性

额外属性 是一种在不改变实体本身的情况下存储额外数据的方式。实体需要实现IHasExtraProperties接口才能支持此功能。所有预构建模块中定义的聚合根实体都实现了IHasExtraProperties接口,因此您可以在这些对象上存储额外属性。

示例:

// 设置额外属性
var user = await _identityUserRepository.GetAsync(userId);
user.SetProperty("Title", "我的自定义标题值!");
await _identityUserRepository.UpdateAsync(user);

// 获取额外属性
var user = await _identityUserRepository.GetAsync(userId);
return user.GetProperty<string>("Title");

这种方法使用非常简单且开箱即用,无需编写额外代码。您可以通过使用不同的属性名称(如此处的Title)同时存储多个属性。

对于EF Core,额外属性以单个JSON格式字符串值的形式存储在数据库中。对于MongoDB,它们则作为文档的单独字段存储。

有关额外属性系统的更多信息,请参阅 实体文档

基于额外属性的值执行业务逻辑是可能的。您可以 重写服务方法 ,然后如上所示获取或设置值。

实体扩展(EF Core)

如前所述,实体的所有额外属性都以单个JSON对象的形式存储在数据库表中。这在某些场景下不够自然,特别是当您希望:

  • 为额外属性创建索引外键
  • 使用额外属性编写SQLLINQ查询(例如,按属性值搜索表)
  • 创建自己的实体映射到同一张表,但在实体中将额外属性定义为常规属性(更多内容请参阅 EF Core 迁移文档

为解决上述问题,ABP为Entity Framework Core提供了实体扩展系统,允许您使用上述相同的额外属性API,但将所需属性作为单独的字段存储在数据库表中。

假设您想为 身份模块IdentityUser 实体添加一个 SocialSecurityNumber 属性。您可以使用 ObjectExtensionManager

ObjectExtensionManager.Instance
    .MapEfCoreProperty<IdentityUser, string>(
        "SocialSecurityNumber",
        (entityBuilder, propertyBuilder) =>
        {
            propertyBuilder.HasMaxLength(32);
        }
    );
  • 您需要指定IdentityUser作为实体名称,string作为新属性的类型,SocialSecurityNumber作为属性名称(同时也是数据库表中的字段名称)
  • 您还需要提供一个操作,使用 EF Core Fluent API 定义数据库映射属性

这部分代码必须在相关DbContext使用之前执行。应用启动模板 定义了一个名为YourProjectNameEfCoreEntityExtensionMappings的静态类。您可以在此类中定义扩展,以确保在适当的时间执行。否则,您需要自行处理。

一旦定义了实体扩展,您需要使用EF Core标准的 Add-MigrationUpdate-Database 命令来创建代码优先迁移类并更新数据库。

然后,您可以使用上一节中定义的相同额外属性系统来操作实体上的属性。

创建映射到同一数据库表/集合的新实体

另一种方法是创建自己的实体,映射到同一数据库表(对于MongoDB数据库则是同一集合)。

创建具有独立数据库表/集合的新实体

将您的实体映射到依赖模块的现有表有一些缺点:

  • 对于EF Core,您需要处理数据库迁移结构。虽然这是可能的,但您需要特别小心迁移代码,尤其是在想要添加实体间关系
  • 您的应用程序数据库和模块数据库将是同一个物理数据库。通常,模块数据库可以根据需要分离,但使用同一张表会限制这一点

如果您希望与模块定义的实体松耦合,可以创建自己的数据库表/集合,并将您的实体映射到您自己数据库中的表。

在这种情况下,您需要处理同步问题,特别是如果您想复制相关实体的某些属性/字段。有几种解决方案:

  • 如果您正在构建单体应用程序(或在同一进程中管理您的实体和相关模块实体),可以使用 本地事件总线 来监听变更
  • 如果您正在构建分布式系统,其中模块实体在与您的实体管理的不同进程/服务中进行管理(创建/更新/删除),则可以订阅 分布式事件总线 来接收变更事件

一旦处理了事件,您就可以在自己的数据库中更新自己的实体。

订阅本地事件

本地事件总线 系统是一种发布和订阅同一应用程序中发生的事件的方式。

假设您希望在 IdentityUser 实体更改(创建、更新或删除)时获得通知。您可以创建一个实现 ILocalEventHandler<EntityChangedEventData<IdentityUser>> 接口的类。

public class MyLocalIdentityUserChangeEventHandler :
    ILocalEventHandler<EntityChangedEventData<IdentityUser>>,
    ITransientDependency
{
    public async Task HandleEventAsync(EntityChangedEventData<IdentityUser> eventData)
    {
        var userId = eventData.Entity.Id;
        var userName = eventData.Entity.UserName; 
        
        //...
    }
}
  • EntityChangedEventData<T>覆盖给定实体的创建、更新和删除事件。如果需要,您可以单独订阅创建、更新和删除事件(在同一类或不同类中)
  • 此代码将在当前工作单元中执行,整个过程具有事务性

提醒:此方法需要在包含处理程序类的同一进程中更改IdentityUser实体。即使在集群环境中(当同一应用程序的多个实例在多台服务器上运行时)也能完美工作

订阅分布式事件

分布式事件总线 系统是一种在一个应用程序中发布事件,并在同一或不同服务器上运行的同一或不同应用程序中接收事件的方式。

假设您希望在 租户管理 模块的Tenant实体创建时获得通知。在这种情况下,您可以订阅EntityCreatedEto<TenantEto>事件,如下例所示:

public class MyDistributedEventHandler :
    IDistributedEventHandler<EntityCreatedEto<TenantEto>>,
    ITransientDependency
{
    public async Task HandleEventAsync(EntityCreatedEto<TenantEto> eventData)
    {
        var tenantId = eventData.Entity.Id;
        var tenantName = eventData.Entity.Name;
        //...您的自定义逻辑
    }

    //...
}

此处理程序仅在新租户创建时执行。所有预构建的 ABP 应用模块 都为其实体定义了相应的 ETO 类型。因此,您可以轻松地在它们发生变化时获得通知。

请注意,ABP 默认不会为实体发布分布式事件。因为这需要成本,并且应该有意识地启用。更多信息请参阅 分布式事件总线文档

另请参阅

在本文档中