Use both AddDbContextFactory() and AddDbContext() extension methods in the same project
Asked Answered
T

4

46

I'm trying to use the new DbContextFactory pattern discussed in the DbContext configuration section of the EF Core docs.

I've got the DbContextFactory up and running successfully in my Blazor app, but I want to retain the option to inject instances of DbContext directly in order to keep my existing code working.

However, when I try to do that, I'm getting an error along the lines of:

System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory1[MyContext]': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'.) ---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory1[MyContext]': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'. ---> System.InvalidOperationException: Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'.

I also managed to get this error at one point while experimenting:

Cannot resolve scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MyContext]' from root provider.

Is it theoretically possible to use both AddDbContext and AddDbContextFactory together?

Telluride answered 26/11, 2020 at 13:9 Comment(0)
T
66

It is, it's all about understanding the lifetimes of the various elements in play and getting those set correctly.

By default the DbContextFactory created by the AddDbContextFactory() extension method has a Singleton lifespan. If you use the AddDbContext() extension method with it's default settings it will create a DbContextOptions with a Scoped lifespan (see the source-code here), and as a Singleton can't use something with a shorter Scoped lifespan, an error is thrown.

To get round this, we need to change the lifespan of the DbContextOptions to also be 'Singleton'. This can be done using by explicitly setting the scope of the DbContextOptions parameter of AddDbContext()

services.AddDbContext<FusionContext>(options =>
    options.UseSqlServer(YourSqlConnection),
    optionsLifetime: ServiceLifetime.Singleton);

There's a really good discussion of this on the EF core GitHub repository starting here. It's also well worth having a look at the source-code for DbContextFactory here.

Alternatively, you can also change the lifetime of the DbContextFactory by setting the ServiceLifetime parameter in the constructor:

services.AddDbContextFactory<FusionContext>(options => 
    options.UseSqlServer(YourSqlConnection), 
    ServiceLifetime.Scoped);

The options should be configured exactly as you would for a normal DbContext as those are the options that will be set on the DbContext the factory creates.

Telluride answered 26/11, 2020 at 13:9 Comment(9)
This answer is very useful to people who are upgrading to .NET 5 and are looking to use Blazor and MVC Core in the same project, although I do not know what ApplyOurOptions is. If that isn't a part of EF that I'm unaware of, perhaps consider converting this to use normal options?Ringe
Hi @BrianMacKay, that was exactly our situation. Sorry, ApplyOurOptions is a helper function from our code, I've changed the code and added some clarification.Telluride
@tomRedux I bet prior to .NET 5, you had to do the same thing as me and figure out how to implement your own AddDbContextFactory extension. Good times. :)Ringe
One caveat: If you're using options.AddInterceptors(...) with AddDbContext alone you'll get a new instance of each interceptor for every new Scoped context (because interceptors are created along with options). So generally this means for each web request you'd get clean interceptors. However if you switch to using AddDbContextFactory you'll now be able to create multiple independent contexts, but they'll all share inteceptors (via the shared options). This is fine if your interceptors are completely thread safe, but be careful if they aren't!Mountbatten
... this is because for AddDbContextFactory there's no way you can specify optionsLifetime to be different from contextLifetime (at least if there is let me know!). There's a similar situation for pooling.Mountbatten
@BrianMacKay And even in 2022 this answer is still useful, as Blazor still used standard Razor Pages for the whole ASP.NET Core Identity thing! I scaffolded those in my Blazor app and found out that way- Horrible!Elute
@Elute Yes, I feel that using Auth0 / Okta (maybe Identity Server) is a better pattern. Especially great for being able to tell compliance auditors that the password never passes through your system, and having your login page super hardened because that's all these folks do. It does bring some complexity with it however.Ringe
Your sample code for AddDbContext has contextLifetime: ServiceLifetime.Transient. There's nothing wrong with this, but it's different than the default, which is scoped. Since the focus is the options lifetime, you may want to leave the service lifetime as the default in your example to avoid confusion (i.e., remove the line with contextLifetime).Kaif
@EdwardBrey good point, I've done that now.Telluride
M
16

Important point:

Both AddDbContextFactory and AddDbContext internally register the DbContextOptions<T> inside a shared private method AddCoreServices using TryAdd. (source)

Which effectively means whichever one is in your code first is the one that gets used.

So you can actually do this for a cleaner setup:

services.AddDbContext<RRStoreContext>(options => {

   // apply options

});

services.AddDbContextFactory<RRStoreContext>(lifetime: ServiceLifetime.Scoped);

I'm using the following to prove to myself it really does function like that:

services.AddDbContextFactory<RRStoreContext>(options =>
{
   throw new Exception("Oops!");  // this should never be reached

}, ServiceLifetime.Scoped);    

Unfortunately I have some query interceptors that aren't thread safe (which is the whole reason I wanted to make multiple instances with a factory), so I think I'll need to make my own context factory because I have separate initialization for Context vs. ContextFactory.


Edit: I ended u making my own context factory to be able to create new options for every new context that was created. The only reason was to allow for non-thread safe interceptors, but if you need that or something similar then this should work.

Influenced by: DbContextFactory

public class SmartRRStoreContextFactory : IDbContextFactory<RRStoreContext>
{
    private readonly IServiceProvider _serviceProvider;

    public SmartRRStoreContextFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public virtual RRStoreContext CreateDbContext()
    {
        // need a new options object for each 'factory generated' context
        // because of thread safety isuess with Interceptors
        var options = (DbContextOptions<RRStoreContext>) _serviceProvider.GetService(typeof(DbContextOptions<RRStoreContext>));
        return new RRStoreContext(options);
    }
}

Note: I only have one context that needs this so I'm hardcoding the new context in my CreateDbContext method. The alternative would be to use reflection - something like this DbContextFactorySource.

Then in my Startup.cs I have:

services.AddDbContext<RRStoreContext>(options => 
{
    var connection = CONNECTION_STRING;

    options.UseSqlServer(connection, sqlOptions =>
    {
        sqlOptions.EnableRetryOnFailure();
    });

    // this is not thread safe
    options.AddInterceptors(new RRSaveChangesInterceptor());

}, optionsLifetime: ServiceLifetime.Transient);

// add context factory, this uses the same options builder that was just defined
// but with a custom factory to force new options every time
services.AddDbContextFactory<RRStoreContext, SmartRRStoreContextFactory>();  

And I'll end with a warning. If you're using a factory (CreateDbContext) in additional to a 'normal' injected DbContext make extra sure not to mix entities. If for instance you call SaveChanges on the wrong context then your entities won't get saved.

Mountbatten answered 8/4, 2021 at 3:30 Comment(7)
that's really interesting, thank you. I'll have a look at that. I think I'll do exactly the same with the exception to help remind me what's going on!Telluride
Basically spent the whole day sort of exploring this and some surrounding issues with async. Managed to save 1 second from an operation that takes 1.5 seconds. It runs once a day so that's about 6 minutes a year I'm saving :-)Mountbatten
Wow, thanks for this. I was surprised to see this behaviour. It feels like .NET should be tracking which options to use for different "requesters"; I'm glad that in my case, the options are the same whether using the ContextPool or the ContextFactory.Muhammadan
@Muhammadan it's just tracked by dependency injection with the generic type name DbContextOptions<MyContextName> - so whether the factory or the non-factory requests it you'll get the same options (and the scope determines whether or not it's a shared copy)Mountbatten
Hmm, when using AddDbContext with transient lifetime it might leak types. Normally the scoped lifetime is used for DbContext because it's calling dispose after scope ends. But when one uses transient the user must call the dispose I think, and it might leak?All
@All I usually do (using var db = new ...) from years of habit - so this quite likely may be true and I'm already working around it! I wonder how to know for sure.Mountbatten
@All No, Dispose() is called at end-of-request for both Scoped and Transient. The difference is Scoped the same instance will be given to multiple objects. This is nice if you want all users of EF to use the same cache for Find(), but still surprises me often. Transient each service (repository and Controller) will get different EF context objects, even for same type. But in both cases they are disposed at the end of request. See #40844651Boozy
B
10

Sarting with EF Core 6 (.NET 6) you don't need to use both in most cases, since AddDbContextFactory also registers the context type itself as a scoped service.

So in services with Singleton lifetime inject IDbContextFactory<MyDbContext>, and in services with Scoped lifetime (e.g. MVC or API controllers) inject MyDbContext directly.

If you want to use different DbContextOptions in each case or if the Singleton lifetime of DbContextOptions do not fit your needs, you have to go with the solutions provided by the other answers.

See also:
Related Github enhancement issue

Bircher answered 19/5, 2023 at 8:34 Comment(0)
G
2

I had a similar error, but I solved it in a different way, because for AddDbContextFactory I needed Sigleton. I added the services in a different order, first AddDbContextFactory and then AddDbContext. Here's what it looks like in my code:

builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, c => c.UseNodaTime()));

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, c => c.UseNodaTime()));
Guillermoguilloche answered 26/12, 2022 at 20:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.