关于 c#:EntityFrameworkCore SQLite in-memory db 表未创建

EntityFrameworkCore SQLite in-memory db tables are not created

对于集成测试,我正在使用 EntityFrameworkCore SQLite 内存数据库并根据 Microsoft 文档创建其架构,但是当我尝试播种数据时,会引发异常,即表不存在。

DbContext.Database.EnsureCreated(); 的鼠标悬停文档:

Ensure that the database for the context exists. If it exists, no
action is taken. If it does not exist then the database and all its
schema are created. If the database exists, then no action is made to
ensure it is compatible with the model for this context.

我已经读到 EntityFrameworkCore 内存数据库仅在开放连接存在时才存在,因此我尝试显式创建一个 var connection = new SqliteConnection("DataSource=:memory:"); 实例并将以下代码package在一个 using(connection) {} 块中并传递连接实例 options.UseSqlite(connection);,但 DbContext.Database.EnsureCreated(); 仍然没有创建任何 db-objects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
    protected override IWebHostBuilder CreateWebHostBuilder()
    {
        return WebHost.CreateDefaultBuilder()
            .UseStartup<Startup>();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
      using (var connection = new SqliteConnection("DataSource=MySharedInMemoryDb;mode=memory;cache=shared"))
      {
          connection.Open();
          builder.ConfigureServices(services =>
          {
              var serviceProvider = new ServiceCollection()
                  .AddEntityFrameworkSqlite()
                  .BuildServiceProvider();

              services.AddDbContext<MyDbContext>(options =>
              {
                  options.UseSqlite(connection);
                  options.UseInternalServiceProvider(serviceProvider);
              });

              var contextServiceProvider = services.BuildServiceProvider();

              // we need a scope to obtain a reference to the database contexts
              using (var scope = contextServiceProvider.CreateScope())
              {
                  var scopedProvider = scope.ServiceProvider;

                  var logger = scopedProvider.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();

                  using (var myDb = scopedProvider.GetRequiredService<MyDbContext>())
                  {
                      // DEBUG CODE
                      // this returns script to create db objects as expected
                      // proving that MyDbContext is setup correctly
                      var script = myDb.Database.GenerateCreateScript();
                      // DEBUG CODE

                      // this does not create the db objects ( tables etc )
                      // this is not as expected and contrary to ms docs
                      var result = myDb.Database.EnsureCreated();

                      try
                      {
                          SeedData.PopulateTestData(myDb);
                      }
                      catch (Exception e)
                      {
                          // exception is thrown that tables don't exist
                          logger.LogError(e, $"SeedData.PopulateTestData(myDb) threw exception=[{e.Message}]");
                      }
                  }
              }
          });
        }
        builder.UseContentRoot(".");
        base.ConfigureWebHost(builder);
    }

请注意,在这篇文章中,我只是在问为什么 DbContext.Database.EnsureCreated(); 没有按预期创建架构。我不会将上述代码作为运行集成测试的一般模式。


使用非共享 SQLite 内存数据库

SQLite 内存数据库默认是瞬态的。正如文档所述:

The database ceases to exist as soon as the database connection is closed. Every :memory: database is distinct from every other.

另一方面,

EF Core 的 DbContext 始终自动打开和关闭与数据库的连接,除非您传递已经打开的连接。

因此,为了在 EF Core 中跨多个调用使用相同的 SQLite 内存数据库,您需要单独创建一个 SqliteConnection 对象,然后将其传递给每个 DbContext

例如:

1
2
3
4
5
6
7
  var keepAliveConnection = new SqliteConnection("DataSource=:memory:");
  keepAliveConnection.Open();

  services.AddDbContext<MyContext>(options =>
  {
    options.UseSqlite(keepAliveConnection);
  });

请注意,SqliteConnection 并不是真正的线程安全的,因此这种方法仅适用于单线程场景。任何时候你想拥有一个可以被多个线程访问的共享数据库(例如,在 ASP.NET Core 应用程序中,服务多个请求),你应该考虑使用磁盘数据库。

顺便说一句,这是 EF Core 文档中当前使用的关于如何使用 SQLite 内存数据库进行测试的方法。

使用共享的 SQLite 内存数据库

SQLite 还支持命名的共享内存数据库。通过使用相同的连接字符串,多个 SqliteConnection 对象可以连接到同一个数据库。但是:

The database is automatically deleted and memory is reclaimed when the last connection to the database closes.

因此,仍然需要维护一个单独的打开连接对象,以便数据库可以跨多个 EF Core 调用使用。例如:

1
2
3
4
5
6
7
8
  var connectionString ="DataSource=myshareddb;mode=memory;cache=shared";
  var keepAliveConnection = new SqliteConnection(connectionString);
  keepAliveConnection.Open();

  services.AddDbContext<MyContext>(options =>
  {
    options.UseSqlite(connectionString);
  });

请注意,这种方法不限于单个线程,因为每个 DbContext 都有自己的 SqliteConnection 实例。