防止与 EF Core 同步使用 API

Evi*_*eon 6 c# entity-framework-core ef-core-3.1

如何防止使用 Entity Framework Core 进行同步数据库访问?例如,我如何确保我们调用的是 ToListAsync() 而不是 ToList()?

我一直在尝试在对调用同步 API 的方法进行单元测试时抛出异常。是否有配置选项或一些我们可以覆盖的方法来使其工作?

我尝试过使用 DbCommandInterceptor,但是在使用内存数据库进行测试时,没有调用任何拦截器方法。

Evi*_*eon 3

解决方案是使用命令拦截器。

public class AsyncOnlyInterceptor : DbCommandInterceptor
{
    public bool AllowSynchronous { get; set; } = false;

    public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
    {
        ThrowIfNotAllowed();
        return result;
    }

    public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        ThrowIfNotAllowed();
        return result;
    }

    public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
    {
        ThrowIfNotAllowed();
        return result;
    }

    private void ThrowIfNotAllowed()
    {
        if (!AllowSynchronous)
        {
            throw new NotAsyncException("Synchronous database access is not allowed. Use the asynchronous EF Core API instead.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您想为此编写一些测试,可以使用 Sqlite 内存数据库。Database.EnsureCreatedAsync() 方法确实使用同步数据库访问,因此您需要一个选项来针对特定情况启用此功能。

public partial class MyDbContext : DbContext
{
    private readonly AsyncOnlyInterceptor _asyncOnlyInterceptor;

    public MyDbContext(IOptionsBuilder optionsBuilder)
        : base(optionsBuilder.BuildOptions())
    {
        _asyncOnlyInterceptor = new AsyncOnlyInterceptor();
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.AddInterceptors(_asyncOnlyInterceptor);
        base.OnConfiguring(optionsBuilder);
    }

    public bool AllowSynchronous
    {
        get => _asyncOnlyInterceptor.AllowSynchronous;
        set => _asyncOnlyInterceptor.AllowSynchronous = value;
    }
}
Run Code Online (Sandbox Code Playgroud)

这里有一些测试助手。确保您没有使用序列 (modelBuilder.HasSequence),因为 Sqlite 不支持此功能。

public class InMemoryOptionsBuilder<TContext> : IOptionsBuilder
    where TContext : DbContext
{
    public DbContextOptions BuildOptions()
    {
        var optionsBuilder = new DbContextOptionsBuilder<TContext>();
        var connection = new SqliteConnection("Filename=:memory:");
        connection.Open();
        optionsBuilder = optionsBuilder.UseSqlite(connection);
        return optionsBuilder.Options;
    }
}

public class Helpers
{
    public static async Task<MyDbContext> BuildTestDbContextAsync()
    {
        var optionBuilder = new InMemoryOptionsBuilder<MyDbContext>();
        var context = new MyDbContext(optionBuilder)
        {
            AllowSynchronous = true
        };
        await context.Database.EnsureCreatedAsync();
        context.AllowSynchronous = false;
        return context;
    }
}
Run Code Online (Sandbox Code Playgroud)