前言

这篇文章源于一位问我的童鞋:在EntityFramework Core中如何动态加载模型呢?在学习EntityFramwork时关于这个问题已有对应园友给出答案,故没有过多研究,虽然最后解决了这位童鞋提出的问题,但是当我再次深入研究时,发现原来问题远没有这么简单,由此而引申出来的问题值得我花了一点时间去思考,个人感觉很有价值和必要,所以在此做下记录或许能够帮助到有需要的童鞋,研究EntityFramework Core动态加载模型的历程由此而开始,接下来跟随我的脚步一起去瞧瞧。

EntityFramework Core动态加载模型

我们依然从零开始,创建EF Core 2.x控制台程序,然后给出本节内容我们需要用到的模型,如往常一样我们已经用烂了的Blog和Post,如下:

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

最终数据库表创建如下:

我们看到上述表名是模型的复数形式,接下来我们查询博客列表,如下:

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

以上演示的则是我们一贯的做法,这个时候就有人问了,随着业务变更,我们都得在上下文中添加多个模型的DbSet属性,能否避免此重复操作的情况,将我们后续添加的模型动态加载到上下文中去从而提高工作效率让我们着重关注业务呢? 当然是阔以的,这里我们借助实际场景来说明,我们将模型通常都会放在一个类库中,比如我们将上述Blog和Post放在如下图Model类库中。

接下来我们要做的则是在初始化模型时,获取模型所在的程序集,然后将该程序集中的模型通过ModelBuilder生成,正常情况下我们是调用如下Entity方法配置模型,如下:

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

有了如上分析,我们就通过反射获取上述Entity方法,然后调用通过ModelBuilder调用反射得到的Entity方法,如下:

        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 == -
? dotIndex == -
? type.Name
: type.Name.Substring(dotIndex + , type.Name.Length - dotIndex - )
: type.Name.Substring(plusIndex + , type.Name.Length - plusIndex - );
}

到这里我们总算明白了,模型类型不为空获取模型的名称,经验证其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表名原理解析,感谢您的阅读,下一节内容相信很快就会到来。

EntityFramework Core一劳永逸动态加载模型,我们要知道些什么呢?的更多相关文章

  1. WPF 3D动态加载模型文件

    原文:WPF 3D动态加载模型文件 这篇文章需要读者对WPF 3D有一个基本了解,至少看过官方的MSDN例子. 一般来说关于WPF使用3D的例子,都是下面的流程: 1.美工用3DMAX做好模型,生成一 ...

  2. Asp.Net Core 项目实战之权限管理系统(8) 功能菜单的动态加载

    0 Asp.Net Core 项目实战之权限管理系统(0) 无中生有 1 Asp.Net Core 项目实战之权限管理系统(1) 使用AdminLTE搭建前端 2 Asp.Net Core 项目实战之 ...

  3. 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图

    标题:从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图 作者:Lamond Lu 地址:http://www.cnblogs ...

  4. .Net Core 通过依赖注入和动态加载程序集实现宿程序和接口实现类库完全解构

    网上很多.Net Core依赖注入的例子代码,例如再宿主程序中要这样写: services.AddTransient<Interface1, Class1>(); 其中Interface1 ...

  5. Net Core动态加载webservice/WCF

    1.动态加载的目的 前端时间和顺丰对接了个项目(PS:顺丰的开发对外能力真的是掉粉),用的webservice 测试时用的无固定IP访问,正式版需要固定IP访问,我的理解是web服务都是全网络可以访问 ...

  6. .Net Core利用反射动态加载类库的方法(解决类库不包含Nuget依赖包的问题)

    在.Net Framework时代,生成类库只需将类库项目编译好,然后拷贝到其他项目,即可引用或动态加载,相对来说,比较简单.但到了.Net Core时代,动态加载第三方类库,则稍微麻烦一些. 一.类 ...

  7. Android中的动态加载机制

    在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...

  8. 【转】Android类动态加载技术

    http://www.blogjava.net/zh-weir/archive/2011/10/29/362294.html Android应用开发在一般情况下,常规的开发方式和代码架构就能满足我们的 ...

  9. Android 动态加载 (一) 态加载机制 案例一

    在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...

随机推荐

  1. JAVA之NIO按行读写大文件,完美解决中文乱码问题

    ;//一次读取的字节长度 File fin = new File("D:\\test\\20160622_627975.txt");//读取的文件 File fout = new  ...

  2. win10访问Microsoft数据库问题总结

    今天突然接到任务 把15年的一个wpf项目倒腾出来,根据客户要求微调界面效果 翻扯项目历史记录,找到最后一版的项目,不过历经三载,开发时的环境和现在的环境略有差距 原来:win7 64位   vs20 ...

  3. node_modules

    怎么使外部访问module,我们知道客户端的JavaScript使用script标签引入JavaScript文件,就可以访问其内容了,但这样会带来问题,最大的就是作用域相同,产生冲突问题,以至于前端大 ...

  4. 2002年NOIP普及组复赛题解

    题目涉及算法: 级数求和:入门题: 选数:搜索: 产生数:搜索.高精度: 过河卒:动态规划. 级数求和 题目链接:https://www.luogu.org/problemnew/show/P1035 ...

  5. Python--day48--今日内容

  6. Django入门1--Django是什么?Django里文件的作用?

    Django项目目录介绍: wsgi.py文件介绍: urls.py文件介绍: __init__.py文件介绍:

  7. P1092 电子表格

    题目描述 在流行的电子表格系统中(例如,在Excel中),使用如下计算方式来对列号进行计算. 第1列对应A,第2列对应B,--,第26列对应Z.然后使用两个大写英文字母来表示列:第27列对应AA,第2 ...

  8. linux 原子变量

    有时, 一个共享资源是一个简单的整数值. 假设你的驱动维护一个共享变量 n_op, 它告 知有多少设备操作目前未完成. 正常地, 即便一个简单的操作例如: n_op++; 可能需要加锁. 某些处理器可 ...

  9. Python中&、^与and、or

    导火索:给定两个列表,怎么找出他们相同的元素和不通的元素? list1 = [1, 2, 3, 4, 57, 8, 90] list2 = [2, 3, 4, 5, 6, 7, 8] lis = li ...

  10. indexdb开cai发keng实zhi践lu

    一直在维护一个用html2canvas截图转base64保存的项目,先不说html2canvas不同版本的不同坑的问题,就说转出来的这个base64字符长度实在太大了,尤其是遇到设计出图高度达到3千多 ...