添加實體
這里我以問答模塊為例,記錄一下我在創建實體類過程中碰到的一些坑。

審計屬性
具體什么是審計屬性我這里就不再介紹了,大家可以參考官方文檔。
這里我是通過繼承定義好的基類來獲得相應的審計屬性,大家如果有需求的話,也可以自己通過接口定義。
其中,abp提供的審計基類有兩種,一種只包含UserId的FullAuditedEntity<TPrimaryKey>,另一種則是添加了User的導航屬性的FullAuditedEntity<TPrimaryKey, TUser>,后一種可方便之后用AutoMapper來獲取用戶信息。
FullAuditedEntity實質為FullAuditedEntity<int>
這里可能會出現的坑就是一時手誤會寫成FullAuditedEntity<User>,這樣的話它是把User類型實體的主鍵,算是不容易察覺的坑。
一對多關系
根據約定,在定義好實體間導航關系之后,EF Core會為其自動創建關系。
但在實際開發中,有時我們並不希望將一些導航屬性暴露出來,例如:Image類理應包含指向Question和Answer的導航屬性。為此,我們可以通過隱藏屬性(Shadow Properties)來化解這一尷尬。
在QincaiDbContext中,我們重載OnModelCreating方法:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Image>(e =>
{
// 添加隱藏屬性
e.Property<int>("QuestionId");
// 配置外鍵
e.HasOne(typeof(Question))
.WithMany(nameof(Question.Images))
.HasForeignKey("QuestionId");
});
}
以上就是完整的步驟,當然有人會覺得奇怪因為完全不做配置也是可以用的,這是EF Core已經根據約定自動為我們創建了隱藏屬性:
Shadow properties can be created by convention when a relationship is discovered but no foreign key property is found in the dependent entity class. In this case, a shadow foreign key property will be introduced. The shadow foreign key property will be named
<navigation property name><principal key property name>(the navigation on the dependent entity, which points to the principal entity, is used for the naming).-- From Microsoft Docs
這里EF Core為我們創建的隱藏屬性將命名為<導航屬性名稱><對應主鍵名稱>,即像我們這里有一個導航屬性Question,其Question類的主鍵為Id,那么隱藏屬性就是QuestionId。
復合主鍵
在一些特殊情況下,我們所需的主鍵可能是由多個屬性決定的,比如QuestionTag就是以QuestionId和TagName為主鍵。
這里我們需要通過Fluent API來進行配置,在重載的OnModelCreating方法中添加:
modelBuilder.Entity<QuestionTag>(qt =>
{
qt.HasKey(e => new { e.QuestionId, e.TagName });
});
通過表達式的形式,我們可以很方便的創建新的復合主鍵。
另外,因為在QuestionTag中的真正主鍵是QuestionId和TagName,所以我們還需要覆蓋掉繼承來的Id屬性:
public class QuestionTag : Entity<string>
{
/// <summary>
/// 無效Id,實際Id為QuestionId和TagName
/// </summary>
[NotMapped]
public override string Id => $"{QuestionId}-{TagName}";
/// <summary>
/// 問題Id
/// </summary>
public int QuestionId { get; set; }
/// <summary>
/// 標簽名稱
/// </summary>
public string TagName { get; set; }
// ...
}
默認值
在官方文檔中,使用默認值的方式是在構造函數中賦值,這里我使用的是C# 6.0中的屬性初始化語法(Auto-property initializers)。從我目前的結果來說,與預期效果基本一致,而且更易於閱讀。
形式如下:
public class Question : FullAuditedAggregateRoot<int, User>, IPassivable
{
/// <summary>
/// 問題狀態(默認為true)
/// </summary>
public bool IsActive { get; set; } = true;
// ...
}
構造函數
這是個一直被我忽略的地方,在此之前常常使用的是默認空構造函數,但若需要一個有參構造函數,且這個參數並不直接對應某個屬性,如:
// 此處僅為舉例說明
public class Question
{
public Category Category { get; set; }
// ...
// 這里構造的參數並不直接對應某個屬性
public Question(string categoryName)
{
Category = new Category { Name = categoryName };
}
}
當你添加遷移的時候就會報如下錯誤:No suitable constructor found for entity type 'Question'. The following constructors had parameters that could not be bound to properties of the entity type: cannot bind 'categoryName' in 'Question(string categoryName)'.
大概就是EF Core不能推斷出categoryName是什么。
解決方法很簡單,手動添加一個空構造函數即可。
按照常識,我們添加新的構造函數:
public class Question
{
// ...
// 空的構造函數
public Question() {}
}
可事實上,我們並不希望有人使用這個空的構造函數,因為它會缺少一些空值檢測等判定。
經過查找資料,我在微軟的eShopOnWeb示例項目中找到了如下寫法:
public class Order : BaseEntity, IAggregateRoot
{
// 注意這里是private
private Order()
{
// required by EF
}
// 含參構造函數包括了空值檢測
public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
{
Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
Guard.Against.Null(shipToAddress, nameof(shipToAddress));
Guard.Against.Null(items, nameof(items));
BuyerId = buyerId;
ShipToAddress = shipToAddress;
_orderItems = items;
}
public string BuyerId { get; private set; }
public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
public Address ShipToAddress { get; private set; }
private readonly List<OrderItem> _orderItems = new List<OrderItem>();
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
// ...
}
回過頭,我又去確認了EF Core的文檔:
When EF Core creates instances of these types, such as for the results of a query, it will first call the default parameterless constructor and then set each property to the value from the database
...
The constructor can be public, private, or have any other accessibility.
-- From Microsoft Docs
也就是,EF Core在創建實例時,會首先去調用無參構造函數,且無論該構造函數是何訪問類型。
那么問題就解決了,我們只需添加私有的無參構造函數即可。
PS:但還是沒找到EF Core是如何調用私有構造的過程,希望知道的大佬能指點一下。
