Search code examples
asp.net-coreentity-framework-coreasp.net-core-webapiin-memory-database

Can't get updated data across multiple DbContext with the same InMemory Db


I want to use an InMemory database to test my web api using a WebApplicationFactory.

In my test I want to set up data in my dbcontext, call my api, test the data. This works fine with getting the data or creating new entries, but on update, the context is not updated, it still contains the old copy of the data. My DbContext is initialized with an InMemoryDatabaseRoot as suggested here.

Here is my WebApplicationFactory

public class InMemDbWebApplicationFactory : WebApplicationFactory<Startup>
{
    public static readonly InMemoryDatabaseRoot InMemoryDatabaseRoot = new InMemoryDatabaseRoot();

    public ServiceProvider ServiceProvider { get; private set; }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkInMemoryDatabase()
                .BuildServiceProvider();

            services.AddDbContext<TestDbContext>(options =>
            {
                options.UseInMemoryDatabase("InMemoryDb", InMemoryDatabaseRoot);
                options.UseInternalServiceProvider(serviceProvider);
            });

            ServiceProvider = services.BuildServiceProvider();
        });
    }
}

My test class use it as a IClassFixture so it is shared across my tests.

Here is my failing test

[Fact]
public async void TestPutBook()
{
    using (var scope = _serviceProvider.CreateScope())
    using (var context = scope.ServiceProvider.GetRequiredService<TestDbContext>())
    {
        var book1 = new Book { Name = "Book1" };
        var book2 = new Book { Name = "Book2" };
        context.Books.AddRange(book1, book2);
        context.SaveChanges();

        var updateResp = await _httpClient.PutAsJsonAsync($"/api/books/{book1.Id}", "Book1Update");
        updateResp.EnsureSuccessStatusCode();

        var getResp = await _httpClient.GetAsync($"/api/books/{book1.Id}");
        getResp.EnsureSuccessStatusCode();
        var stringResponse = await getResp.Content.ReadAsStringAsync();
        book1 = JsonConvert.DeserializeObject<Book>(stringResponse);
        Assert.NotNull(book1);
        //this works
        Assert.Equal("Book1Update", book1.Name);

        book1 = context.Books.Find(book1.Id);
        Assert.NotNull(book1);
        //this fails book1.Name is still equal to "Book1"
        Assert.Equal("Book1Update", book1.Name);
    }
}

I tried also to get a new DbContext within the same scope, but then it fails saying the DbContext is already disposed.

You can find a complete test solution here.


Solution

    1. Avoid disposing DbContext manually because it is managed by Dependency container (DI). In other words, don't use using :

      using (var context = scope.ServiceProvider.GetRequiredService())
      {
           ...
      }  // the context will be DISPOSED now!
         // now if you try to resolve the TestDbContext service in future, it throws
         //     var context = scope.ServiceProvider.GetRequiredService(); // Wrong
      
    2. The test fails also because the DbContext used by controller & the DbContext used in your unit test are in different scopes. Here're two approaches to solve this question:

      Apprach 1 :Reload() manually:

      using (var scope = _serviceProvider.CreateScope())
      {
          var context = scope.ServiceProvider.GetRequiredService<TestDbContext>();
          var book1 = new Book { Name = "Book1" };
          var book2 = new Book { Name = "Book2" };
          context.Books.AddRange(book1, book2);
          context.SaveChanges();
          ...
          ...
          book1 = context.Books.Find(book1.Id);
          context.Entry(book1).Reload(); // refresh to make sure we can get the newest book1 
          Assert.NotNull(book1);
          Assert.Equal("Book1Update", book1.Name);
      }
      

      Approach 2: Create another smaller scope to get the newest record:

      using (var scope = _serviceProvider.CreateScope())
      {
          Book book1;
          var context = scope.ServiceProvider.GetRequiredService<TestDbContext>();
          book1 = new Book { Name = "Book1" };
          context.Books.AddRange(book1);
          context.SaveChanges();
          var updateResp = await _httpClient.PutAsJsonAsync($"/api/books/{book1.Id}", "Book1Update");
          updateResp.EnsureSuccessStatusCode();
          // create a new smaller scope 
          using(var scope2= scope.ServiceProvider.CreateScope()){
              var context2 = scope2.ServiceProvider.GetRequiredService<TestDbContext>();
              book1 = context2.Books.Find(book1.Id);
              Assert.NotNull(book1);
              Assert.Equal("Book1Update", book1.Name);
          }
      }