這篇文章源於一位問個人童鞋:在EntityFramework Core中如何動態加載模型呢?在學習EntityFramwork時關於這個問題已有對應園友給出答案,故沒有過多研究,雖然最後解決了這位童鞋提出的問題,可是當我再次深刻研究時,發現原來問題遠沒有這麼簡單,由此而引伸出來的問題值得我花了一點時間去思考,我的感受頗有價值和必要,因此在此作下記錄或許可以幫助到有須要的童鞋,研究EntityFramework Core動態加載模型的歷程由此而開始,接下來跟隨個人腳步一塊兒去瞧瞧。git
咱們依然從零開始,建立EF Core 2.x控制檯程序,而後給出本節內容咱們須要用到的模型,如往常同樣咱們已經用爛了的Blog和Post,以下:github
public class Blog { public int Id { get; set; } public string Name { get; set; } public List<Post> Posts { get; set; } }
/// <summary> /// 博客文章 /// </summary> public class Post { public int Id { get; set; } public int BlogId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }
接下來是咱們須要用到的上下文,以下:數據庫
public class EFCoreDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer(@"Server=.;Database=EFTest;Trusted_Connection=True;"); public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } }
最終數據庫表建立以下:架構
咱們看到上述表名是模型的複數形式,接下來咱們查詢博客列表,以下:app
var context = new EFCoreDbContext(); var blogs = context.Blogs.Include(d => d.Posts).ToList();
以上演示的則是咱們一向的作法,這個時候就有人問了,隨着業務變動,咱們都得在上下文中添加多個模型的DbSet屬性,可否避免此重複操做的狀況,將咱們後續添加的模型動態加載到上下文中去從而提升工做效率讓咱們着重關注業務呢? 固然是闊以的,這裏咱們藉助實際場景來講明,咱們將模型一般都會放在一個類庫中,好比咱們將上述Blog和Post放在以下圖Model類庫中。ide
接下來咱們要作的則是在初始化模型時,獲取模型所在的程序集,而後將該程序集中的模型經過ModelBuilder生成,正常狀況下咱們是調用以下Entity方法配置模型,以下:學習
modelBuilder.Entity<Blog>(typebuilder =>
{
......
});
有了如上分析,咱們就經過反射獲取上述Entity方法,而後調用經過ModelBuilder調用反射獲得的Entity方法,以下:ui
protected override void OnModelCreating(ModelBuilder modelBuilder) { var assembly = Assembly.Load("Model"); var entityMethod = typeof(ModelBuilder).GetMethod("Entity", new Type[] { }); var entityTypes = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && !t.IsNested); foreach (var type in entityTypes) { entityMethod.MakeGenericMethod(type).Invoke(modelBuilder, new object[] { }); } base.OnModelCreating(modelBuilder); }
固然上述加載模型程序集的方式根據咱們實際項目狀況而定,同時在咱們過濾程序集中類型時也一樣如此,好比如果DDD架構,對於倉儲都會封裝一層進行基本操做的倉儲,此時其餘模型倉儲必派生於基倉儲,經過基本倉儲模型進行過濾等等。接下來咱們將上下文中添加的DbSet<Blog>和DbSet<Post>給去掉,以下:this
public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; }
而後咱們直接經過上下文中的Set方法來查詢數據,以下:spa
var context = new EFCoreDbContext(); context.Database.EnsureCreated(); var blogs = context.Set<Blog>().Include(d => d.Posts).ToList();
上述咱們多添加了一行確保數據庫模型已提早被建立,這是必要的,其背後本質就是經過命令進行遷移,要否則在加載模型時應該會報錯,固然若在Web應用程序中,咱們在Configure方法中也一樣添加以下一行:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, EFCoreDbContext context) { context.Database.EnsureCreated(); ...... }
此時將會拋出上述異常,這是爲什麼呢?這是由於數據庫表名是和如上上下文中咱們已經註釋掉的DbSet包含的模型屬性名稱一致,若咱們將上述DbSet包含的模型屬性的註釋給去掉,當加載DbSet屬性時將獲取該屬性名稱和咱們配置的Schema做爲架構名稱(不配置,默認爲空),咱們經過以下源碼可得知(固然咱們經過SQL Server Profiler生成的SQL語句也可得知)
上述咱們只是獲得最終表的架構和名稱而已,那麼默認表名稱是怎樣的呢?當咱們查詢時,會從上述DatasetTable類中去獲取表名,以下:
public class DatabaseTable : Annotatable { /// <summary> /// The database that contains the table. /// </summary> public virtual DatabaseModel Database { get; [param: NotNull] set; } /// <summary> /// The table name. /// </summary> public virtual string Name { get; [param: NotNull] set; } ...... }
它具體是何時調用的呢,咱們看以下代碼:
protected virtual EntityTypeBuilder VisitTable([NotNull] ModelBuilder modelBuilder, [NotNull] DatabaseTable table) { var entityTypeName = GetEntityTypeName(table); var builder = modelBuilder.Entity(entityTypeName); var dbSetName = GetDbSetName(table); builder.Metadata.SetDbSetName(dbSetName); if (table is DatabaseView) { builder.ToView(table.Name, table.Schema); } else { builder.ToTable(table.Name, table.Schema); } if (table.Comment != null) { builder.HasComment(table.Comment); } ...... return builder; }
到了這裏咱們並未看到任何有效的信息,只是將該類中獲得的表名和架構設置到ToTable方法中,讓咱們從頭開始梳理思路,由於從一開始咱們並未經過註解或者Fluent APi去顯式配置表名,因此此時必將走EntityFramework Core的默認約定,思路已經很清晰,最終咱們找到獲取表名的方法,以下:
private static void TryUniquifyTableNames( IConventionModel model, Dictionary<(string, string), List<IConventionEntityType>> tables, int maxLength) { foreach (var entityType in model.GetEntityTypes()) { var tableName = (Schema: entityType.GetSchema(), TableName: entityType.GetTableName()); if (!tables.TryGetValue(tableName, out var entityTypes)) { entityTypes = new List<IConventionEntityType>(); tables[tableName] = entityTypes; } ...... } }
到這裏咱們看到了獲取表名的方法,咱們繼續往下走,看看具體是如何獲取表名的呢?
public static string GetTableName([NotNull] this IEntityType entityType) => entityType.BaseType != null ? entityType.GetRootType().GetTableName() : (string)entityType[RelationalAnnotationNames.TableName] ?? GetDefaultTableName(entityType);
由於對應類型並未有其基類,接下來去獲取註解的表名,此時咱們也並未經過註解設置表名,到這裏咱們也能明白如果咱們經過註解在對應模型上添加與數據庫表名一致的複數便可解決問題。咱們繼續往下走,最後調用獲取默認表名的方法:
public static string GetDefaultTableName([NotNull] this IEntityType entityType) { var ownership = entityType.FindOwnership(); if (ownership != null && ownership.IsUnique) { return ownership.PrincipalEntityType.GetTableName(); } return Uniquifier.Truncate( entityType.HasDefiningNavigation() ? $"{entityType.DefiningEntityType.GetTableName()}_{entityType.DefiningNavigationName}" : entityType.ShortName(), entityType.Model.GetMaxIdentifierLength()); }
首先咱們並未設置模型的OwnType,接下來調用方法根據註釋意爲:獲取模型是否有定義的導航類型,看到這裏時,我認爲Post不就是Blog的導航嗎,此方法被暴露出來可供咱們調用,當我去驗證時發現結果卻返回false,不由讓我心生疑竇
/// <summary> /// Gets a value indicating whether this entity type has a defining navigation. /// </summary> /// <returns> True if this entity type has a defining navigation. </returns> [DebuggerStepThrough] public static bool HasDefiningNavigation([NotNull] this IEntityType entityType) => entityType.DefiningEntityType != null;
經過其方法解釋實在不解導航具體指的啥玩意,因而乎我在github上提了對該方法的疑惑《https://github.com/dotnet/efcore/issues/19559》,根據解答,即便配置了owned Type依然返回false(這個問題後續再詳細分析下源碼),接下來繼續往下看ShortName方法,以下:
/// <summary> /// Gets a short name for the given <see cref="ITypeBase" /> that can be used in other identifiers. /// </summary> /// <param name="type"> The entity type. </param> /// <returns> The short name. </returns> [DebuggerStepThrough] public static string ShortName([NotNull] this ITypeBase type) { if (type.ClrType != null) { return type.ClrType.ShortDisplayName(); } var plusIndex = type.Name.LastIndexOf("+", StringComparison.Ordinal); var dotIndex = type.Name.LastIndexOf(".", StringComparison.Ordinal); return plusIndex == -1 ? dotIndex == -1 ? type.Name : type.Name.Substring(dotIndex + 1, type.Name.Length - dotIndex - 1) : type.Name.Substring(plusIndex + 1, type.Name.Length - plusIndex - 1); }
到這裏咱們總算明白了,模型類型不爲空獲取模型的名稱,經驗證其ShortDisplayName方法返回值就是模型名稱即Blog,因此才拋出最開始異常對象名無效,咱們也可經過以下代碼驗證表名是否是Blog
var mapping = context.Model.FindEntityType(typeof(Blog)).Relational(); var schema = mapping.Schema; var tableName = mapping.TableName;
注意:若您是EntityFramework Core 3.x版本上述獲取架構和表名等方式已經修改爲直接針對模型的擴展方法。以下:
var mapping = context.Model.FindEntityType(typeof(Blog)); var schema = mapping.GetSchema(); var tableName = mapping.GetTableName();
因此對於EF Core而言,默認的表名就是模型名稱,若咱們以DbSet屬性暴露模型則以DbSet屬性名稱做爲表名,一樣咱們也驗證下,咱們將最開始註釋掉的DbSet<Blog> Blogs,修改爲以下:
public DbSet<Blog> BlogAlias { get; set; }
因此若採用動態加載模型,若是數據庫表名就是模型名稱,那麼沒毛病,不然咱們應該根據項目約定而須要進行相應的修改才行,如最開始給出的數據庫表名爲複數爲例,此時咱們還需修改數據庫表名的約定,在OnModelCreating方法添加以下代碼:
foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var tableName = entityType.Relational().TableName; modelBuilder.Entity(entityType.Name).ToTable($"{tableName}s"); }
同理針對EntityFramework Core 3.x版本修改爲如上注意說明,接下來咱們再次註釋掉上述驗證時暴露出的DbSet,最後查詢結果以下:
事情還未結束,配置動態加載模型後,由上只是證實關係映射等沒問題,接下來咱們以下配置owned Type,咱們將看到會拋出異常,很顯然,雖然咱們只是加載了模型,可是對於映射關係經過約定能夠獲得,而owned Type必須顯式配置,因此在遍歷生成模型時,咱們恐怕還須要額外處理owned Type,遺留的這個問題等待空閒時再弄下,暫時就到這裏吧。
public class Blog { public int Id { get; set; } public string Name { get; set; } public List<Post> Posts { get; set; } public Tag Tag { get; set; } } public class Tag { public string Name { get; set; } public Blog Blog { get; set; } } modelBuilder.Entity<Blog>().OwnsOne(t => t.Tag).WithOwner(b => b.Blog);
本節咱們詳細講解了在EntityFramework Core如何動態加載模型,同時針對動態加載模型所帶來的問題也只是進行了一丟丟的論述,來,咱們下一個結論:在EntityFramework Core中根據約定表名爲DbSet屬性名稱,若在上下文中未暴露DbSet屬性,則表名爲模型名稱,若是採用動態加載模型,那麼表名必須與模型名稱一致,不然將拋出異常,固然咱們也能夠根據實際項目約定更改表名。經過本節動態加載模型將引入下一節內容:EntityFramework Core表名原理解析,感謝您的閱讀,下一節內容相信很快就會到來。