Search code examples
c#asp.net-coreentity-framework-coreintegration-testingxunit

Dispose() succeeds to delete db file but DisposeAsync() fails, why?


I want each integration test to create a unique database filename and delete it at the end of the test.

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
var constr = $"DataSource={Guid.NewGuid()}.db";
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlite(constr));
var app = builder.Build();
app.Run();

public record Person(int Id);
public class AppDbContext(DbContextOptions<AppDbContext> o) : DbContext(o)
{
    public DbSet<Person> People { get; set; }
}
public interface IWebApiMarker;

In order to ease switching between DisposeAsync() and Dispose(), I created a compilation variable SYNC. It is only for diagnosing purpose, not for production.

#define SYNC
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;

namespace WebApi.Test;

public class Test_1 : BaseTest
{
    [Fact] public async Task Test() => await Task.Delay(2000);
}

#if SYNC
public abstract class BaseTest : IDisposable
#else
public abstract class BaseTest : IAsyncDisposable
#endif
{
    private readonly WebApplicationFactory<IWebApiMarker> _factory;
    protected BaseTest()
    {
        _factory = new WebApplicationFactory<IWebApiMarker>();
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        db.Database.EnsureCreated();
    }
#if SYNC
    public void Dispose()
    {
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        db.Database.EnsureDeleted();
        _factory.Dispose();
    }
#else
    public async ValueTask DisposeAsync()
    {
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        db.Database.EnsureDeleted();
        await _factory.DisposeAsync();
    }
#endif
}

I don't understand why DisposeAsync() fails to delete the database file at the end of each test.

Failed attempts

  • Caching db resolved in BaseTest() as private readonly AppDbContext db and reusing it (to make sure it is the same object) in DisposeAsync() does not help.
  • Replacing db.Database.EnsureDeleted() with await db.Database.EnsureDeletedAsync() does not help.
  • Calling DisposeAsync() in a test method, [Fact] public async Task Test() => await DisposeAsync(); does not help. It throws exception:

System.ObjectDisposedException : Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.


Solution

  • Apparently xunit does not support IAsyncDisposable in version 2 (release version). So I have to use IAsyncLifetime instead. IAsyncDisposable is suppoerted in version 3 (beta version).

    // solution for xunit version 2
    using Microsoft.AspNetCore.Mvc.Testing;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace WebApi.Test;
    
    public class Test_1 : BaseTest
    {
        [Fact] public async Task Test() => await Task.Delay(2000);
    }
    
    public class Test_2 : BaseTest
    {
        [Fact] public async Task Test() => await Task.Delay(2000);
    }
    
    public abstract class BaseTest : IAsyncLifetime
    {
        private readonly WebApplicationFactory<IWebApiMarker> _factory;
        protected BaseTest()
        {
            _factory = new WebApplicationFactory<IWebApiMarker>();
        }
        public async Task InitializeAsync()
        {
            using var scope = _factory.Services.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            await db.Database.EnsureCreatedAsync();
        }
        async Task IAsyncLifetime.DisposeAsync()
        {
            using var scope = _factory.Services.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            await db.Database.EnsureDeletedAsync();
            await _factory.DisposeAsync();
        }
    }
    

    Now I can run parallelism without any issue.