002從零開始入門Entity Framework Core——DbContext生存期、配置和初始化

語言: CN / TW / HK

閱讀須知:本文為入門介紹、指引文章,所示程式碼皆為最簡易(或僅為實現功能)的演示示例版本,不一定切實符合個人(企業)實際開發需求。

一、DbContext生存期

DbContext 的生存期從建立例項時開始,並在釋放例項時結束。 DbContext 例項旨在用於單個工作單元。這意味著 DbContext 例項的生存期通常很短。

使用 Entity Framework Core (EF Core) 時的典型工作單元包括:

  • 建立 DbContext 例項
  • 根據上下文跟蹤實體例項。 實體將在以下情況下被跟蹤
    • 正在從查詢返回
    • 正在新增或附加到上下文
  • 根據需要對所跟蹤的實體進行更改以實現業務規則
  • 呼叫 SaveChanges 或 SaveChangesAsync。 EF Core 檢測所做的更改,並將這些更改寫入資料庫
  • 釋放 DbContext 例項
注意事項:
1、使用後釋放 DbContext 非常重要。這可確保釋放所有非託管資源,並登出任何事件或其他掛鉤,以防止在例項保持引用時出現記憶體洩漏。
2、DbContext 不是執行緒安全的。不要線上程之間共享上下文。請確保在繼續使用上下文例項之前,等待所有非同步呼叫。
3、EF Core 程式碼引發的 InvalidOperationException 可以使上下文進入不可恢復的狀態。此類異常指示程式錯誤,並且不旨在從其中恢復。

二、ASP.NET Core 依賴關係注入中的 DbContext

在許多 Web 應用程式中,每個 HTTP 請求都對應於單個工作單元。這使得上下文生存期與請求的生存期相關,成為 Web 應用程式的一個良好預設值。

使用依賴關係注入配置 ASP.NET Core 應用程式。可以在 Startup.cs 檔案的  ConfigureServices 方法中,用 AddDbContext 擴充套件方法將 EF Core 新增到此處進行配置。

本文中我使用的是 MySQL 資料庫,例如:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    
    #region 配置MySQL資料庫
    var connectionString = "server=資料庫部署的伺服器地址;user=資料庫登入使用者名稱;password=資料庫登入密碼;database=資料庫名;charset=utf8";
    var serverVersion = new MySqlServerVersion(new Version(5, 7, 22));
    services.AddDbContext<CustomAppDbContext>(
        dbContextOptions => dbContextOptions
            .UseMySql(connectionString, serverVersion)
    );
    #endregion
}

此示例將名為 CustomAppDbContext 的 DbContext 子類註冊為 ASP.NET Core 應用程式服務提供程式(也稱為依賴關係注入容器)中的作用域服務。上下文配置為使用 MySQL 資料庫提供程式,並將從 ASP.NET Core 配置讀取連線字串。 在 ConfigureServices 中的何處呼叫 AddDbContext 通常不重要。

F12轉到 DbContext 類的定義,發現其有參建構函式定義形式為:

public DbContext([NotNullAttribute] DbContextOptions options);

CustomAppDbContext 類必須公開具有 DbContextOptions<CustomAppDbContext> 引數的公共建構函式。這是將 AddDbContext 的上下文配置傳遞到 DbContext 的方式。例如:

public class CustomAppDbContext : DbContext
{
    public CustomAppDbContext(DbContextOptions<CustomAppDbContext> options) : base(options)//呼叫父類的建構函式
    {
    }
    
    public DbSet<Student> Student { get; set; }
}

其中 Student 類如下所示:

public partial class Student
{
    public string Id { get; set; }
    public string Name { get; set; }
    public DateTime? JoinTime { get; set; }
    public int Sex { get; set; }
}

然後,CustomAppDbContext 可以通過建構函式注入在 ASP.NET Core 控制器或其他服務中使用。例如:

public class MyController : Controller
{
    private readonly CustomAppDbContext _context;
    
    public MyController(CustomAppDbContext context)//建構函式
    {
        _context = context;
    }
    
    public JsonResult Index()
    {
        _context.Student.Add(new Student
        {
            Id = "10001",
            Name = "張三",
            JoinTime = DateTime.Now,
            Sex = 1
        });
        _context.SaveChanges();
        return new JsonResult("success");
    }
}

最終結果是為每個請求建立一個 CustomAppDbContext 例項,並傳遞給控制器,以在請求結束後釋放前執行工作單元。

三、使用“new”的簡單的 DbContext 初始化

可以按照常規的 .NET 方式構造 DbContext 例項,例如,使用 C# 中的 new 。可以通過重寫 OnConfiguring 方法或通過將選項傳遞給建構函式來執行配置。

1、重寫 OnConfiguring 方法。

F12轉到 DbContext 類的定義,發現 OnConfiguring 方法的定義形式為:

protected internal virtual void OnConfiguring(DbContextOptionsBuilder optionsBuilder);

DbContext 子類的程式碼示例如下所示:

public class NewCustomAppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseMySql("server=資料庫部署的伺服器地址;user=資料庫登入使用者名稱;password=資料庫登入密碼;database=資料庫名;charset=utf8", new MySqlServerVersion(new Version(5, 7, 22)));
    }
     
    public DbSet<Student> Student { get; set; }
}

此種方式構造的 DbContext 例項在控制器方法中呼叫如下所示:

public class MyNewController : Controller
{
    public string Index()
    {
        using  var db = new NewCustomAppDbContext();
        var list = db.Student.ToList();
        return JsonConvert.SerializeObject(list);
    }
}

2、通過 DbContext 建構函式傳遞配置

通過此模式,我們還可以輕鬆地通過 DbContext 建構函式傳遞配置(如連線字串)。例如:

public class NewCustomAppDbContext : DbContext
{
    private readonly string _connectionString;
    
    public NewCustomAppDbContext(string connectionString)
    {
        _connectionString = connectionString;
    }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseMySql(_connectionString, new MySqlServerVersion(new Version(5, 7, 22)));
    }
    
    public DbSet<Student> Student { get; set; }
}

此種方式構造的 DbContext 例項在控制器方法中呼叫如下所示:

public class MyNewController : Controller
{
    public string Index()
    {
        using  var db = new NewCustomAppDbContext("server=資料庫部署的伺服器地址;user=資料庫登入使用者名稱;password=資料庫登入密碼;database=資料庫名;charset=utf8");
        var list = db.Student.ToList();
        return JsonConvert.SerializeObject(list);
    }
}

3、使用 DbContextOptionsBuilder 建立 DbContextOptions 物件

可以使用 DbContextOptionsBuilder 建立 DbContextOptions 物件,然後將該物件傳遞到 DbContext 建構函式。這使得為依賴關係注入配置的 DbContext 也能顯式構造。例如:

public class DICustomAppDbContext:DbContext
{
    public DICustomAppDbContext(DbContextOptions<DICustomAppDbContext> optionsBuilder):base(optionsBuilder)
    {
    }
    
    public DbSet<Student> Student { get; set; }
}

此種構造方式,在 Controller 中可以建立 DbContextOptions,並可以顯式呼叫建構函式,程式碼如下所示:

public class MyDIController : Controller
{
    private readonly string _connectionString = "server=資料庫部署的伺服器地址;user=資料庫登入使用者名稱;password=資料庫登入密碼;database=資料庫名;charset=utf8";
    private readonly MySqlServerVersion _serverVersion = new MySqlServerVersion(new Version(5, 7, 22));

    public string Index()
    {
        var contextOptions = new DbContextOptionsBuilder<DICustomAppDbContext>()
            .UseMySql(_connectionString, _serverVersion)
            .Options;
        using var context = new DICustomAppDbContext(contextOptions);
        var list = context.Student.ToList();
        return JsonConvert.SerializeObject(list);
    }
}

四、使用 DbContext 工廠

某些應用程式型別(例如 ASP.NET Core Blazor)使用依賴關係注入,但不建立與所需的 DbContext 生存期一致的服務作用域。即使存在這樣的對齊方式,應用程式也可能需要在此作用域內執行多個工作單元。例如,單個 HTTP 請求中的多個工作單元。

在這些情況下,可以使用 AddDbContextFactory 來註冊工廠以建立 DbContext 例項。例如:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    
    #region 配置MySQL資料庫
    var connectionString = "server=資料庫部署的伺服器地址;user=資料庫登入使用者名稱;password=資料庫登入密碼;database=資料庫名;charset=utf8";
    var serverVersion = new MySqlServerVersion(new Version(5, 7, 22));
    services.AddDbContextFactory<FactoryCustomAppDbContext>(
        dbContextOptions => dbContextOptions
            .UseMySql(connectionString, serverVersion)
    );
    #endregion
}

FactoryCustomAppDbContext 類必須公開具有 DbContextOptions<FactoryCustomAppDbContext> 引數的公共建構函式。此模式與上面傳統 ASP.NET Core 部分中使用的模式相同。例如:

public class FactoryCustomAppDbContext : DbContext
{
    public FactoryCustomAppDbContext(DbContextOptions<FactoryCustomAppDbContext> options) : base(options)
    {
    }
    
    public DbSet<Student> Student { get; set; }
}

然後,可以通過建構函式注入在其他服務中使用 DbContextFactory 工廠。最後,可以使用注入的工廠在服務程式碼中構造 DbContext 例項。例如:

public class MyFactoryController : Controller
{
    private readonly IDbContextFactory<FactoryCustomAppDbContext> _contextFactory;
    
    public MyFactoryController(IDbContextFactory<FactoryCustomAppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }
    
    public string Index()
    {
        using (var context = _contextFactory.CreateDbContext())
        {
            var list = context.Student.ToList();
            return JsonConvert.SerializeObject(list);
        }
    }
}

請注意,以這種方式建立的 DbContext 例項並非由應用程式的服務提供程式進行管理,因此必須由應用程式釋放。

五、DbContextOptions

所有 DbContext 配置的起始點都是 DbContextOptionsBuilder。 可以通過以下三種方式獲取此生成器:

  1. 在 AddDbContext 和相關方法中
  2. 在 OnConfiguring 中
  3. 使用 new 顯式構造

每種配置方式的示例在本文上述內容中都進行了講解和程式碼展示。無論生成器來自何處,都可以應用相同的配置。此外, 無論如何構造上下文,都將始終呼叫 OnConfiguring 。這意味著即使使用 AddDbContext,OnConfiguring 也可用於執行其他配置。

六、配置資料庫提供程式

每個 DbContext 例項都必須配置為使用 一個且僅一個 資料庫提供程式。(DbContext 子型別的不同例項可用於不同的資料庫提供程式,但一個例項只能使用一個。)一個數據庫提供程式要使用一個特定的  Use* 呼叫進行配置。

例如,若要使用 MySQL 資料庫提供程式:

public class MySQLAppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseMySql("資料庫連線字串", new MySqlServerVersion(new Version(5, 7, 22)));
    }
}

例如,若要使用 SQL Server 資料庫提供程式:

public class SQLServerApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("資料庫連線字串");
    }
}

這些 Use* 方法是由資料庫提供程式實現的擴充套件方法。 這意味著必須先安裝資料庫提供程式 NuGet 包,然後才能使用擴充套件方法。

EF Core 資料庫提供程式廣泛使用擴充套件方法。如果編譯器指示找不到方法,請確保已安裝提供程式的 NuGet 包,並且在程式碼中已有 using Microsoft.EntityFrameworkCore;。

下表包含常見資料庫提供程式的示例。

資料庫系統 配置示例 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

六、避免 DbContext 執行緒處理問題

Entity Framework Core 不支援在同一 DbContext 例項上執行多個並行操作。 這包括非同步查詢的並行執行以及從多個執行緒進行的任何顯式併發使用。因此,始終立即 await 非同步呼叫,或對並行執行的操作使用單獨的 DbContext 例項。

當 EF Core 檢測到嘗試同時使用 DbContext 例項的情況時,你將看到 InvalidOperationException ,其中包含類似於以下內容的訊息:

A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe.

翻譯成中文就是:

在上一個操作完成之前,在此上下文上啟動了第二個操作。這通常是由不同執行緒使用相同的DbContext例項引起的,但不保證例項成員是執行緒安全的。

當併發訪問未被檢測到時,可能會導致未定義的行為、應用程式崩潰和資料損壞。

七、非同步操作缺陷

使用非同步方法,EF Core 可以啟動以非阻擋式訪問資料庫的操作。但是,如果呼叫方不等待其中一個方法完成,而是繼續對 DbContext 執行其他操作,則 DbContext 的狀態可能會(並且很可能會)損壞。

應始終立即等待 EF Core 非同步方法。

-------------------------------本篇文章到此結束-------------------------------------