这篇文章源于一位问个人童鞋:在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表名原理解析,感谢您的阅读,下一节内容相信很快就会到来。