EntityFramwork Core的基本使用

概述

引入nuget包

注意:提前应导入对应版本的EF core包。
Microsoft.EntityFrameworkCore ->对应基本的EFAPI 包括DbContext与DbSet
Microsoft.EntityFrameworkCore.SqlServer ->对应着数据库的连接服务,这个包对应Sqlserver连接服务
Microsoft.EntityFrameworkCore.Tools ->对应控制台命令工具

DbContext

生命周期

DbContext的生命周期从创建实例开始到释放实例时结束.DbContext的工作实例旨在用于单个的工作单元,这意味着DbContext的生命周期通常很短.DbContext不是线程安全的,禁止在多线程中复用单个DbContext.
使用DbContext时的工作单元指的是:

  • 创建 DbContext 实例
  • 根据上下文跟踪实体实例。实体将在以下情况下被跟踪
  • 根据需要对所跟踪的实体进行更改以实现业务规则
  • 调用 SaveChangesSaveChangesAsync。 EF Core 检测所做的更改,并将这些更改写入数据库。
  • 释放 DbContext 实例

注意:

  • 使用后释放 DbContext 非常重要。 这可确保释放所有非托管资源,并注销任何事件或其他挂钩,以防止在实例保持引用时出现内存泄漏。
  • DbContext 不是线程安全的。 不要在线程之间共享上下文。 请确保在继续使用上下文实例之前,等待所有异步调用。
  • EF Core 代码引发的 InvalidOperationException 可以使上下文进入不可恢复的状态。 此类异常指示程序错误,并且不旨在从其中恢复。

    创建并配置DbContext实例

    ASP.NET Core中DbContext的依赖注入
    在许多 Web 应用程序中,每个 HTTP 请求都对应于单个工作单元。 这使得上下文生存期与请求的生存期相关,成为 Web 应用程序的一个良好默认值。
  1. 在core中,配置依赖注入只需要在startup中的[ConfigureServices](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/startup#the-configureservices-method)将自己的Context加入容器就可以了:

    1. public void ConfigureServices(IServiceCollection services)
    2. {
    3. services.AddControllers();
    4. services.AddDbContext<ApplicationDbContext>(
    5. options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
    6. }

    这里添加了自己的ApplicationDbContext并配置了连接字符串,其实连接字符串可以不在这里配置,在之后有三种方法可以配置连接,但本质都是使用了DbContextOptionsBuilder

  2. ApplicationDbContext 类必须公开具有 DbContextOptions 参数的公共构造函数。 这是将 AddDbContext 的上下文配置传递到 DbContext 的方式。 例如:

    public class ApplicationDbContext : DbContext
    {
     public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
         : base(options)
     {
     }
    }
    
  3. 使用注入 : ApplicationDbContext 可以通过构造函数注入在 ASP.NET Core 控制器或其他服务中使用。 例如:

    public class MyController
    {
     private readonly ApplicationDbContext _context;
    
     public MyController(ApplicationDbContext context)
     {
         _context = context;
     }
    }
    

    最终结果是为每个请求创建一个 ApplicationDbContext 实例,并传递给控制器,以在请求结束后释放前执行工作单元。

    使用New初始化DbContext,并重写OnConfiguring()
    public class ApplicationDbContext : DbContext
    {
     private readonly string _connectionString;
    
     public ApplicationDbContext(string connectionString)
     {
         _connectionString = connectionString;
     }
    
     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     {
         optionsBuilder.UseSqlServer(_connectionString);
     }
    }
    

    其实在OnConfiguring中就可以读取配置问卷的ConnectionString来配置不必分类配置.

    New一个DbContextOptionsBuilder实例配置DbContext

    可以使用 DbContextOptionsBuilder 创建 DbContextOptions 对象,然后将该对象传递到DbContext 构造函数。 这使得为依赖关系注入配置的 DbContext 也能显式构造。

    public class ApplicationDbContext : DbContext
    {
     public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
         : base(options)
     {
     }
    }
    

    ```csharp var contextOptions = new DbContextOptionsBuilder() .UseSqlServer(@”Server=(localdb)\mssqllocaldb;Database=Test”) .Options;

using var context = new ApplicationDbContext(contextOptions);

<a name="0Mfzo"></a>
##### 创建DbContext工厂
某些应用程序类型(例如 [ASP.NET Core Blazor](https://docs.microsoft.com/zh-cn/aspnet/core/blazor/))使用依赖关系注入,但不创建与所需的 DbContext 生存期一致的服务作用域。 即使存在这样的对齐方式,应用程序也可能需要在此作用域内执行多个工作单元。 例如,单个 HTTP 请求中的多个工作单元。<br />(以下创建DbContext的方法只适用于使用依赖注入的方式创建对象,当控制台应用程序不使用依赖注入的时候,ApplicationDbContext的构造函数不应该有参数。)

1. 在这些情况下,可以使用 [AddDbContextFactory](https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkservicecollectionextensions.adddbcontextfactory) 来注册工厂以创建 DbContext 实例。
```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContextFactory<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));
}
  1. ApplicationDbContext 类必须公开具有 DbContextOptions 参数的公共构造函数。 此模式与上面传统 ASP.NET Core 部分中使用的模式相同。

    public class ApplicationDbContext : DbContext
    {
     public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
         : base(options)
     {
     }
    }
    
  2. 可以通过构造函数注入在其他服务中使用 DbContextFactory 工厂。 ```csharp private readonly IDbContextFactory _contextFactory;

public MyController(IDbContextFactory contextFactory) { _contextFactory = contextFactory; }


4. 使用
```csharp
public void DoSomething()
{
    using (var context = _contextFactory.CreateDbContext())
    {
        // ...
    }
}

注意:以这种方式创建的 DbContext 实例并非由应用程序的服务提供程序进行管理,因此必须由应用程序释放。

DbContextOptions

所有 DbContext 配置的起始点都是 DbContextOptionsBuilder。 可以通过三种方式获取此生成器:

  • 在 AddDbContext 和相关方法中
  • 在 OnConfiguring 中
  • 使用 new 显式构造

无论生成器来自何处,都可以应用相同的配置。 此外,无论如何构造上下文,都将始终调用OnConfiguring。 这意味着即使使用 AddDbContext,OnConfiguring 也可用于执行其他配置。
这些 Use*” 方法是由数据库提供程序实现的扩展方法。 这意味着必须先安装数据库提供程序 NuGet 包,然后才能使用扩展方法。

数据库系统 配置示例 NuGet 程序包
SQL Server 或 Azure SQL .UseSqlServer(connectionString) Microsoft.EntityFrameworkCore.SqlServer
Azure Cosmos DB .UseCosmos(connectionString, databaseName) Microsoft.EntityFrameworkCore.Cosmos
SQLite .UseSqlite(connectionString) Microsoft.EntityFrameworkCore.Sqlite
EF Core 内存中数据库 .UseInMemoryDatabase(databaseName) Microsoft.EntityFrameworkCore.InMemory
PostgreSQL* .UseNpgsql(connectionString) Npgsql.EntityFrameworkCore.PostgreSQL
MySQL/MariaDB* .UseMySql((connectionString) Pomelo.EntityFrameworkCore.MySql
Oracle* .UseOracle(connectionString) Oracle.EntityFrameworkCore

特定于数据库提供程序的可选配置是在其他特定于提供程序的生成器中执行的。 例如,在连接到 Azure SQL 时,使用 EnableRetryOnFailure 为连接复原配置重试:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=Test",
                providerOptions => { providerOptions.EnableRetryOnFailure(); });
    }
}

其他配置:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .EnableSensitiveDataLogging()
            .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

下表包含 DbContextOptionsBuilder 调用的常见方法的示例。

DbContextOptionsBuilder 方法 作用 了解更多
UseQueryTrackingBehavior 设置查询的默认跟踪行为 查询跟踪行为
LogTo 获取 EF Core 日志的一种简单方法(EF Core 5.0 及更高版本) 日志记录、事件和诊断
UseLoggerFactory 注册 Microsoft.Extensions.Logging 工厂 日志记录、事件和诊断
EnableSensitiveDataLogging 在异常和日志记录中包括应用程序数据 日志记录、事件和诊断
EnableDetailedErrors 更详细的查询错误(以性能为代价) 日志记录、事件和诊断
ConfigureWarnings 忽略或引发警告和其他事件 日志记录、事件和诊断
AddInterceptors 注册 EF Core 侦听器 日志记录、事件和诊断
UseLazyLoadingProxies 使用动态代理进行延迟加载 延迟加载

注意:UseLazyLoadingProxiesUseChangeTrackingProxiesMicrosoft.EntityFrameworkCore.Proxies NuGet 包中的扩展方法。 建议的方式是使用这种类型的“.UseSomething()”调用来配置和/或使用其他包中包含的 EF Core 扩展。

对比DbContextOptions 与 DbContextOptions

大多数接受 DbContextOptions 的 DbContext 子类应使用 泛型 DbContextOptions 变体。

public sealed class SealedApplicationDbContext : DbContext
{
    public SealedApplicationDbContext(DbContextOptions<SealedApplicationDbContext> contextOptions)
        : base(contextOptions)
    {
    }
}

这可确保从依赖关系注入中解析特定 DbContext 子类型的正确选项,即使注册了多个 DbContext 子类型也是如此。但是,如果 DbContext 子类型本身旨在继承,则它应公开采用非泛型 DbContextOptions 的受保护构造函数。 例如:

public abstract class ApplicationDbContextBase : DbContext
{
    protected ApplicationDbContextBase(DbContextOptions contextOptions)
        : base(contextOptions)
    {
    }
}

注意

避免DbContext的线程处理问题

Entity Framework Core 不支持在同一DbContext实例上运行多个并行操作。 这包括异步查询的并行执行以及从多个线程进行的任何显式并发使用。 因此,始终立即 await 异步调用,或对并行执行的操作使用单独的 DbContext 实例。
当 EF Core 检测到尝试同时使用 DbContext 实例的情况时,你将看到 InvalidOperationException,其中包含类似于以下内容的消息:

在上一个操作完成之前,第二个操作已在此上下文中启动。 这通常是由使用同一个 DbContext 实例的不同线程引起的,但不保证实例成员是线程安全的。

检测不到并发访问时,可能会导致未定义的行为、应用程序崩溃和数据损坏。
一些常见错误可能会无意中导致并发访问同一 DbContext 实例:

异步操作的缺陷

使用异步方法,EF Core 可以启动以非阻挡式访问数据库的操作。 但是,如果调用方不等待其中一个方法完成,而是继续对 DbContext 执行其他操作,则 DbContext 的状态可能会(并且很可能会)损坏。
结论:始终立即等待 EF Core 异步方法。

通过依赖注入隐式共享DbContext实例

默认情况下 AddDbContext 扩展方法使用范围内生存期来注册 DbContext 类型。这样可以避免在大多数 ASP.NET Core 应用程序中出现并发访问问题,因为在给定时间内只有一个线程在执行每个客户端请求,并且每个请求都有单独的依赖关系注入范围(因此有单独的 DbContext 实例)。
任何并行显式执行多个线程的代码都应确保 DbContext 实例不会同时访问。
使用依赖关系注入可以通过以下方式实现:将上下文注册为范围内,并为每个线程创建范围(使用 IServiceScopeFactory),或将 DbContext 注册为暂时性(使用采用 ServiceLifetime 参数的 AddDbContext 的重载)。
对于 Blazor Server 托管模型,一个逻辑请求用来维护 Blazor 用户线路,因此,如果使用默认注入范围,则每个用户线路只能提供一个范围内的 DbContext 实例。

创建并配置模型

Entity Framework Core 使用一组约定来根据实体类的形状生成模型。 可指定其他配置以补充和/或替代约定的内容。

在ef core中配置模型有三种,其优先级为:约定 < 数据注释 < fluent API,其中约定是框架的默认配置。而剩下的两种配置形式都是需要学习的,也是用来补充或替代约定的配置形式。

使用fluent API配置模型

可在派生上下文中替代 OnModelCreating 方法,并使用 ModelBuilder API 来配置模型。 此配置方法最为有效,并可在不修改实体类的情况下指定配置。 Fluent API 配置具有最高优先级,并将替代约定和数据注释。

using Microsoft.EntityFrameworkCore;

namespace EFModeling.FluentAPI.Required
{
    internal class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }

        #region Required
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>()
                .Property(b => b.Url)
                .IsRequired();
        }
        #endregion
    }

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    }
}

当实体类过多的时候,OnModelCreating方法会过大,所以可以使用分组配置的方法(将实体类型的所有配置提取到实现 IEntityTypeConfiguration 的单独类中。),一个实体类一个配置文件。在使用的时候在OnModelCreating方法中调用配置的Configure方法。

public class BlogEntityTypeConfiguration : IEntityTypeConfiguration<Blog>
{
    public void Configure(EntityTypeBuilder<Blog> builder)
    {
        builder
            .Property(b => b.Url)
            .IsRequired();
    }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
      new BlogEntityTypeConfiguration().Configure(modelBuilder.Entity<Blog>());
}

使用数据注释配置模型

也可将特性(称为数据注释)应用于类和属性。 数据注释会替代约定,但会被 Fluent API 配置替代。

using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;

namespace EFModeling.DataAnnotations.Required
{
    internal class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
    }

    #region Required
    public class Blog
    {
        public int BlogId { get; set; }

        [Required]
        public string Url { get; set; }
    }
    #endregion
}

DbContext引入实体

在上下文中包含一种类型的 DbSet,这意味着它包含在 EF Core 的模型中;我们通常将此类类型称为 实体。 EF Core 可以从/向数据库中读取和写入实体实例,如果使用的是关系数据库,EF Core 可以通过迁移为实体创建表。

在上下文中包含实体

按照约定,在上下文中的 DbSet 属性中公开的类型作为实体包含在模型中。还包括方法中指定的实体类型OnModelCreating ,就像通过递归方式浏览其他发现的实体类型的导航属性找到的任何类型一样。
在下面的代码示例中,包含了所有类型:

  • Blog 包含,因为它在上下文的 DbSet 属性中公开。
  • Post 包含,因为它通过 Blog.Posts 导航属性发现。
  • AuditEntry 因为它是在中指定的 OnModelCreating 。 ```csharp internal class MyContext : DbContext { public DbSet Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder) {

      modelBuilder.Entity<AuditEntry>();
    

    } }

public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List Posts { get; set; } }

public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }

public class AuditEntry { public int AuditEntryId { get; set; } public string Username { get; set; } public string Action { get; set; } }

根据这一特性,当实体过多的时候,可以使用反射来批量包含实体,有程序集名称和命名空间名称就可以确定该程序集下所有的模型类,包含到上下文:
```csharp
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //反射获取程序集
            AddAssembly(modelBuilder, "TestClass", "TestClass.Entity");

            //将程序集中所有的实体全部映射到数据库。
            //modelBuilder.Entity<Teacher>().ToTable("teachers").Property(w=>w.Name).HasMaxLength(40);
        }

        public void AddAssembly(ModelBuilder modelBuilder, string assemblyName,string nameSpace)
        {
            if (!String.IsNullOrEmpty(assemblyName))
            {
                Assembly assembly = Assembly.Load(assemblyName);
                List<Type> ts = assembly.GetTypes().Where(u => u.IsClass && !u.IsAbstract && !u.IsGenericType &&u.Namespace == nameSpace).ToList();
                foreach (var item in ts)
                {
                     modelBuilder.Entity(item).ToTable(item.Name+"s");
                }
            }
        }

从模型中排除实体

如果不希望将模型包含到上下文中,可以设置:

//数据注释
[NotMapped]
public class BlogMetadata
{
    public DateTime LoadedFromDatabase { get; set; }
}
//fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Ignore<BlogMetadata>();
}

从迁移中排除实体

有时在多个类型中映射相同的实体类型会很有用 DbContext 。 当使用 绑定上下文时,尤其是对于 DbContext 每个边界上下文都有不同类型的情况。(EF Core 5.0 中引入了从迁移中排除表的功能。)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<IdentityUser>()
        .ToTable("AspNetUsers", t => t.ExcludeFromMigrations());
}

此配置迁移不会创建 AspNetUsers 该表,但 IdentityUser 仍包含在模型中,并且可正常使用。如果需要再次使用迁移来管理表,则应创建不包括的新迁移 AspNetUsers 。 下一次迁移将包含对表所做的任何更改。

迁移Migration

EF Core 提供两种主要方法来保持 EF Core 模型和数据库架构同步。至于我们应该选用哪个方法,请确定你是希望以 EF Core 模型为准还是以数据库为准。
如果希望以 EF Core 模型为准,请使用迁移。 对 EF Core 模型进行更改时,此方法会以增量方式将相应架构更改应用到数据库,以使数据库保持与 EF Core 模型兼容。
如果希望以数据库架构为准,请使用反向工程。 使用此方法,可通过将数据库架构反向工程到 EF Core 模型来生成相应的 DbContext 和实体类型。

Code First

使用需要使用单独nuget包:
image.png

    Add-Migration  //添加Migration
    Drop-Database  //删除数据库
    Get-DbContext
    Remove-Migration //删除最近的一次Migration
    Scaffold-DbContext
    Script-DbContext
    Script-Migration  //生产sql语句
    /*
    生成sql代码,常用于部署。可使用参数:
        -From:从哪一次Migration开始,默认为第一次
        -To:到哪一次结束,默认为最近一次
        -Output:输出路径,默认在 程序运行 位置,如: /obj/Debug/netcoreapp2.1/ghbkztfz.sql
                生成的SQL只包含_EFMigrationHistory数据。只包含表结构的更改,不会包含数据。
    */
    Update-Database  //更新Migration

使用(C#)API

public DBUserRepository()
{
    //类似于Update-Database: apply all pending migrations
    //本身不生成Migrations
    Database.Migrate();

    //Enusure:存在才删除,不存在才创建
    Database.EnsureDeleted();
    //Create数据库的同时建立表结构
    Database.EnsureCreated();
}

EnsureCreated()建立的数据库没有__EFMigrationsHistory表,所以不能再使用Migration。

建表过程

EFcore不会在程序第一次运行的时候自动建表(EF在程序第一次运行的时候会检查是否有对应的表,如果没有,则会建立一个与实体对应的表),EFcore应在实体设置完成之后,通过Migration在控制台中手动迁移,去数据库建表。如果不这么做,则会报错。

1、调用Add-Migration进行添加迁移
2、调用Update-Database进行更新(推送)已添加的迁移,如果不指定,则默认推送最新的一次添加的迁移。
快捷Update-Database 0  >表示回到数据库最开始的状态(无表)
Update-Database init  >表示回到初始的状态(有表,但无数据)

反向工程

官方不建议这么使用,当然如果要使用数据库先行的话,就可以使用反向工程:
https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/scaffolding?tabs=dotnet-core-cli