dotnet / EntityFramework.Docs

Documentation for Entity Framework Core and Entity Framework 6
https://docs.microsoft.com/ef/
Creative Commons Attribution 4.0 International
1.59k stars 1.95k forks source link

Clarify scopes of added services and how they interact #3259

Open royberris opened 3 years ago

royberris commented 3 years ago

Ask a question

Following the docs: https://docs.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor

I am trying to add a DbContextFactory for my DbContext so my program can work with blazor. I am running blazor components that access services.

These services have repositories. And these repositories have a base class which implements a lot of repeating behaviour.

Include your code

I am registering in startup:

services.AddDbContextFactory<FocusContext>(options => options.UseSqlServer(config.DBConfiguration.ConnectionString));

The config is added as a singleton before

services.AddSingleton<AppConfiguration>(_config);

Now I am using this in my repositories, which are transient. The base class is very big so I'll try to summarize it:

I still have the option to initialize from a DbContext directly for backwards compatibility.

internal abstract class Repository<TEntity, TKey> : IDisposable
        where TEntity : class, IEntity<TKey>
    {
        private DbSet<TEntity> _dbSet;

        protected Repository(IDbContextFactory<FocusContext> factory)
        {
            Context = factory.CreateDbContext();
        }

        protected Repository(FocusContext context)
        {
            Context = context;
        }

        protected FocusContext Context { get; init; }

        protected DbSet<TEntity> DbSet => _dbSet ??= Context.Set<TEntity>();

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            Context?.Dispose();
        }

        // ... a lot of methods that interact with the database go here.
        // example
        public async Task<TEntity> GetAsync(TKey id) =>
            await DbSet
                .AsQueryable()
                .FirstOrDefaultAsync(ActivePredicate.And(i => i.Id.Equals(id)))
            ?? throw new ObjectNotFoundException();

}

The Repository is implemented like this:

internal class FeedItemLikeRepository : Repository<FeedItemLike>
    {
        public FeedItemLikeRepository(IDbContextFactory<FocusContext> factory)
            : base(factory)
        {
        }

       // custom repository implementation here...
}

Now it is called from a transient service. And that service is injected in to blazor with @inject IService _service.

So my question is, with my structure. How can I achieve working with the DbContextFactory so that in blazor I do not get an error stating that my DbContext is accessed before another operation has finished.

I have a pretty big API that is depending on these repositories. (Not the services, different services for CMS / API). So I can't just go around and do a big refactor. But I might need to.

Include stack traces

When starting I get this breakpoint in the Program.cs file.

Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.EntityFrameworkCore.IDbContextFactory`1[Focus.Common.DB.FocusContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory`1[Focus.Common.DB.FocusContext]': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Focus.Common.DB.FocusContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Focus.Common.DB.FocusContext]'.) (Error while validating the service descriptor 'ServiceType: Focus.Common.App.Feed.Repositories.FeedItemRepository Lifetime: Transient ImplementationType: Focus.Common.App.Feed.Repositories.FeedItemRepository': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Focus.Common.DB.FocusContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Focus.Common.DB.FocusContext]'.) (Error while validating the service descriptor 'ServiceType: Focus.Common.App.Feed.Repositories.FeedItemContentRepository Lifetime: Transient ImplementationType: Focus.Common.App.Feed.Repositories.FeedItemContentRepository': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Focus.Common.DB.FocusContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Focus.Common.DB.FocusContext]'.) (Error while validating the service descriptor 'ServiceType: Focus.Common.App.Feed.Repositories.FeedItemContentLabelRepository Lifetime: Transient ImplementationType: Focus.Common.App.Feed.Repositories.FeedItemContentLabelRepository': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Focus.Common.DB.FocusContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Focus.Common.DB.FocusContext]'.) (Error while validating the service descriptor 'ServiceType: Focus.Common.App.Feed.Repositories.FeedItemLikeRepository Lifetime: Transient ImplementationType: Focus.Common.App.Feed.Repositories.FeedItemLikeRepository': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Focus.Common.DB.FocusContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Focus.Common.DB.FocusContext]'.) (Error while validating the service descriptor 'ServiceType: Focus.Common.App.Feed.Services.IFeedService Lifetime: Transient ImplementationType: Focus.Common.App.Feed.Services.FeedService': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Focus.Common.DB.FocusContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Focus.Common.DB.FocusContext]'.) (Error while validating the service descriptor 'ServiceType: Focus.Common.App.Feed.Services.IFeedManagementService Lifetime: Transient ImplementationType: Focus.Common.App.Feed.Services.FeedManagementService': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Focus.Common.DB.FocusContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[Focus.Common.DB.FocusContext]'.)

Include provider and version information

EF Core version: 5.0.0 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: net5.0 Operating system: Windows 10 IDE: Visual Studio 16.9.2

royberris commented 3 years ago

So doing some more research I came across this approach which allows me to keep the pure DbContext injection, but does create a new database for each injection. (Correct me if this is wrong).

This approach does not throw the error above. But I am not sure if this will introduce new issues that are not directly visible.

using var scope = services.BuildServiceProvider().CreateScope();
            var config = scope.ServiceProvider.GetRequiredService<AppConfiguration>();

            services.AddDbContextFactory<FocusContext>(options => options.UseSqlServer(config.DBConfiguration.ConnectionString));

            // Database
            services.AddTransient(p => p.GetRequiredService<IDbContextFactory<FocusContext>>().CreateDbContext());

Will this solve my issue, or do I get different issues with this approach.

ajcvickers commented 3 years ago

@royberris I don't see anything in your code that would obviously cause this. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

royberris commented 3 years ago

@ajcvickers I've tried to reproduce it in a new project but couldn't. So this bug was probably very specific to the project we have and once I fixed it I couldn't get back to the "broken" state. So the stacktrace is the only thing left of it.

Thanks

cubed-it commented 3 years ago

@ajcvickers I have just stumbled across this error. AddDbContext followed by AddDbContextFactory creates this state. AddDbContextFactory before AddDbContext on the other hand does not.

AndriySvyryd commented 3 years ago

AddDbContext adds the options as Scoped by default, while AddDbContextFactory does it as Singleton by default. We might need to call this out in documentation.

cubed-it commented 3 years ago

There may also be a missing overload for registering the options with a different ServiceLifetime? AddDbContext offers this possibility.

AndriySvyryd commented 3 years ago

@cubed-it options can't be registered with a shorter lifetime than the context/factory and it doesn't make sense to register the factory as transient, so the only valid configuration where the options lifetime would be different is scoped factory with singleton options. And I can't think of a scenario that would need this.

FindRobBrodie commented 1 year ago

I have just stumbled across this error. AddDbContext followed by AddDbContextFactory creates this state. AddDbContextFactory before AddDbContext on the other hand does not.

@cubed-it This was my exact issue. Rearranging the order of the calls to register those services fixed it!