阻止使用 EF Core 同步使用 API



如何防止使用实体框架核心进行同步数据库访问? 例如,如何确保我们调用的是 ToListAsync(( 而不是 ToList((?

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

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

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

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.");
}
}
}

如果你想为此编写一些测试,你可以使用 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;
}
}

以下是一些用于测试的帮助程序。确保您没有使用序列(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;
}
}

如何防止使用实体框架核心进行同步数据库访问?

你不能。句号。这也不是永远的理由。你基本上假设使用你的API的程序员要么是白痴,要么是恶意的 - 否则你为什么要试图阻止他们做一些用他们的语言合法的事情?

我尝试过使用 DbCommandInterceptor,但没有一个拦截器方法是 使用内存中数据库进行测试时调用

内存数据库中存在大量问题。我通常建议不要使用它 - 就像根本不一样。除非您更喜欢"可能工作"和"根本不实际使用数据库的高级功能"。这是一个死胡同 - 我们从来没有像这样在API上进行单元测试,我们所有的单元测试实际上是集成测试和端到端测试(相对于真正的数据库(。

在记忆中,根本无法保证在任何非平凡的事情上工作。细节可能是错误的 - 当问题是内存数据库的行为与真实数据库略有不同时,您最终会编写虚假测试并寻找问题。让我们不要讨论你可以用内存中不知道如何开始的真实数据库做什么(迁移也不包括在内(。部分和过滤索引,索引视图是无法正确显示的巨大性能工具。并且不详细介绍字符串比较之类的差异。

但一般的结论是,阻止用户在 EfCore 等上调用有效方法不是您的工作,而且您实际上这样做并不幸运 - 这不是团队会支持的场景。目前有很好的理由使用同步调用 - 在某些情况下,异步处理似乎正在崩溃。我有一些拦截器(在 http 堆栈中(,其中异步调用不起作用。就像再也回不来了。我曾经尝试过的东西在那里工作 - 所以我在必要时会同步调用(谢天谢地,我在那里有大量缓存(。

可以使用Microsoft.CodeAnalysis.BannedApiAnalyzersNuGet 包在编译时在一定程度上阻止它。有关它的更多信息,请点击此处。

然后可以将最终执行同步查询的方法添加到BannedSymbols.txt中,并且在尝试使用它们时会收到编译器警告。例如,在IQueryable<T>上使用First()时,将以下行添加到BannedSymbols.txt会发出警告:

M:System.Linq.Queryable.First`1(System.Linq.IQueryable{``0});Use async overload

也可以通过将警告视为错误来将这些警告升级为编译器错误,如下所述: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/errors-warnings

遗憾的是,并非所有同步方法都可以通过此方法涵盖。例如,由于ToList()IEnumerable<T>(而不是IQueryable<T>(的扩展,因此禁止它不允许在同一项目中使用任何ToList()

我真的找不到一个好的谷歌答案给你。所以我在此期间的建议是你开始做同行评审,也就是代码评审,任何时候你找到一个.Tolist(),你把它改成等待.ToListAsync()

这不是最高科技的解决方案,但它确实让每个人都诚实,但它也允许其他人熟悉你的工作,如果他们需要在你生病时维护它。