[EF Core][SQLite]如何使用 EF Core DbContext 以 Microsoft.EntityFrameworkCore.Sqlite 為例

.NET Core 1.0 開始為了跨平台重新改寫了 SQLite,名為 Microsoft.Data.Sqlite,刪除了過時的 DataTable 和 DataAdapter 相關的 DataSet  API,這已經和之前的 System.Data.SQLite 不一樣,但團隊還是盡量讓它們兩者的 API 變化降到最低。這裡有官方的比較說明文件 與System.Data.SQLite的比較-Microsoft.Data.Sqlite | 微軟文檔

Microsoft.Data.Sqlite 使用 SQLitePCLRaw,它綑綁了 SQLite 所需要的底層物件,也會自動初始化環境,讓我們使用起來沒有太大的阻隔,不過我還沒有找到像 

System.Data.SQLite 全域組件,目前就只能安裝在各個專案底下。

本來這篇是要寫 EF Core  SQLite 的使用和限制,後來發現好像都沒有寫到 DbContext 的使用,就順手改了主題方向。

SQLite 限制

SQLite 畢竟沒有執行個體管理的檔案型資料庫,使用上的限制還是要了解一下,請參考官方文件

SQLite 功能

SQLite EF Core 

骨子裡面也就是將 Microsoft.Data.Sqlite 再封裝一層,SQLite EF Core 有甚麼好處

  • 整合 MS DI Container
  • Mirgation 讓 SQLite 的檔案可以動態的長出來,就跟 SQL Server 一樣

記住,不是甚麼所有功能都有實現,它是有限制的,限制如下:SQLite Database Provider - Limitations - EF Core | Microsoft Docs

SQLite 管理工具

Rider Database

只要 Rider 的 Explorer 雙擊 sqlite 檔案,就可以直接連接它,第一次使用要安裝驅動,Rider 整合得很好不需要太費心,參考:Database connection | JetBrains Rider

 

DB Browser for SQLite

 

開發環境

  • Rider 2021.1.1
  • Windows 10
  • Microsoft.EntityFrameworkCore.Sqlite 5.0.5

起手式

新增一個 .NET 5 Lib 專案,名為 Lab.DAL,並在 Rider 的 Terminal 視窗輸入以下命令

dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 5.0.5
dotnet add package Microsoft.Extensions.Logging.Console --version 5.0.0
dotnet add package Microsoft.Extensions.Configuration.Json --version 5.0.0

 

EmployeeDbContext

  • EmployeeDbContext 實作 DbContext
  • DbSet<Employee> 代表的是 Employee 資料表
  • this.Database.Migrate(),自動更新資料表
public class EmployeeDbContext : DbContext
{
   private static readonly bool[] s_migrated = {false};
   public virtual DbSet<Employee> Employees { get; set; }
   public virtual DbSet<Identity> Identities { get; set; }
   public virtual DbSet<OrderHistory> OrderHistories { get; set; }
   public EmployeeDbContext(DbContextOptions<EmployeeDbContext> options)
       : base(options)
   {
       if (s_migrated[0])
       {
           return;
       }
       lock (s_migrated)
       {
           if (s_migrated[0] == false)
           {
               var memoryOptions = options.FindExtension<InMemoryOptionsExtension>();
               if (memoryOptions == null)
               {
                   var sqliteOptionsExtension = options.FindExtension<SqliteOptionsExtension>();
                   if (sqliteOptionsExtension != null)
                   {
                       Console.WriteLine($"EmployeeDbContext 的連線字串為:{sqliteOptionsExtension.ConnectionString},執行 Migration");
                   }
                   this.Database.Migrate();
               }
               s_migrated[0] = true;
           }
       }
   }
}

 

EF Core Code First 請參考 [EF Core 3] 如何使用 Code First 定義資料庫結構 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

完整代碼如下

sample.dotblog/EmployeeDbContext.cs at master · yaochangyu/sample.dotblog (github.com)

sample.dotblog/Employee.cs at master · yaochangyu/sample.dotblog (github.com)

sample.dotblog/Identity.cs at master · yaochangyu/sample.dotblog (github.com)

sample.dotblog/OrderHistory.cs at master · yaochangyu/sample.dotblog (github.com)

組態檔

appsettings.json 內容如下

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=bin\\Lab.DAL.db"
  }
}

 

EmployeeContextFactory

這是用來讓 .NET Core CLI 套用的物件,連線字串來自於組態檔

public class EmployeeContextFactory : IDesignTimeDbContextFactory<EmployeeDbContext>
{
    public EmployeeDbContext CreateDbContext(string[] args)
    {
        Console.WriteLine("EmployeeContextFactory - 由設計工具產生 Database,初始化 DbContextOptionsBuilder");

        var config = DefaultDbContextManager.Configuration;
        var connectionString = config.GetConnectionString("DefaultConnection");
        
        Console.WriteLine($"EmployeeContextFactory - 讀取 appsettings.json 檔案的讀取連線字串為:{connectionString}");
    
        var optionsBuilder = new DbContextOptionsBuilder<EmployeeDbContext>();
        optionsBuilder.UseSqlite(connectionString);
        Console.WriteLine($"EmployeeContextFactory - DbContextOptionsBuilder 設定完成");

        return new EmployeeDbContext(optionsBuilder.Options);
    }
}

 

EF Core Script

接下來用 Rider Terminal 或是 cmd 命令提示字元,先切換到 Lab.DAL 目錄

安裝全域通用工具 

dotnet tool install --global dotnet-ef

 

Migration Add

dotnet ef migrations add InitialCreate

執行結果如下:

 

專案會多 Migrations 目錄,如下圖:

Update Database

產生 SQL 語法並執行

dotnet ef database update

 

或者用 dbContext.Database.Migrate

 

有關 Migration 請參考: [EF Core 3] 如何使用 Code First 的 Migration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

 

DbContext 的設計

DbContext 以需求設計為出發點,一個需求對應一個 DbContext 和所需要的資料表欄位 (DbSet);而不是用一個 DbContext 裝載所有的 DbSet

 

下圖出自黃忠誠老師的 Entity Framework 效能全進化課程

DbContext 的生命週期

DbContext 實例的設計目的是要用於單一 UnitOfWork (工作單元), 這表示 DbContext 實例的存留期通常很短。

UnitOfWork 簡單來講,根據你的需求異動模型狀態,最後,再將最終的狀態,翻譯成命令一次丟給資料庫;而不是每一次模型異動就丟給資料庫

在上面的鏈接中引用 Martin Fowler 的話:「工作單元會跟踪您在業務交易過程中可能會影響數據庫的所有操作。完成後,它將計算出更改數據庫所需要做的一切,是你工作的結果。」

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

重要的

  • 使用後,釋放  DbContext  非常重要,這樣可以確保釋放所有非託管資源,並確保未註冊任何事件或其他掛鉤,以防止在實例仍被引用的情況下發生內存洩漏。
  • DbContext不是線程安全的,不要在線程之間共享 Context(上下文),在繼續使用Context(上下文)實例之前,請確保等待所有異步調用。
  • EF核心代碼引發的 InvalidOperationException  可使Context(上下文)處於不可恢復的狀態,此類異常表示程序錯誤,並非指從錯誤中恢復。

參考:DbContext Lifetime, Configuration, and Initialization - EF Core | Microsoft Docs

 

測試專案

當我需要 Survey / Study 的時候,我習慣使用測試專案來完成,新增一個 .NET 5 測試專案,名為 Lab.DAL.TestProject,並在 Rider 的 Terminal 視窗輸入以下命令

dotnet add package Microsoft.Extensions.Hosting --version 5.0.0
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 5.0.5

新增 appsettings.json,內容如下

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=Lab.DAL.TestProject.db"
  }
}

 

手動管理 SQLite DbContext 執行個體

private static DbContextOptions<EmployeeDbContext> CreateDbContextOptions()
{
    var configBuilder = new ConfigurationBuilder()
                        .SetBasePath(Directory.GetCurrentDirectory())
                        .AddJsonFile("appsettings.json");

    var configRoot       = configBuilder.Build();
    var connectionString = configRoot.GetConnectionString("DefaultConnection");

    var loggerFactory = LoggerFactory.Create(builder =>
                                             {
                                                 builder

                                                     //.AddFilter("Microsoft",                 LogLevel.Warning)
                                                     //.AddFilter("System",                    LogLevel.Warning)
                                                     .AddFilter("Lab.DAL", LogLevel.Debug)
                                                     .AddConsole()
                                                     ;
                                             });
    return new DbContextOptionsBuilder<EmployeeDbContext>()
           .UseSqlite(connectionString)
           .UseLoggerFactory(loggerFactory)
           .Options;
}

 

實例化 EmployeeDbContext 傳入 Options

[TestMethod]
public void 操作真實資料庫_手動實例化EmployeeDbContext()
{
    var       contextOptions = CreateDbContextOptions();
    using var dbContext      = new EmployeeDbContext(contextOptions);
    var       id             = Guid.NewGuid().ToString();
    dbContext.Employees.Add(new Employee()
    {
        Age      = 18,
        Id       = id,
        CreateAt = DateTime.Now,
        CreateBy = "test",
        Name     = "yao"
    });
    dbContext.SaveChanges();

    var actual = dbContext.Employees.AsNoTracking().FirstOrDefault(p => p.Id ==id);
    Assert.AreEqual(18,actual.Age);
    Assert.AreEqual("yao",actual.Name);
}

 

通過 DI Container 管理 DbContext 執行個體

用通用性主機 .NET Generic Host 將 EmployeeDbContext 註冊到 DI Container,有以下方法

接下來的範例我會

  1. 從 DI Container 取出 appsetting.json 組態檔的連線字串,組態請參考 如何使用組態 Microsoft.Extensions.Configuration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
  2. 設定 SQLite,builder.UseSqlite(connectionString)

通用性主機請參考:如何使用 .NET Generic Host for Microsoft.Extensions.Hosting | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

註冊到 DI Container 原始碼:efcore/EntityFrameworkServiceCollectionExtensions.cs at main · dotnet/efcore (github.com)

IServiceCollection.AddDbContext

用 services.AddDbContext<EmployeeDbContext> 設定

  1. DI Container 生命週期預設為 Scope,正好和 ASP.NET / ASP.NET Core 的 Request 的生命週期相同,常見的用法就是 Controller 依賴 DbContext,DbContext 宣告成 field,Request 結束 DbContext 也跟著結束

使用範例如下:

[TestMethod]
public void 操作真實資料庫_由Host註冊EmployeeDbContext()
{
    //arrange
    var builder = Host.CreateDefaultBuilder()
                      .ConfigureServices(services =>
                                         {
                                             services.AddDbContext<EmployeeDbContext>((provider, builder) =>
                                             {
                                                 var config = provider.GetService<IConfiguration>();
                                                 var connectionString = config.GetConnectionString("DefaultConnection");
                                                 builder.UseSqlite(connectionString);
                                             });
                                         });
    var host = builder.Build();

    var       dbContextOptions = host.Services.GetService<DbContextOptions<EmployeeDbContext>>();
    using var dbContext        = host.Services.GetService<EmployeeDbContext>();

    //act
    var id       = Guid.NewGuid().ToString();
    var now = DateTime.Now;
    dbContext.Employees.Add(new Employee()
    {
        Id   = id,
        Name = "余小章",
        Age  = 18,
        CreateAt = now,
        CreateBy = "test user"
    });
    dbContext.Identities.Add(new Identity()
    {
        Employee_Id = id,
        Account  = "yao",
        Password = "123456",
        CreateAt = now,
        CreateBy = "test user"
    });
    var count = dbContext.SaveChanges();

    //assert
    Assert.AreEqual(2, count);

    using var db = new EmployeeDbContext(dbContextOptions);

    var actual = db.Employees
                   .Include(p => p.Identity)
                   .AsNoTracking()
                   .FirstOrDefault();

    Assert.AreEqual(actual.Name,              "余小章");
    Assert.AreEqual(actual.Age,               18);
    Assert.AreEqual(actual.Identity.Account,  "yao");
    Assert.AreEqual(actual.Identity.Password, "123456");
}

 

IServiceCollection.AddDbContextPool

DbContext Pooling 是 EF Core 2 出來的功能,主要是用來提升高吞吐量情境的效能,在一般的情境不會有太大的感覺,

  • 向 CI Container 請求 DbContext 實例時,會先檢查 Pool 有沒有可用的 DbContext 實例,有的話,則返回已存在的實例
  • DbContext 實例使用完畢後,狀態會被清除;若 DbContext 有維護自已的狀態就不應該使用 Pool
    參考 Advanced Performance 主題 | Microsoft Docs
  • Pool 預設為 128
  • 節省 DbContext 實例化的開銷

 

使用片段範例如下

[TestMethod]
public void 操作真實資料庫_由Host註冊EmployeeDbContextPool()
{
    //arrange
    var builder = Host.CreateDefaultBuilder()
                      .ConfigureServices(services =>
                                         {
                                             services.AddDbContextPool<EmployeeDbContext>(
                                              (provider, builder) =>
                                              {
                                                  var config =
                                                      provider.GetService<IConfiguration>();
                                                  var connectionString =
                                                      config.GetConnectionString("DefaultConnection");
                                                  var loggerFactory = provider.GetService<ILoggerFactory>();
                                                  builder.UseSqlite(connectionString)
                                                         .UseLoggerFactory(loggerFactory)
                                                      ;
                                              }, 64);
                                         });
    var host = builder.Build();

    var       dbContextOptions = host.Services.GetService<DbContextOptions<EmployeeDbContext>>();
    using var dbContext        = host.Services.GetService<EmployeeDbContext>();

    ...
}

 

IServiceCollection.AddDbContextFactory

DbContextFactory 是 EF Core 5 出現的 What's New in EF Core 5.0 | Microsoft Docs,預設 DbContext從 DI Container 取出的生命週期為 Scope,和 Http Request 一樣,DbContextFactory 可以讓 DbContext 的生命週期更短

  • IDbContextFactory 在 DI Container 的生命週期是 Singleton
  • 用 DbContextFactory.CreateDbContext() 建立 DbContext 實例,用完即可釋放,可以讓 DbContext 的存活時間更短,我習慣讓他在一個方法內結束

efcore/IDbContextFactory.cs at main · dotnet/efcore (github.com)

 

使用範例如下:

[TestMethod]
public void 操作真實資料庫_由Host註冊EmployeeDbContextFactory()
{
    //arrange
    var builder = Host.CreateDefaultBuilder()
                      .ConfigureServices(services =>
                                         {
                                             services
                                                 .AddDbContextFactory<EmployeeDbContext>((provider, builder) =>
                                                 {
                                                     var config = provider.GetService<IConfiguration>();
                                                     var connectionString =
                                                         config.GetConnectionString("DefaultConnection");
                                                     builder.UseSqlite(connectionString);
                                                 });
                                         });
    var host = builder.Build();

    var       dbContextOptions = host.Services.GetService<DbContextOptions<EmployeeDbContext>>();
    var       dbContextFactory = host.Services.GetService<IDbContextFactory<EmployeeDbContext>>();
    using var dbContext        = dbContextFactory.CreateDbContext();

    //act
    var id  = Guid.NewGuid().ToString();
    var now = DateTime.Now;
    dbContext.Employees.Add(new Employee()
    {
        Id       = id,
        Name     = "余小章",
        Age      = 18,
        CreateAt = now,
        CreateBy = "test user"
    });
    dbContext.Identities.Add(new Identity()
    {
        Employee_Id = id,
        Account     = "yao",
        Password    = "123456",
        CreateAt    = now,
        CreateBy    = "test user"
    });
    var count = dbContext.SaveChanges();

    //assert
    Assert.AreEqual(2, count);

    using var db = new EmployeeDbContext(dbContextOptions);

    var actual = db.Employees
                   .Include(p => p.Identity)
                   .AsNoTracking()
                   .FirstOrDefault();

    Assert.AreEqual(actual.Name,              "余小章");
    Assert.AreEqual(actual.Age,               18);
    Assert.AreEqual(actual.Identity.Account,  "yao");
    Assert.AreEqual(actual.Identity.Password, "123456");
}

 

IServiceCollection.AddPooledDbContextFactory

IServiceCollection.AddDbContextFactory 和  IServiceCollection.AddDbContextPool 的綜合體

使用範例如下

[TestMethod]
public void 操作真實資料庫_由Host註冊EmployeeDbContextPoolFactory()
{
    //arrange
    var builder = Host.CreateDefaultBuilder()
                      .ConfigureServices(services =>
                                         {
                                             services.AddPooledDbContextFactory<EmployeeDbContext>(
                                              (provider, builder) =>
                                              {
                                                  var config =
                                                      provider.GetService<IConfiguration>();
                                                  var connectionString =
                                                      config.GetConnectionString("DefaultConnection");
                                                  var loggerFactory = provider.GetService<ILoggerFactory>();
                                                  builder.UseSqlite(connectionString)
                                                         .UseLoggerFactory(loggerFactory)
                                                      ;
                                              }, 64);
                                         });
    var host = builder.Build();

    var       dbContextOptions = host.Services.GetService<DbContextOptions<EmployeeDbContext>>();
    var       dbContextFactory = host.Services.GetService<IDbContextFactory<EmployeeDbContext>>();
    using var dbContext        = dbContextFactory.CreateDbContext();

    //act
    var id  = Guid.NewGuid().ToString();
    var now = DateTime.Now;
    dbContext.Employees.Add(new Employee()
    {
        Id       = id,
        Name     = "余小章",
        Age      = 18,
        CreateAt = now,
        CreateBy = "test user"
    });
    dbContext.Identities.Add(new Identity()
    {
        Employee_Id = id,
        Account     = "yao",
        Password    = "123456",
        CreateAt    = now,
        CreateBy    = "test user"
    });
    var count = dbContext.SaveChanges();

    //assert
    Assert.AreEqual(2, count);

    using var db = new EmployeeDbContext(dbContextOptions);

    var actual = db.Employees
                   .Include(p => p.Identity)
                   .AsNoTracking()
                   .FirstOrDefault();

    Assert.AreEqual(actual.Name,              "余小章");
    Assert.AreEqual(actual.Age,               18);
    Assert.AreEqual(actual.Identity.Account,  "yao");
    Assert.AreEqual(actual.Identity.Password, "123456");
}

相關文章

通用性主機請參考:

如何使用 .NET Generic Host for Microsoft.Extensions.Hosting | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

使用 Microsoft.Extensions.Hosting.WindowsServices 和 Topshelf.Extensions.Hosting 建立 Windows Service 應用程式 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

DI Container 請參考:

如何使用 DI Container for Microsoft.Extensions.DependencyInjection | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

如何使用 Microsoft.Extensions.DependencyInjection for Autofac | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

組態請參考:

如何使用組態 Microsoft.Extensions.Configuration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

如何使用 Options Pattern for Microsoft.Extensions.Options | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

EF Core 請參考

[EF Core 3] 安裝 EF Core 3 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

[EF Core 3] 如何使用 Code First 定義資料庫結構 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

[EF Core 3] 如何使用 Code First 的 Migration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

EF 請參考

[EF6][SQLite] SQLite Code First 和 Migration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

[EF6][SQLite] SQLite Code First 和 Migration (2) | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

[SQLite] 解決佈署 Web 站台時出現 SQLite 問題 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

範例位置

sample.dotblog/ORM/EFCore/Lab.SQLite at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo