6

How can I prevent synchronous database access with Entity Framework Core? e.g. how can I make sure we are calling ToListAsync() instead of ToList()?

I've been trying to get an exception to throw when unit testing a method which calls the synchronous API. Are there configuration options or some methods we could override to make this work?

I have tried using a DbCommandInterceptor, but none of the interceptor methods are called when testing with an in-memory database.

Evil Pigeon
  • 1,887
  • 3
  • 23
  • 31
  • If you in ASP.NET Core, you can allow synchronized instead of forcing them to async. https://stackoverflow.com/a/55196057/1655141 –  Jul 06 '20 at 08:57

4 Answers4

3

The solution is to use a command interceptor.

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

If you're wanting to write some tests for this, you can use a Sqlite in-memory database. The Database.EnsureCreatedAsync() method does use synchronous database access, so you will need an option to enable this for specific cases.

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;
    }
}

Here are some helpers for testing. Ensure you aren't using sequences (modelBuilder.HasSequence) because this is not supported by 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;
    }
}
Evil Pigeon
  • 1,887
  • 3
  • 23
  • 31
1

How can I prevent synchronous database access with Entity Framework Core?

You can not. Period. THere is also no reason for this ever. You basically assume programmers using your API either are idiots or malicious - why else would you try to stop them from doing something that is legal in their language?

I have tried using a DbCommandInterceptor, but none of the interceptor methods are called when testing with an in-memory database

There are a TON of problems with the in memory database. I would generally suggest not to use it - like at all. Unless you prefer a "works possibly" and "never actually use advanced features of the database at all". It is a dead end - we never do unit testing on API like this, all our unit tests actually are integration tests and test end to end (vs a real database).

In memory has serious no guarantee to work in anything non trivial at all. Details may be wrong - and you end up writing fake tests and looking for issues when the issue is that the behavior of the in memory database just is a little different than the real database. And let's not get into what you can do with the real database that in memory has no clue how to do to start with (and migrations also do not cover). Partial and filtered indices, indexed views are tremendous performance tools that can not be properly shown. And not get into detail differences for things like string comparisons.

But the general conclusion is that it is not your job to stop users from calling valid methods on EfCore etc. and you are not lucky to actually do that - not a scenario the team will ever support. There are REALLY good reasons at time to use synchronous calls - in SOME scenarios it seems the async handling is breaking down. I have some interceptors (in the http stack) where async calls just do not work. Like never return. Nothing I ever tried worked there - so I do sync calls when I have to (thank heaven I have a ton of caching in there).

TomTom
  • 61,059
  • 10
  • 88
  • 148
  • Thanks for your answer. I respectfully disagree. Automated tests are cheaper than developer eyeballs. I mean I _could_ fork the ef core repo, and disable the synchronous API that way, so there is a way. – Evil Pigeon Jul 06 '20 at 10:03
  • You may disagree - that is your right. The statement is that what you want is irrelevant and not supported, and that is the truth. What you want does not count if you use a platform that does not support it. – TomTom Jul 06 '20 at 10:23
1

You can prevent it at compile-time to some degree by using the Microsoft.CodeAnalysis.BannedApiAnalyzers NuGet package. More information about it here.

Methods that end up doing synchronous queries can then be added to BannedSymbols.txt, and you will get a compiler warning when attempting to use them. For example adding the following line to BannedSymbols.txt gives a warning when using First() on an IQueryable<T>:

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

These warnings can also be escalated to become compiler errors by treating warnings as errors as explained here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/errors-warnings

Unfortunately not all synchronous methods can be covered by this approach. For example since ToList() is an extension on IEnumerable<T> (and not on IQueryable<T>), banning it will not allow any use of ToList() in the same project.

sveinungf
  • 841
  • 1
  • 12
  • 22
0

I can't really find a good Google answer for you. So my suggestion in the meantime is that you start doing peer-review, aka Code Reviews and any time you find a .Tolist(), you change it to await .ToListAsync().

It's not the most high tech solution, but it does keep everyone honest, but it also allows others to become familiar with your work should they ever need to maintain it while you're booked off sick.

Captain Kenpachi
  • 6,960
  • 7
  • 47
  • 68
  • 1
    Thanks for the answer, this is what we are doing already. Unfortunately human error can let these things slip through the cracks. Would be nice if we could run these tests on controller methods, for example. – Evil Pigeon Jul 06 '20 at 09:15
  • You shouldn't be writing data access code in your controllers. You should at the very least have separate projects for presentation (MVC, WebAPI, etc), business logic, and data access. The highest up data access code should go is in the business layer. And even that is pushing it. – Captain Kenpachi Jul 06 '20 at 09:34
  • 1
    Yes we do that. I was providing a simple example that expresses the following: we are using a web application therefore asynchronous IO is important so that we don't tie up the thread pool. Of course, our service layer methods return Task, but that doesn't prevent developers from accidentally calling the synchronous versions of methods when using EF Core. – Evil Pigeon Jul 06 '20 at 09:58