实体
实体是DDD(领域驱动设计)的核心概念之一。Eric Evans将其描述为“一个并非由其属性从根本上定义,而是由连续性和标识的线索定义的对象”。
实体通常映射到关系数据库中的一个表。
实体类
实体派生自 Entity<TKey> 类,如下所示:
public class Book : Entity<Guid>
{
public string Name { get; set; }
public float Price { get; set; }
}
如果您不想让您的实体从基类
Entity<TKey>派生,可以直接实现IEntity<TKey>接口。
Entity<TKey> 类只定义了一个具有给定主键类型的 Id 属性,在上面的例子中是 Guid。它可以是其他类型,如 string、int、long 或您需要的任何类型。
具有GUID键的实体
如果实体的Id类型是 Guid,有一些最佳实践需要实现:
- 创建一个构造函数,该函数以Id作为参数并将其传递给基类。
- 如果您不设置GUID Id,ABP会在保存时设置它,但即使在保存到数据库之前就让实体拥有一个有效的Id也是很好的做法。
- 如果您创建了一个带参数的构造函数,请同时创建一个
private或protected的空构造函数。这用于您的数据库提供程序从数据库(反序列化时)读取您的实体时。 - 不要使用
Guid.NewGuid()来设置Id!在创建实体的代码中传递Id时,请使用IGuidGenerator服务。IGuidGenerator经过优化以生成顺序GUID,这对于关系数据库中的聚集索引至关重要。
一个实体示例:
public class Book : Entity<Guid>
{
public string Name { get; set; }
public float Price { get; set; }
protected Book()
{
}
public Book(Guid id)
: base(id)
{
}
}
在 应用服务 中的使用示例:
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book> _bookRepository;
public BookAppService(IRepository<Book> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task CreateAsync(CreateBookDto input)
{
await _bookRepository.InsertAsync(
new Book(GuidGenerator.Create())
{
Name = input.Name,
Price = input.Price
}
);
}
}
BookAppService为book实体注入了默认的 存储库 并使用其InsertAsync方法将Book插入数据库。GuidGenerator是IGuidGenerator类型,它是ApplicationService基类中定义的属性。ABP为您预注入了一些常用的基类属性,因此您不需要手动 注入 它们。- 如果您想遵循DDD最佳实践,请参阅下面的聚合示例部分。
具有复合键的实体
有些实体可能需要有复合键。在这种情况下,您可以从非泛型 Entity 类派生您的实体。示例:
public class UserRole : Entity
{
public Guid UserId { get; set; }
public Guid RoleId { get; set; }
public DateTime CreationTime { get; set; }
public UserRole()
{
}
public override object[] GetKeys()
{
return new object[] { UserId, RoleId };
}
}
对于上面的例子,复合键由 UserId 和 RoleId 组成。对于关系数据库,它是相关表的复合主键。具有复合键的实体应如上所示实现 GetKeys() 方法。
注意,您还需要在您的对象关系映射(ORM)配置中定义实体的键。例如,请参阅 Entity Framework Core 集成文档。
还要注意,具有复合主键的实体无法利用
IRepository<TEntity, TKey>接口,因为它需要一个单独的Id属性。但是,您可以始终使用IRepository<TEntity>。有关更多信息,请参阅 存储库文档 。
EntityEquals
Entity.EntityEquals(...) 方法用于检查两个实体对象是否相等。
示例:
Book book1 = ...
Book book2 = ...
if (book1.EntityEquals(book2)) //检查相等性
{
...
}
聚合根类
“聚合是领域驱动设计中的一种模式。DDD聚合是可以被视为单个单元的领域对象集群。一个例子可能是订单及其行项目,这些是单独的对象,但将订单(连同其行项目)视为单个聚合很有用。”(参见 完整描述 )
AggregateRoot<TKey> 类扩展了 Entity<TKey> 类。因此,默认情况下它也有一个 Id 属性。
请注意,默认情况下ABP只为聚合根创建默认存储库。但是,可以包含所有实体。更多信息请参阅 存储库文档 。
ABP并不强制您使用聚合根,实际上您可以使用之前定义的 Entity 类。但是,如果您想实现 领域驱动设计 并想创建聚合根类,有一些最佳实践您可能需要考虑:
- 聚合根负责维护其自身的完整性。这对所有实体都是如此,但聚合根也对其子实体负有责任。因此,聚合根必须始终处于有效状态。
- 聚合根可以通过其
Id引用。不要通过其导航属性引用它。 - 聚合根被视为一个单元。它作为一个单元被检索和更新。通常它被视为事务边界。
- 通过聚合根处理子实体——不要独立修改它们。
如果您想在应用程序中实现DDD,请参阅 实体设计最佳实践指南 。
聚合示例
这是一个包含相关子实体集合的聚合根的完整示例:
public class Order : AggregateRoot<Guid>
{
public virtual string ReferenceNo { get; protected set; }
public virtual int TotalItemCount { get; protected set; }
public virtual DateTime CreationTime { get; protected set; }
public virtual List<OrderLine> OrderLines { get; protected set; }
protected Order()
{
}
public Order(Guid id, string referenceNo)
{
Check.NotNull(referenceNo, nameof(referenceNo));
Id = id;
ReferenceNo = referenceNo;
OrderLines = new List<OrderLine>();
}
public void AddProduct(Guid productId, int count)
{
if (count <= 0)
{
throw new ArgumentException(
"You can not add zero or negative count of products!",
nameof(count)
);
}
var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId);
if (existingLine == null)
{
OrderLines.Add(new OrderLine(this.Id, productId, count));
}
else
{
existingLine.ChangeCount(existingLine.Count + count);
}
TotalItemCount += count;
}
}
public class OrderLine : Entity
{
public virtual Guid OrderId { get; protected set; }
public virtual Guid ProductId { get; protected set; }
public virtual int Count { get; protected set; }
protected OrderLine()
{
}
internal OrderLine(Guid orderId, Guid productId, int count)
{
OrderId = orderId;
ProductId = productId;
Count = count;
}
internal void ChangeCount(int newCount)
{
Count = newCount;
}
public override object[] GetKeys()
{
return new Object[] {OrderId, ProductId};
}
}
如果您不想让您的聚合根从基类
AggregateRoot<TKey>派生,可以直接实现IAggregateRoot<TKey>接口。
Order 是一个具有 Guid 类型 Id 属性的聚合根。它有一个 OrderLine 实体集合。OrderLine 是另一个具有复合主键(OrderId 和 ProductId)的实体。
虽然这个示例可能没有实现聚合根的所有最佳实践,但它仍然遵循了一些良好的实践:
Order有一个公共构造函数,它接受构造Order实例的最小要求。因此,没有id和参考号就无法创建订单。protected/private 构造函数仅在从数据源读取对象进行反序列化时是必需的。OrderLine构造函数是 internal 的,因此只允许由领域层创建。它在Order.AddProduct方法内部使用。Order.AddProduct实现了向订单添加产品的业务规则。- 所有属性都有
protected的setter。这是为了防止实体从外部任意更改。例如,在不向订单添加新产品的情况下设置TotalItemCount是危险的。它的值由AddProduct方法维护。
ABP并不强制您应用任何DDD规则或模式。但是,当您确实想应用它们时,它会使其成为可能且更容易。文档也遵循同样的原则。
具有复合键的聚合根
虽然对于聚合根来说不常见(也不建议),但事实上可以以与上述实体相同的方式定义复合键。在这种情况下,请使用非泛型 AggregateRoot 基类。
BasicAggregateRoot 类
AggregateRoot 类实现了 IHasExtraProperties 和 IHasConcurrencyStamp 接口,这为派生类带来了两个属性。IHasExtraProperties 使实体可扩展(参见下面的额外属性部分),IHasConcurrencyStamp 添加了一个 ConcurrencyStamp 属性,该属性由ABP管理以实现 乐观并发 。在大多数情况下,这些是聚合根需要的特性。
但是,如果您不需要这些特性,可以为您的聚合根继承 BasicAggregateRoot<TKey>(或 BasicAggregateRoot)。
用于审计属性的基类和接口
有一些属性,如 CreationTime、CreatorId、LastModificationTime... 在所有应用程序中都非常常见。ABP提供了一些接口和基类来标准化这些属性,并自动设置它们的值。
审计接口
有很多审计接口,因此您可以根据需要实现相应的接口。
虽然您可以手动实现这些接口,但可以使用下一节中定义的基类来简化操作。
IHasCreationTime定义了以下属性:CreationTime
IMayHaveCreator定义了以下属性:CreatorId
ICreationAuditedObject继承自IHasCreationTime和IMayHaveCreator,因此定义了以下属性:CreationTimeCreatorId
IHasModificationTime定义了以下属性:LastModificationTime
IModificationAuditedObject扩展了IHasModificationTime并添加了LastModifierId属性。因此,它定义了以下属性:LastModificationTimeLastModifierId
IAuditedObject扩展了ICreationAuditedObject和IModificationAuditedObject,因此定义了以下属性:CreationTimeCreatorIdLastModificationTimeLastModifierId
ISoftDelete(参见 数据过滤文档 )定义了以下属性:IsDeleted
IHasDeletionTime扩展了ISoftDelete并添加了DeletionTime属性。因此,它定义了以下属性:IsDeletedDeletionTime
IDeletionAuditedObject扩展了IHasDeletionTime并添加了DeleterId属性。因此,它定义了以下属性:IsDeletedDeletionTimeDeleterId
IFullAuditedObject继承自IAuditedObject和IDeletionAuditedObject,因此定义了以下属性:CreationTimeCreatorIdLastModificationTimeLastModifierIdIsDeletedDeletionTimeDeleterId
一旦您实现了任何接口,或从下一节定义的类派生,ABP就会在可能的情况下自动管理这些属性。
实现
ISoftDelete、IDeletionAuditedObject或IFullAuditedObject会使您的实体软删除。有关软删除模式,请参阅 数据过滤文档 。
审计基类
虽然您可以手动实现上述任何接口,但建议继承此处定义的基类:
CreationAuditedEntity<TKey>和CreationAuditedAggregateRoot<TKey>实现了ICreationAuditedObject接口。AuditedEntity<TKey>和AuditedAggregateRoot<TKey>实现了IAuditedObject接口。FullAuditedEntity<TKey>和FullAuditedAggregateRoot<TKey>实现了IFullAuditedObject接口。
所有基类也有非泛型版本,如 AuditedEntity 和 FullAuditedAggregateRoot,以支持复合主键。
所有基类还有 ...WithUser 配对,如 FullAuditedAggregateRootWithUser<TUser> 和 FullAuditedAggregateRootWithUser<TKey, TUser>。这使得可以向用户实体添加导航属性。但是,在聚合根之间添加导航属性并不是一个好做法,因此不推荐使用这种方法(除非您使用的是像EF Core这样很好支持此场景的ORM,并且您确实需要它——否则请记住,这种方法不适用于像MongoDB这样的NoSQL数据库,您必须真正实现聚合模式)。此外,如果您向启动模板附带的AppUser类添加导航属性,请考虑在迁移dbcontext上处理(忽略/映射)它(参见 EF Core迁移文档 )。
缓存实体
ABP提供了一个 分布式实体缓存系统 用于缓存实体。如果您想使用缓存来更快地访问实体,而不是反复从数据库查询,这将非常有用。
它被设计为只读,并在实体更新或删除时自动使缓存的实体失效。
更多信息请参阅 实体缓存 文档。
版本化实体
ABP定义了 IHasEntityVersion 接口,用于自动对实体进行版本控制。它只提供了一个 EntityVersion 属性,如下面的代码块所示:
public interface IHasEntityVersion
{
int EntityVersion { get; }
}
如果您实现了 IHasEntityVersion 接口,每当您更新实体时,ABP会自动增加 EntityVersion 值。首次创建实体并保存到数据库时,初始 EntityVersion 值为 0。
如果您直接在数据库中执行SQL
UPDATE命令,ABP无法增加版本。在这种情况下,增加EntityVersion值是您的责任。此外,如果您使用聚合模式并更改聚合根的子集合,如果您想增加聚合根对象的版本,这也是您的责任。
额外属性
ABP定义了 IHasExtraProperties 接口,实体可以实现该接口以能够动态设置和获取实体的属性。AggregateRoot 基类已经实现了 IHasExtraProperties 接口。如果您从此类(或上面定义的任何相关审计类)派生,可以直接使用API。
GetProperty 和 SetProperty 扩展方法
这些扩展方法是获取和设置实体数据的推荐方式。示例:
public class ExtraPropertiesDemoService : ITransientDependency
{
private readonly IIdentityUserRepository _identityUserRepository;
public ExtraPropertiesDemoService(IIdentityUserRepository identityUserRepository)
{
_identityUserRepository = identityUserRepository;
}
public async Task SetTitle(Guid userId, string title)
{
var user = await _identityUserRepository.GetAsync(userId);
//设置属性
user.SetProperty("Title", title);
await _identityUserRepository.UpdateAsync(user);
}
public async Task<string> GetTitle(Guid userId)
{
var user = await _identityUserRepository.GetAsync(userId);
//获取属性
return user.GetProperty<string>("Title");
}
}
- 属性的值是对象,可以是任何类型的对象(字符串、整数、布尔值...等)。
- 如果之前未设置给定属性,
GetProperty返回null。 - 您可以使用不同的属性名称(如此处的
Title)同时存储多个属性。
为属性名称定义一个常量以防止拼写错误是一个好做法。定义扩展方法以利用智能感知是一个更好的做法。示例:
public static class IdentityUserExtensions
{
private const string TitlePropertyName = "Title";
public static void SetTitle(this IdentityUser user, string title)
{
user.SetProperty(TitlePropertyName, title);
}
public static string GetTitle(this IdentityUser user)
{
return user.GetProperty<string>(TitlePropertyName);
}
}
然后,您可以直接对 IdentityUser 对象使用 user.SetTitle("...") 和 user.GetTitle()。
HasProperty 和 RemoveProperty 扩展方法
HasProperty用于检查对象之前是否设置了某个属性。RemoveProperty用于从对象中移除属性。您可以使用此方法而不是设置null值。
它是如何实现的?
IHasExtraProperties 接口要求为实现的类定义一个 Dictionary<string, object> 属性,名为 ExtraProperties。
因此,如果您愿意,可以直接使用 ExtraProperties 属性来使用字典API。但是,SetProperty 和 GetProperty 方法是推荐的方式,因为它们还会检查 null。
如何存储?
这个字典在数据库中的存储方式取决于您使用的数据库提供程序。
- 对于 Entity Framework Core,有两种配置类型;
- 默认情况下,它存储在单个
ExtraProperties字段中作为JSON字符串(这意味着所有额外属性都存储在单个数据库表字段中)。通过EF Core的 值转换 系统,ABP自动完成序列化为JSON和从JSON反序列化。 - 如果您愿意,可以使用
ObjectExtensionManager为所需的额外属性定义单独的表字段。未通过ObjectExtensionManager配置的属性将继续使用单个JSON字段,如上所述。当您使用预构建的 应用程序模块 并希望 扩展其实体 时,此功能特别有用。有关如何使用ObjectExtensionManager,请参阅 EF Core集成文档 。
- 默认情况下,它存储在单个
- 对于 MongoDB ,它存储为常规字段,因为 MongoDB 本身支持这种 额外元素 系统。
关于额外属性的讨论
额外属性系统特别有用,如果您正在使用一个可重用模块,该模块内部定义了一个实体,并且您想以简单的方式获取/设置与此实体相关的一些数据。
您通常不需要为您自己的实体使用此系统,因为它有以下缺点:
- 由于它使用字符串作为属性名称,因此不完全类型安全。
- 将这些属性 自动映射 到/从其他对象并不容易。
实体背后的额外属性
IHasExtraProperties 不限于与实体一起使用。您可以为任何类型的类实现此接口,并使用 GetProperty、SetProperty 和其他相关方法。
抠丁客


