EntityFramework Core dynamic loading model once and for all, what we want to know about it?

Foreword

This article comes from a children's shoes asked me: how to dynamically load the model it in EntityFramework Core in? In learning EntityFramwork on this issue has been correspondence Park Friends of the answers, it is not too much research, although the final settlement of the problem raised by the shoes, but when I re-depth study and found that the problem is far from simple, the the problems arise out of this worth it took me a little time to think, personally feel valuable and necessary, so this may be able to do the next record to help the needy children's shoes, research course EntityFramework Core dynamic loading model thus begin , then follow in my footsteps went to look.

EntityFramework Core dynamic loading model

We still scratch, create EF Core 2.x console application, and then give this section we need to use the model, as usual we have used up the Blog and Post, as follows:

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; }
 }

Next is the context we need to use, as follows:

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; }

}

The final database tables created as follows:

We see the above table name is the plural form of the model, then we inquire blog list, as follows:

var context = new EFCoreDbContext();
var blogs = context.Blogs.Include(d => d.Posts).ToList();

The presentation above is our usual practice, this time it was asked, along with business changes, we have to add DbSet property in the context of multiple models, the ability to avoid repeating this operation, the subsequent addition of our model dynamically loaded into context so as to improve the efficiency of business we focus on it? In the course is wide, here we use the actual scenario described, we usually placed in a model library, such as the above will be shown below on Blog and Post Model class library.

Next we need to do is in the model initialization, set model acquisition program is located, then the assembly of the model generated by ModelBuilder, under normal circumstances, we are calling the following method Entity allocation model, as follows:

modelBuilder.Entity<Blog>(typebuilder => 
{
       ......
});

With the above analysis, we obtain the above-described method through reflection Entity and Entity calling method reflected by ModelBuilder calling obtained as follows:

        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>给去掉,如下:

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

然后我们直接通过上下文中的Set方法来查询数据,如下:

            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表名原理解析,感谢您的阅读,下一节内容相信很快就会到来。

Guess you like

Origin www.cnblogs.com/CreateMyself/p/12175618.html