實體是DDD(Domain Driven Design)中核心概念.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.
- 如果使用帶參數的構造函數創建實體,那麼還要創建一個
protected
構造函數. 當數據庫提供程序從數據庫讀取你的實體時(反序列化時)將使用它. - 不要使用
Guid.NewGuid()
來設置Id! 在創建實體的代碼中使用IGuidGenerator服務傳遞Id參數.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
注入圖書實體的默認倉庫,使用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>。
更多信息請參見倉儲的文檔。
聚合根
"聚合是域驅動設計中的一種模式.DDD的聚合是一組可以作爲一個單元處理的域對象。例如,訂單及訂單系列的商品,這些是獨立的對象,但將訂單(連同訂單系列的商品)視爲一個聚合通常是很有用的"( 查看詳細介紹)
AggregateRoot<TKey>
類繼承自Entity<TKey>
類,所以默認有Id
這個屬性
值得注意的是 ABP 會默認爲聚合根創建倉儲,當然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
是一個具有組合鍵(OrderLine
和 ProductId
)的實體。
雖然這個示例可能無法實現聚合根的所有最佳實踐,但它仍然遵循良好的實踐:
Order
有一個公共的構造函數,它需要 minimal requirements 來構造一個"訂單"實例.因此,在沒有Id
和referenceNo
的時候是無法創建訂單的.protected/private的構造函數只有從數據庫讀取對象時 反序列化 才需要。OrderLine
的構造函數是internal的,所以它只能由領域層來創建.在Order.AddProduct
這個方法的內部被使用。Order.AddProduct
實現了業務規則將商品添加到訂單中。- 所有屬性都有
protected
的set.這是爲了防止實體在實體外部任意改變.因此,在沒有向訂單中添加新產品的情況下設置TotalItemCount
將是危險的.它的值由AddProduct
方法維護。
ABP框架不強制你應用任何DDD規則或模式.但是,當你準備應用的DDD規則或模式時候,ABP會讓這變的可能而且更簡單.文檔同樣遵循這個原則。
帶有組合鍵的聚合根
雖然這種聚合根並不常見(也不建議使用),但實際上可以按照與上面提到的跟實體相同的方式定義複合鍵.在這種情況下,要使用非泛型的AggregateRoot
基類。
基類和接口的審計屬性
有一些屬性,像CreationTime
,CreatorId
,LastModificationTime
...在所有應用中都很常見. ABP框架提供了一些接口和基類來標準化這些屬性,並自動設置它們的值。
審計接口
有很多的審計接口,你可以選擇一個你需要的來實現它。
雖然可以手動實現這些接口,但是可以使用下一節中定義的基類簡化代碼。
IHasCreationTime
定義了以下屬性:CreationTime
IMayHaveCreator
定義了以下屬性:CreatorId
ICreationAuditedObject
繼承IHasCreationTime
和IMayHaveCreator
, 所以它定義了以下屬性:CreationTime
CreatorId
IHasModificationTime
定義了以下屬性:LastModificationTime
IModificationAuditedObject
擴展IHasModificationTime
並添加了LastModifierId
屬性. 所以它定義了以下屬性:LastModificationTime
LastModifierId
IAuditedObject
擴展ICreationAuditedObject
和IModificationAuditedObject
, 所以它定義了以下屬性:CreationTime
CreatorId
LastModificationTime
LastModifierId
ISoftDelete
(參閱 數據過濾文檔) 定義了以下屬性:IsDeleted
IHasDeletionTime
擴展ISoftDelete
並添加了DeletionTime
屬性. 所以它定義了以下屬性:IsDeleted
DeletionTime
IDeletionAuditedObject
擴展IHasDeletionTime
並添加了DeleterId
屬性. 所以它定義了以下屬性:IsDeleted
DeletionTime
DeleterId
IFullAuditedObject
繼承IAuditedObject
和IDeletionAuditedObject
, 所以它定義了以下屬性:CreationTime
CreatorId
LastModificationTime
LastModifierId
IsDeleted
DeletionTime
DeleterId
當你實現了任意接口,或者從下一節定義的類派生,ABP框架就會儘可能地自動管理這些屬性。
實現
ISoftDelete
,IDeletionAuditedObject
或IFullAuditedObject
讓你的實體軟刪除。參閱數據過濾文檔,瞭解軟刪除模式。
審計基類
雖然可以手動實現以上定義的任何接口,但建議從這裏定義的基類繼承:
CreationAuditedEntity<TKey>
和CreationAuditedAggregateRoot<TKey>
實現了ICreationAuditedObject
接口.AuditedEntity<TKey>
和AuditedAggregateRoot<TKey>
實現了IAuditedObject
接口.FullAuditedEntity<TKey>
andFullAuditedAggregateRoot<TKey>
實現了IFullAuditedObject
接口.
所有這些基類都有非泛型版本,可以使用 AuditedEntity
和 FullAuditedAggregateRoot
來支持複合主鍵;
所有這些基類也有 ... WithUser
,像 FullAuditedAggregateRootWithUser<TUser>
和 FullAuditedAggregateRootWithUser<TKey, TUser>。
這樣就可以將導航屬性添加到你的用戶實體. 但在聚合根之間添加導航屬性不是一個好做法,所以這種用法是不建議的(除非你使用EF Core之類的ORM可以很好地支持這種情況,並且你真的需要它. 請記住這種方法不適用於NoSQL數據庫(如MongoDB),你必須真正實現聚合模式)。
額外的屬性
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);
//SET A PROPERTY
user.SetProperty("Title", title);
await _identityUserRepository.UpdateAsync(user);
}
public async Task<string> GetTitle(Guid userId)
{
var user = await _identityUserRepository.GetAsync(userId);
//GET A PROPERTY
return user.GetProperty<string>("Title");
}
}
- 屬性的值是object,可以是任何類型的對象(string,int,bool...等)。
- 如果給定的屬性未設置值,
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
要求實現類定義一個名稱爲 ExtraProperties
的Dictionary<string, object>
屬性。
所以,如果你需要你可以直接使用 ExtraProperties
屬性來使用字典API,但是推薦使用 SetProperty
和 GetProperty
方法,因爲它們會檢查 null
值。
它是如何存儲的?
存儲字典的方式取決於你使用的數據庫提供程序.
- 對於 Entity Framework Core,這是兩種類型的配置;
- 默認它以
JSON
字符串形式存儲在ExtraProperties
字段中. 序列化到JSON
和反序列化到JSON
由ABP使用EF Core的值轉換系統自動完成. - 如果需要,你可以使用
ObjectExtensionManager
爲所需的額外屬性定義一個單獨的數據庫字段. 那些使用ObjectExtensionManager
配置的屬性繼續使用單個JSON
字段. 當你使用預構建的應用模塊並且想要擴展模塊的實體. 參閱EF Core遷移文檔瞭解如何使用ObjectExtensionManager
.
- 默認它以
- 對於 MongoDB, 它以 常規字段 存儲, 因爲 MongoDB 天生支持這種 額外 系統.
討論額外的屬性
如果你使用可重複使用的模塊,其中定義了一個實體,你想使用簡單的方式get/set此實體相關的一些數據,那麼額外的屬性系統是非常有用的. 你通常 不需要 爲自己的實體使用這個系統,是因爲它有以下缺點:
- 它不是完全類型安全的,因爲它使用字符串用作屬性名稱.
- 這些屬性不容易自動映射到其他對象.
- 它不會爲EF Core在數據庫表中創建字段,因此在數據庫中針對這個字段創建索引或搜索/排序並不容易.
額外屬性背後的實體
IHasExtraProperties
不限於與實體一起使用. 你可以爲任何類型的類實現這個接口,使用 GetProperty
,SetProperty
和其他相關方法。