Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AddDbContextFactory to allow registering a factory for creating contexts #21246

Merged
merged 1 commit into from Jun 13, 2020

Conversation

ajcvickers
Copy link
Member

Fixes #18575

Mostly this is copied from @JeremyLikness's sample, with tests added. It evolved a bit as I was testing. Most significantly, we now register an IDbContextFactory interface, for which we provide a default implementation. This allows the factory to be replaced by one specific for the application, which can be more performant because it doesn't use ActivatorUtilities. Overloads have been added for this.

I believe we can also add overloads to allow the factory to resolve from a context pool, but I will file a new issue for this.

…ontexts

Fixes #18575

Mostly this is copied from @JeremyLikness's sample, with tests added. It evolved a bit as I was testing. Most significantly, we now register an IDbContextFactory interface, for which we provide a default implementation. This allows the factory to be replaced by one specific for the application, which can be more performant because it doesn't use ActivatorUtilities. Overloads have been added for this.

I believe we can also add overloads to allow the factory to resolve from a context pool, but I will file a new issue for this.
@ajcvickers ajcvickers requested a review from a team June 12, 2020 23:11
@ajcvickers
Copy link
Member Author

/cc @JeremyLikness

Copy link
Member

@AndriySvyryd AndriySvyryd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This basically makes Reset() obsolete
We should show custom factory implementation in the docs as that will probably be at least twice as performant

@ajcvickers
Copy link
Member Author

@AndriySvyryd Which Reset() are you referring to?

@ajcvickers ajcvickers merged commit d814eba into master Jun 13, 2020
@ajcvickers ajcvickers deleted the LiverpoolToLeedsCanal0608 branch June 13, 2020 14:46
@AndriySvyryd
Copy link
Member

I meant to say Clear() #21169

@ajcvickers
Copy link
Member Author

@AndriySvyryd I've come to the conclusion that it won't matter how many ways we have to create a context instance--some people are still going to want to detach all entity instances. If they are going to do it anyway, then Clear() is a safer, more performant option.

/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual TContext CreateDbContext()
=> ActivatorUtilities.CreateInstance<TContext>(_provider);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajcvickers for better performance you should stash and ObjectFactory in a field by calling ActivatorUtilities.CreateFactory<TContext>(). Then you can use that factory to create instances of TContext. It's much more optimized and avoids the constructor selection per call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidkarlsson
Copy link

Is it going to be possible to use both AddDbContext() and AddDbContextFactory() simultaneously to allow injecting a context directly but also be able to create new instances with the DbContextFactory while keeping the ServiceLifetime for the factory set to Singleton?

This doesn't seem possible right now as the DbContextFactory can't consume DbContextOptions because it's set as a scoped service by the call to AddDbContext() I think. This scenario throws the following exception for me at the moment and I don't see how I could fix it:

System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.EntityFrameworkCore.IDbContextFactory`1[Test.Database.TestDbContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory`1[Test.Database.TestDbContext]': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Test.Database.TestDbContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Test.Database.TestDbContext]'.) ---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: Microsoft.EntityFrameworkCore.IDbContextFactory`1[Test.Database.TestDbContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory`1[Test.Database.TestDbContext]': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Test.Database.TestDbContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Test.Database.TestDbContext]'. ---> System.InvalidOperationException: Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Test.Database.TestDbContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Test.Database.TestDbContext]'.
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.VisitScopeCache(ServiceCallSite scopedCallSite, CallSiteValidatorState state)
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.VisitConstructor(ConstructorCallSite constructorCallSite, CallSiteValidatorState state)
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.VisitRootCache(ServiceCallSite singletonCallSite, CallSiteValidatorState state)
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateCallSite(ServiceCallSite callSite)
  at at Microsoft.Extensions.DependencyInjection.ServiceProvider.Microsoft.Extensions.DependencyInjection.ServiceLookup.IServiceProviderEngineCallback.OnCreate(ServiceCallSite callSite)
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.ValidateService(ServiceDescriptor descriptor)
  --- End of inner exception stack trace ---
  at at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.ValidateService(ServiceDescriptor descriptor)
  at at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(IEnumerable`1 serviceDescriptors, ServiceProviderOptions options)
  --- End of inner exception stack trace ---
  at at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(IEnumerable`1 serviceDescriptors, ServiceProviderOptions options)
  at at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
  at at Microsoft.Extensions.DependencyInjection.DefaultServiceProviderFactory.CreateServiceProvider(IServiceCollection containerBuilder)
  at at Microsoft.Extensions.Hosting.Internal.ServiceFactoryAdapter`1.CreateServiceProvider(Object containerBuilder)
  at at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
  at at Microsoft.Extensions.Hosting.HostBuilder.Build()

@ajcvickers
Copy link
Member Author

@davidkarlsson Yes, this will work, but an overload of AddDbContext that allows the options lifetime to be set must be used, and the lifetime should be set to Singleton.

So we can understand how this feature is going to be used, could you provide some more details on why you want to do this?

@davidkarlsson
Copy link

@ajcvickers The reasons I would want to do this might be based on issues with my underlying design and the lifetime of the services I use..

I have multiple projects (ASP.NET and Windows services) that share singletons in which I would like to use the DbContextFactory for creating new contexts since they are not always used by the Windows service for example so injecting them in the constructor would be wasteful. At the same time I would like to continue injecting request scoped contexts directly in the controllers of the ASP.NET project instead of using the factory there so I don't have to dispose the contexts manually.

In the singletons I'm just creating new instances with the parameterless constructor at the moment and use OnConfiguring() for setting the connecting string using a static variable set by IConfiguration.Bind(). This doesn't feel quite right to me and using the DbContextFactory seems like a better alternative to this.

I'm also thinking that I might want to use the context pooling feature in the future which isn't possible with my current setup because the context class has a parameterless constructor. If I could use the DbContextFactory instead this would allow me to use the context pooling feature because I wouldn't need parameterless constructor any longer.

@davidfowl
Copy link
Member

I agree it should be possible to use both at the same time. It's really common to want to use a DbContext per request and the factory for singletons e.g. https://github.com/dotnet-presentations/aspnetcore-app-workshop/blob/b9cde864b6917c10fbef979a4806dd1a658030a8/src/FrontEnd/Services/AdminService.cs#L9-L44

@ajcvickers
Copy link
Member Author

@davidkarlsson Thanks for the info--your case makes sense and aligns with the reason we made this possible.

@davidkarlsson
Copy link

@ajcvickers But it isn't possible right now unless I'm missing something?

Changing the ServiceLifetime for contexts injected directly to Singleton with the AddDbContext() overload just so that it can be injected in the DbContextFactory makes it impossible to inject contexts directly in controllers with a request scoped lifetime. And I also can't change the lifetime of the factory to scoped instead because I need to be able to inject it in singletons.

@ajcvickers
Copy link
Member Author

@davidkarlsson Only the lifetime of the options needs to change from the default. For example:

.AddDbContext<WoolacombeContext>(
    b => b.UseInMemoryDatabase(nameof(WoolacombeContext)), ServiceLifetime.Scoped, ServiceLifetime.Singleton)
.AddDbContextFactory<WoolacombeContext>(
    b => b.UseInMemoryDatabase(nameof(WoolacombeContext)))

@davidkarlsson
Copy link

@ajcvickers I see! I totally missed that that was possible. Thank you!

@tomRedox
Copy link

tomRedox commented Nov 26, 2020

@ajcvickers I'd missed this was possible too and spent a while done the rabbit-hole before coming across this thread. I think a lot of people will be using the DbContextFactory approach with existing code and may run up against lifetime errors. Would it be worth adding something in the docs specifically about the need to set the lifetimes for AddDbContext?

I've stuck a Q&A on stack overflow to try and surface this too.

@ajcvickers
Copy link
Member Author

@tomRedox Thanks--I added a note to the item tracking documentation.

@lonix1
Copy link

lonix1 commented Apr 4, 2022

The alternative to the above solution is to register the factory as scoped as well:

.AddDbContext<WoolacombeContext>(
    b => b.UseInMemoryDatabase(nameof(WoolacombeContext)), ServiceLifetime.Scoped)
.AddDbContextFactory<WoolacombeContext>(
    b => b.UseInMemoryDatabase(nameof(WoolacombeContext)), ServiceLifetime.Scoped)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a performant way to register a context factory in DI
6 participants