项目

实体

实体是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。它可以是其他类型,如 stringintlong 或您需要的任何类型。

具有GUID键的实体

如果实体的Id类型是 Guid,有一些最佳实践需要实现:

  • 创建一个构造函数,该函数以Id作为参数并将其传递给基类。
    • 如果您不设置GUID Id,ABP会在保存时设置它,但即使在保存到数据库之前就让实体拥有一个有效的Id也是很好的做法。
  • 如果您创建了一个带参数的构造函数,请同时创建一个 privateprotected 的空构造函数。这用于您的数据库提供程序从数据库(反序列化时)读取您的实体时。
  • 不要使用 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 插入数据库。
  • GuidGeneratorIGuidGenerator 类型,它是 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 };
    }
}

对于上面的例子,复合键由 UserIdRoleId 组成。对于关系数据库,它是相关表的复合主键。具有复合键的实体应如上所示实现 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 是另一个具有复合主键(OrderIdProductId)的实体。

虽然这个示例可能没有实现聚合根的所有最佳实践,但它仍然遵循了一些良好的实践:

  • Order 有一个公共构造函数,它接受构造 Order 实例的最小要求。因此,没有id和参考号就无法创建订单。protected/private 构造函数仅在从数据源读取对象进行反序列化时是必需的。
  • OrderLine 构造函数是 internal 的,因此只允许由领域层创建。它在 Order.AddProduct 方法内部使用。
  • Order.AddProduct 实现了向订单添加产品的业务规则。
  • 所有属性都有 protected 的setter。这是为了防止实体从外部任意更改。例如,在不向订单添加新产品的情况下设置 TotalItemCount 是危险的。它的值由 AddProduct 方法维护。

ABP并不强制您应用任何DDD规则或模式。但是,当您确实想应用它们时,它会使其成为可能且更容易。文档也遵循同样的原则。

具有复合键的聚合根

虽然对于聚合根来说不常见(也不建议),但事实上可以以与上述实体相同的方式定义复合键。在这种情况下,请使用非泛型 AggregateRoot 基类。

BasicAggregateRoot 类

AggregateRoot 类实现了 IHasExtraPropertiesIHasConcurrencyStamp 接口,这为派生类带来了两个属性。IHasExtraProperties 使实体可扩展(参见下面的额外属性部分),IHasConcurrencyStamp 添加了一个 ConcurrencyStamp 属性,该属性由ABP管理以实现 乐观并发 。在大多数情况下,这些是聚合根需要的特性。

但是,如果您不需要这些特性,可以为您的聚合根继承 BasicAggregateRoot<TKey>(或 BasicAggregateRoot)。

用于审计属性的基类和接口

有一些属性,如 CreationTimeCreatorIdLastModificationTime... 在所有应用程序中都非常常见。ABP提供了一些接口和基类来标准化这些属性,并自动设置它们的值。

审计接口

有很多审计接口,因此您可以根据需要实现相应的接口。

虽然您可以手动实现这些接口,但可以使用下一节中定义的基类来简化操作。

  • IHasCreationTime 定义了以下属性:
    • CreationTime
  • IMayHaveCreator 定义了以下属性:
    • CreatorId
  • ICreationAuditedObject 继承自 IHasCreationTimeIMayHaveCreator,因此定义了以下属性:
    • CreationTime
    • CreatorId
  • IHasModificationTime 定义了以下属性:
    • LastModificationTime
  • IModificationAuditedObject 扩展了 IHasModificationTime 并添加了 LastModifierId 属性。因此,它定义了以下属性:
    • LastModificationTime
    • LastModifierId
  • IAuditedObject 扩展了 ICreationAuditedObjectIModificationAuditedObject,因此定义了以下属性:
    • CreationTime
    • CreatorId
    • LastModificationTime
    • LastModifierId
  • ISoftDelete(参见 数据过滤文档 )定义了以下属性:
    • IsDeleted
  • IHasDeletionTime 扩展了 ISoftDelete 并添加了 DeletionTime 属性。因此,它定义了以下属性:
    • IsDeleted
    • DeletionTime
  • IDeletionAuditedObject 扩展了 IHasDeletionTime 并添加了 DeleterId 属性。因此,它定义了以下属性:
    • IsDeleted
    • DeletionTime
    • DeleterId
  • IFullAuditedObject 继承自 IAuditedObjectIDeletionAuditedObject,因此定义了以下属性:
    • CreationTime
    • CreatorId
    • LastModificationTime
    • LastModifierId
    • IsDeleted
    • DeletionTime
    • DeleterId

一旦您实现了任何接口,或从下一节定义的类派生,ABP就会在可能的情况下自动管理这些属性。

实现 ISoftDeleteIDeletionAuditedObjectIFullAuditedObject 会使您的实体软删除。有关软删除模式,请参阅 数据过滤文档

审计基类

虽然您可以手动实现上述任何接口,但建议继承此处定义的基类:

  • CreationAuditedEntity<TKey>CreationAuditedAggregateRoot<TKey> 实现了 ICreationAuditedObject 接口。
  • AuditedEntity<TKey>AuditedAggregateRoot<TKey> 实现了 IAuditedObject 接口。
  • FullAuditedEntity<TKey>FullAuditedAggregateRoot<TKey> 实现了 IFullAuditedObject 接口。

所有基类也有非泛型版本,如 AuditedEntityFullAuditedAggregateRoot,以支持复合主键。

所有基类还有 ...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。但是,SetPropertyGetProperty 方法是推荐的方式,因为它们还会检查 null

如何存储?

这个字典在数据库中的存储方式取决于您使用的数据库提供程序。

  • 对于 Entity Framework Core,有两种配置类型;
    • 默认情况下,它存储在单个 ExtraProperties 字段中作为 JSON 字符串(这意味着所有额外属性都存储在单个数据库表字段中)。通过EF Core的 值转换 系统,ABP自动完成序列化为JSON和从JSON反序列化。
    • 如果您愿意,可以使用 ObjectExtensionManager 为所需的额外属性定义单独的表字段。未通过 ObjectExtensionManager 配置的属性将继续使用单个 JSON 字段,如上所述。当您使用预构建的 应用程序模块 并希望 扩展其实体 时,此功能特别有用。有关如何使用 ObjectExtensionManager,请参阅 EF Core集成文档
  • 对于 MongoDB ,它存储为常规字段,因为 MongoDB 本身支持这种 额外元素 系统。

关于额外属性的讨论

额外属性系统特别有用,如果您正在使用一个可重用模块,该模块内部定义了一个实体,并且您想以简单的方式获取/设置与此实体相关的一些数据。

您通常不需要为您自己的实体使用此系统,因为它有以下缺点:

  • 由于它使用字符串作为属性名称,因此不完全类型安全
  • 将这些属性 自动映射 到/从其他对象并不容易

实体背后的额外属性

IHasExtraProperties 不限于与实体一起使用。您可以为任何类型的类实现此接口,并使用 GetPropertySetProperty 和其他相关方法。

另请参阅

在本文档中