自定义应用模块: 扩展实体

在某些情况下你可能希望为依赖模块中定义的实体添加一些额外的属性(和数据库字段). 本节将介绍一些实现这一目标的不同方法.

Extra Properties

Extra properties是一种存储实体的一些额外数据但不用更改实体的方式. 实体应该实现 IHasExtraProperties 接口. 所有预构建模块定义的聚合根实体都实现了 IHasExtraProperties 接口,所以你可以在这些实体中存储额外的属性.

示例:

  1. //SET AN EXTRA PROPERTY
  2. var user = await _identityUserRepository.GetAsync(userId);
  3. user.SetProperty("Title", "My custom title value!");
  4. await _identityUserRepository.UpdateAsync(user);
  5. //GET AN EXTRA PROPERTY
  6. var user = await _identityUserRepository.GetAsync(userId);
  7. return user.GetProperty<string>("Title");

这种方法开箱即用并且非常简单,你可以使用不同的属性名称(如这里的Title)在同一时间存储多个属性.

对于EF Core额外的属性被格式化成单个 JSON 字符值串存储在数据库中. 对于MongoDB它们做为单独的字段存储.

参阅实体文档了解更多关于额外系统.

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

实体扩展 (EF Core)

如上所述,实体所有的额外属性都作为单个JSON对象存储在数据库表中. 它不适用复杂的场景,特别是在你需要的时候.

  • 使用额外属性创建索引外键.
  • 使用额外属性编写SQLLINQ(例如根据属性值搜索).
  • 创建你自己的实体映射到相同的表,但在实体中定义一个额外属性做为 常规属性(参阅 EF Core迁移文档了解更多).

为了解决上面的问题,用于EF Core的ABP框架实体扩展系统允许你使用上面定义相同的额外属性API,但将所需的属性存储在单独的数据库表字段中.

假设你想要添加 SocialSecurityNumber身份模块IdentityUser 实体. 你可以使用 ObjectExtensionManager 类:

  1. ObjectExtensionManager.Instance
  2. .MapEfCoreProperty<IdentityUser, string>(
  3. "SocialSecurityNumber",
  4. (entityBuilder, propertyBuilder) =>
  5. {
  6. propertyBuilder.HasMaxLength(32);
  7. }
  8. );
  • 你提供了 IdentityUser 作为实体名(泛型参数), string 做为新属性的类型, SocialSecurityNumber 做为属性名(也是数据库表的字段名).
  • 你还需要提供一个使用EF Core Fluent API定义数据库映射属性的操作.

必须在使用相关的 DbContext 之前执行此代码. 应用程序启动模板定义了一个名为 YourProjectNameEfCoreEntityExtensionMappings 的静态类. 你可以在此类中定义扩展确保在正确的时间执行它. 否则你需要自己处理.

定义实体扩展后你需要使用EF Core的Add-MigrationUpdate-Database命令来创建code first迁移类并更新数据库.

然后你可以使用上一部分中定义的相同额外属性系统来操纵实体上的属性.

创建新实体映射到同一个数据库表/Collection

另一个方法是创建你自己的实体映射到同一个数据库库(对于MongoDB数据库是collection)

应用程序启动模板AppUser 已经实现了这种方法. EF Core迁移文档描述了在这些情况下如何实现和管理EF Core数据库迁移. 这种方法同样适用于MongoDB,但你不需要处理数据库迁移问题.

创建一个拥有自己数据库表/Collection的新实体

映射你的实体到依赖模块的已存在的表有一些缺点;

  • 你需要处理EF Core的数据库迁移架构. 需要特别注意迁移代码,特别是当你需要在实体间添加关系时.
  • 你的应用程序数据库和模块数据库将是 同一个物理数据库. 通常需要时可以将模块数据库分开,但使用相同的表会对其进行限制.

如果你想要使你的实体或模块定义的实体低耦合,那么可以创建自己的数据库表/collection并且将你的实体映射到自己的数据库表.

在这种情况下你需要处理同步问题,尤其是你要复制相关实体的某些属性/字段时,有一些解决方案;

  • 如果你构建的是一个 单体 应用程序(或者在同一进程管理你的实体和依赖模块的实体),那么你可以使用本地事件总线监听实体更改.
  • 如果你构建的是一个 分布式 系统,模块的实体和你的实体在不同的 进程/服务 管理(创建/更新/删除),那么你可以使用分布式事件总线订阅实体的更改事件.

在你处理事件时,你可以在自己的数据库中更改自己的实体.

订阅本地事件总线

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

假设你想要获取 IdentityUser 实体的更改信息(创建,更改或删除). 你可以创建一个类实现 ILocalEventHandler<EntityChangedEventData<IdentityUser>> 接口.

  1. public class MyLocalIdentityUserChangeEventHandler :
  2. ILocalEventHandler<EntityChangedEventData<IdentityUser>>,
  3. ITransientDependency
  4. {
  5. public async Task HandleEventAsync(EntityChangedEventData<IdentityUser> eventData)
  6. {
  7. var userId = eventData.Entity.Id;
  8. var userName = eventData.Entity.UserName;
  9. //...
  10. }
  11. }
  • EntityChangedEventData<T> 涵盖了给定实体的创建,更新或删除事件. 如果你需要你可以分别订阅创建,更新或删除事件(在同一个类或不同的类中).
  • 这里的代码在本地事务之外执行,因为它监听 EntityChanged 事件. 如果当前工作单元是事务性的,你可以订阅 EntityChangingEventData<T> 事件,它在同一本地(进行)事务中执行事件处理.

提醒:这些方法需要在包含处理类的同一进程中更改 IdentityUser 实体. 即使在集群环境(同一应用程序的多个实例在不同的服务器进行),它也完美工作.

订阅分布式事件总线

分布式事件总线是在一个应用程序中发布事件,并在相同服务器或不同服务器运行的相同应用程序或不同应用程序中接收事件的方法.

假设你想要获取 IdentityUser 实体的创建,更改或删除信息. 你可以像以下一样创建一个类:

  1. public class MyDistributedIdentityUserChangeEventHandler :
  2. IDistributedEventHandler<EntityCreatedEto<EntityEto>>,
  3. IDistributedEventHandler<EntityUpdatedEto<EntityEto>>,
  4. IDistributedEventHandler<EntityDeletedEto<EntityEto>>,
  5. ITransientDependency
  6. {
  7. public async Task HandleEventAsync(EntityCreatedEto<EntityEto> eventData)
  8. {
  9. if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser")
  10. {
  11. var userId = Guid.Parse(eventData.Entity.KeysAsString);
  12. //...handle the "created" event
  13. }
  14. }
  15. public async Task HandleEventAsync(EntityUpdatedEto<EntityEto> eventData)
  16. {
  17. if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser")
  18. {
  19. var userId = Guid.Parse(eventData.Entity.KeysAsString);
  20. //...handle the "updated" event
  21. }
  22. }
  23. public async Task HandleEventAsync(EntityDeletedEto<EntityEto> eventData)
  24. {
  25. if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser")
  26. {
  27. var userId = Guid.Parse(eventData.Entity.KeysAsString);
  28. //...handle the "deleted" event
  29. }
  30. }
  31. }
  • 它实现了多个 IDistributedEventHandler 接口: 创建,更改删除,因为分布式事件总线单独发布事件,没有本地事件总线那样的”Changed”事件.
  • 它订阅了 EntityEto, 这是一个通用的事件类,ABP框架针对所有类型的实体自动发布. 这就是为什么它检查实体类型(因为我们没有假设有对 IdentityUser 实体有安全的类型引用,所以它是字符串类型的).

预构建应用模块没有定义专门的事件类型(如IdentityUserEto - “ETO” 意思是 “事件传输对象”). 此功能在路线图上(关注这个issue),一旦完成后,你就可以订阅独立的实体类型:

  1. public class MyDistributedIdentityUserCreatedEventHandler :
  2. IDistributedEventHandler<EntityCreatedEto<IdentityUserEto>>,
  3. ITransientDependency
  4. {
  5. public async Task HandleEventAsync(EntityCreatedEto<IdentityUserEto> eventData)
  6. {
  7. var userId = eventData.Entity.Id;
  8. var userName = eventData.Entity.UserName;
  9. //...handle the "created" event
  10. }
  11. //...
  12. }
  • 这个处理程序只会在新用户创建时执行.

唯一预定义的专门事件类是 UserEto, 你可以订阅 EntityCreatedEto<UserEto> 获取用户创建时的通知. 此事件也适用于身份模块.

另请参阅