Perils of AddDbContext

The following snippet is the suggested approach to injecting an Entity Framework context in ASP.NET Core, taken from the docs page on Dependency Injection in ASP.NET Core:

services.AddDbContext<SqlContext>(options => 
    options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));

This means that whenever you have a constructor that includes SqlContext, the runtime will provide an instance of SqlContext without the developer having to type new anywhere.

Dependency injection in ASP.NET Core comes in three flavours: Transient, meaning a new object (e.g. SqlContext) is called each time a constructor requires the dependency; Scoped, meaning the same object is used for all dependencies of that type during the current web request; and Singleton, where only one instance of the object is ever created. The default for AddDbContext is Scoped meaning that during a web request all classes will be accessing the same SqlContext.

When Entity Framework fetches a record from the database it caches it so if the application requests it again it returns the results from its cache. If someone else updated the database during the request, their change won’t be seen by the first caller. For short and stateless web requests this seems like a reasonable optimization. While technically the data you’re seeing is inconsistent, it was valid a few milliseconds ago, so any permissions that might have been revoked were valid then, and if you try and write anything then optimistic concurrency will alert you to a problem.

Problems

However care must be taken as it is very easy to end up getting cached values when that wasn’t intended.

If the dependent object is a singleton, then it will receive a context instance when the object is first created, and hold onto that context until the server recycles. During that time anything it reads will be cached in the context and any changes to the database ignored. This is particularly problematic where there are balanced web servers where the singletons on two different servers will be unaware of the changes the other made.

This approach is also quite opaque. It is not obvious from the code, compared to say a using statement, the lifetime of the context and therefore what side-effects might occur among the many classes in a given web request using this context. This is true of any object with scoped lifetime and is why I tend to avoid using scoped.

While not really a problem with its usage, that we injecting a concrete class rather than an interface doesn’t really follow the basic pattern of dependency injection. It is not terribly difficult to create an interface for the context, but as noted earlier, it is not the standard being documented. I assume the reason for this is that the testability problem with injecting a concrete context has been removed by the introduction of DbContextOptionsBuilder.UseInMemoryDatabase().

A Solution

Frankly I prefer more control and transparency so I’ve returned to the time-honored tradition of having a data factory so that my classes can create a database context when they want to. To do this the factory uses the IoC container to create contexts on demand.

public interface ISqlContextFactory { SqlContext NewContext { get; }}

public class SqlContextFactory : ISqlContextFactory
{
    private Func<SqlContext> _getSqlContext;
    public SqlContextFactory(Func<SqlContext> getSqlContext)
    {
        _getSqlContext = getSqlContext;
    }
    public SqlContext NewContext { get { return _getSqlContext(); } }
}

This could be done by passing the IoC container into the factory, but I chose to pass the factory a function which generates the contexts, leaving options configuration in Startup.

Edit 16-March-2017: The original code source caused problems with the mysql connector similar to those discussed on Connection reuse here. The following code has been updated so the options are set for every request, rather than just once at startup, and this appears to have fixed the issue.

services.AddSingleton<ISqlContextFactory, SqlContextFactory>(provider => {

    string mysqlConnStr = Configuration.GetConnectionString("Mysql");

    return new Data.SqlContextFactory(() => {
        var options = new DbContextOptionsBuilder<Data.SqlContext>();
        options.UseMySql(mysqlConnStr);
        return new Data.SqlContext(options.Options);
    });
});

By removing the AddDbContext we have broken migrations and fixing this requires a little unsavory use of statics. The dotnet ef command line seems to run the Startup class but does not use the dependency injector, so instead I created a little class just for the migrations to use, and set the static connection string in Startup.ConfigureServices() so that the migration class doesn’t have to repeat the configuration loading code.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

/// This is ONLY for use by dotnet command line tool
public class SqlContextMigrationsTarget : IDbContextFactory<SqlContext>
{
    public static string ConnectionString;
    private DbContextOptionsBuilder<SqlContext> _builder = new DbContextOptionsBuilder<SqlContext>();

    public SqlContextMigrationsTarget() {}

    public SqlContext Create(DbContextFactoryOptions options)
    {
        _builder.UseMySql(ConnectionString);
        return new SqlContext(_builder.Options);
    }
}

// and in Startup.ConfigureServices()
SqlContextMigrationsTarget.ConnectionString = Configuration.GetConnectionString("Mysql");