EF Core与ASP.NET Core 的集成

目录

一、分层项目中EF Core的用法

二、使用“上下文池”时要谨慎

三、案例:批量注册上下文


一、分层项目中EF Core的用法

        在编写简单的演示案例的时候,我们通常会把项目的所有代码放到同一个文件夹中,而对于现实中比较复杂的项目,我们通常是要对其进行分层的,也就是不同的类放到不同的文件夹中。这样的分层项目中使用 EF Core 的时候有一些问题需要考虑。

        第1步,创建一个NET 类库项目,项目名字为 BooksEFCore。通过 NuGet 为项目安装MicrosoftEntityFrameworkCore.Relational包,并且在项目中增加代表图书的实体类 Book和它的实体类的配置类BookConfig,如下代码所示。

public record Book
{
public Guid Id{ get; set; }
public stringName{get; set;}
public double Price { get; set;}
}
class BookConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("T_Books");
}}

        这里,我们把 Book 类声明为一个记录类,而不是普通的类,主要是为了让编译器自动生成ToString 方法,帮我们简化对象的输出。

        第2步,在 BooksEFCore 项目中增加上下文类,如下代码所示

public class MyDbContext : DbContext
{
public DbSet<Book> Books { get;set;}
public MyDbContext(DbContextOptions<MyDbContext> options):base(options)
{}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}

        这里编写的MyDbContext和之前编写的上下文类不同。我们之前是重写OnConfiguring方法,在OnConfiguring方法中调用UseSqlServer 等方法来设置要使用的数据库。在实际项目中直接在OnConfiguring方法中硬编码要连接的数据库是不太合理的,因为我们可能需要在运行时通过读取配置来确定要连接的数据库,如果在上下文中硬编码了要连接的数据库,就会导致上下文复用性太差。因此我们尽量把上下文的数据库配置的代码写到ASPNET Core 项目中因此,在这里,我们没有重写 OnConfiguring 方法,而是为MyDbContext 类增加DbContextOptions<MyDbContex>类型参数的构造方法。DbContextOptions 是一个数据库连接配置对象,我们会在ASPNET Core 项目中提供对 DbContextOptions 的配置。

        第3步,创建一个ASPNET Core 项目,在这个项目中添加对 BooksEFCore 项目的引用。因为要连接SQLServer 数据库,所以我们通过 NuGet 安装 MicrosofEntityFramework Core.SqlServer。在ASP.NET Core 项目的 appsettings.json 中增加对数据连接字符的配置,如下代码所示。

"ConnectionStrings": {
"Default":"Server=.;Database=demo7;Trusted_Connection=True;"
}

        第4步,在 ASP.NET Core 项目的 Program.cs 的 builder Build0之前增加对上下文进行配置的代码如下所示。

builder.Services.AddDbContext<MyDbContext>(opt => {
string connStr = builder.Configuration.GetConnectionString("Default");
opt.UseSqlServer(connStr);
})

        使用AddDbContext方法来通过依赖注入的方式让MyDbContext采用我们指定的连接字符串连接数据库。由于 AddDbContext 方法是泛型的,因此我们可以为同一个项目中的多个不同的上下文设定连接不同的数据库。

        第5步,在ASPNET Core 项目中增加使用 MyDbContext 进行数据库读写的测试代码,如下代码所示。

public class TestController : Controller
{
private readonly MyDbContext dbCtx;
public TestController(MyDbContext dbctx)
{
this.dbCtx= dbCtx;
}
public async Task<IActionResult> Index()
{
dbCtx.Add(new Book { Id=Guid.NewGuid(),Name="C#",Price=59});
await dbCtx.SaveChangesAsync();
var book = dbCtx.Books.First();
return Content(book.Tostring()); ;
}
}

        由于在代码中采用依赖注入的形式配置并且注入了 MyDbContext,因此我们可以用依赖注入的形式来创建上下文,而不用像以前那样在代码中手动创建 MyDbContext 类的实体类。可以看到,依赖注入让代码的职责划分更加清晰。

        我们知道,如果一个被依赖注入容器管理的类实现了 IDisposable 接口,则离开作用域之后容器会自动调用对象的 Dispose方法。上下文是实现了Disposable接口的,因此注入的上文对象会被依赖注入容器正确地回收,开发人员一般不需要手动回收上下文对象。

        第6步,生成实体类的迁移脚本。在多项目的环境下执行 EF Core 的数据库迁有很多特殊的要求,稍不注意,在执行 Add-Migration 的时候,迁移工具就会提示“No DbContext was foundmassembly.” “Unable to create an obiect of type'MyDbContext.” 等错误。

        如果使用数据库迁移工具的时候出现这种错误,我们是可以通过研究数据库迁移工具的要求来调整代码来让它能够正常运行的,但是这个调整过程是非常麻烦的。因此,建议大家不要浪费时间在研究数据库迁移工具的配置上面,而是建议采用IDesignTimeDbContextFactory 接口来解决这个问题。

        当项目中存在一个IDesignTimeDbContextFactory 接口的实现类的时候,数据库迁移工具口来解决这个问题。就会调用这个实现类的CreateDbContext 方法来获取上下文对象,然后迁移工具会使用这个上下文对象来连接数据库。因此,我们需要在 BooksEFCore 项目中创建一个IDesignTimeDbContextFactory 接口的实现类,如下代码所示。

class MyDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
public MyDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<MyDbContext> builder = new ();
string connStr = Environment.GetEnvironmentVariable("ConnectionStrings;BooksEFCore");
builder.UseSqlserver(connStr);
return new MyDbContext(builder.Options);
}}

        我们可以用硬编码的方式把连接配置信息写到代码中,因为这个代码只有在开发环境下才会运行。如果我们不希望数据库的连接字符串被写到项目的代码中,我们也可以把连接字符串配置到环境变量中,不过 MyDesignTimeDbContextFactory 中很难使用IConfiguration 等来读取NET的配置,我们可以直接调用 Environment.GetEnvironmentVariable 来读取环境变量,如第6行代码所示。我们要在环境变量中增加一个名字为 ConnectionStrings:BooksEFCore 的项,其值为数据库的连接字符串。

        因为数据库迁移脚本要生成到 BooksEFCore 项目中,所以我们为这个项目安装Microsoft.EntityFrameworkCore.Tools、Microsof.EntityFrameworkCore.SqlServer 这两个程序集,然后把 BooksEFCore 设置为启动项目,并且在[程序包管理器控制台]中选中 BooksEFCore项目后,执行 Add-Migration Init 生成数据库迁移脚本,然后执行 Update-database 命令即可完成数据库的创建。

        接下来启动 ASP.NET Core 项目,我们就可以看到序能够正常地执行了。我们向Test/Index 发送请求,就可以看到程序运行成功,数据库中也正常地插入了数据。

        综上所述,在分层项目中,我们把实体类、上下文写到独立于 ASP.NET Core 的项目中把数据库连接的配置使用依赖注入的方式写到 ASP.NET Core 项目中,这样就做到了项目职责的清晰划分。

二、使用“上下文池”时要谨慎

        上下文被创建的时候不仅要创建数据库连接,而且要执行实体类的配置等,因此实例化上下文的时候会消耗较多的资源。为了避免性能损失,EF Core 中提供了可以用来代替AddDbContext 的 AddDbContextPol来注入上下文。对于使用 AddDbContextPool注入的上下文,EF Core 会优先从“上下文池”中获取实例,当一个上下文不再被使用后,会被返回上下文池,而不会被销毁。因此,使用上下文池能够在一定程度上提升程序的性能。不过,使用AddDbContextPool时也有一些需要注意的问题。

        首先,使用 AddDbContext 的时候,我们可以为上下文注入服务;但是在使用AddDbContextPool的时候,由于上下文实例会被复用,因此我们无法为上下文注入服务。

        其次,很多数据库的ADO.NET 提供者都实现了“数据库连接池”机制,由于 EF Core 是基于ADO.NET的,因此 EF Core 也自然可以使用数据库连接池。但是上下文池和数据库连接池的共存如果处理不当就会引起问题,对这个感兴趣的读者可以去网上搜索AddDbContextPool 连接池耗尽”。

        在进行项目开发时,推荐开发人员采用“小上下文”策略,也就是不要把项目中所有的实体类都放到同一个上下文类中,而是只把关系紧密的实体类放到同一个上下文类中,把关系不紧密的实体类放到不同的上下文类中。也就是项目中存在多个上下文类,每个上下文类中只有少数几个实体类。如果采用这样的小上下文策略,那么一个上下文实例初始化的时候,实体类的配置等过程将非常快,其不会成为性能瓶颈,而且如果启用了数据库连接池,数据库连接的创建也不会成为性能瓶颈。

        总之,如果项目中需要为上下文注入其他服务,则不能使用 AddDbContextPool; 如果项目中采用小上下文策略,并且启用了数据库连接池的话,一般也不需要使用AddDbContextPool。

三、案例:批量注册上下文

        如果项目采用小上下文策略,在项目中可能就存在着多个上下文类,我们需要手动为这些项目调用AddDbContext方法进行注册,显然这比较麻烦。

        如果这些上下文连接的都是相同的数据库的话,我们可以采用反射的方式扫描程序集中所有的上下文类,然后为它们逐个调用AddDbContext 注册,如下代码所示。

        

public static IServiceCollection AddAllDbContexts(this IServiceCollection services,
Action<DbContextOptionsBuilder> builder,IEnumerable<Assembly> assemblies)
{
Type[] types = new Type[] { typeof(IServiceCollection),
typeof(Action<DbContextOptionsBuilder>),
typeof(ServiceLifetime),typeof(ServiceLifetime) };
var methodAddDbContext = typeof(EntityFrameworkServiceCollectionExtensions).GetMethod("AddDbContext",1,types);
foreach (var asmToLoad in assemblies)
{
foreach (var dbCtxType in asmToLoad.GetTypes().Where(t => !t.IsAbstract && typeof(DbContext).IsAssignableFrom(t)))
{
var methodGenericAddDbContext = methodAddDbContext.MakeGenericMethod(dbctxType);
methodGenericAddDbContext.Invoke(null,new object[] { services,builder,ServiceLifetime.Scoped,ServiceLifetime.Scoped });
}
}
return services;
}

        其中,builder 参数是对上下文的连接字符串等进行配置的回调方法,而assemblies 参数则为所有含有上下文类的程序集。在第 7行代码中通过反射获得 AddDbContext 方法,然后在第10行代码中通过反射获得程序中所有非抽象的上下文类,这里我们使用 GetTypes而非GetExportedTypes方法来获得程序中的类,因为考虑到有的项目中会把上下文的访问修饰符设置为 internal。在第 12~13代码中通过反射调用 AddDbContext 方法,由于AddDbContext方法是泛型的,因此我们要先使用 MakeGenericMethod 方法设定泛型的类型,然后才能调用AddDbContext 方法。

猜你喜欢

转载自blog.csdn.net/xxxcAxx/article/details/128472588