【问题标题】:Ensuring the same DbContext within a SCOPED RequestEnsuring the same DbContext within a SCOPED Request
【发布时间】:2022-12-27 01:06:55
【问题描述】:

In Entity Framework (EF) Core, SCOPED OBJECTS are the same within a request (but different across different requests). Calling AddDbContext is supposed to be SCOPED by-default...so I am expecting each DbContext instance to be the same instance when marked as SCOPED...and it's not.

I know this because every DbContext handed-up using Dependency Injection (DI) has a different ContextId...and "save changes" no longer works across all Repository's in my UnitOfWork. As such, it seems like DbContext creation is acting TRANSIENT not SCOPED.

Q: How do I guarantee each instance of the concrete DbContext is the same object in EF Core's DI-model?

Why do I want this?
Calling the UnitOfWork's "save changes" used to work across all Repository's...but not anymore because each DbContxet is different (and has a separate change tracker)

Lamar Service Registry Code:

public class ContainerRegistry : ServiceRegistry
{
    public ContainerRegistry()
    {
        Scan(scan =>
        {
            scan.TheCallingAssembly();
            scan.WithDefaultConventions();
            scan.LookForRegistries();
            scan.SingleImplementationsOfInterface();
        });

        // --------
        // DATABASE
        //ForSingletonOf<WorkflowComponentDbContext>(); //<-- Doesnt work b/c each DbContext is still a separate instance

        For<DbContext>().Use<WorkflowComponentDbContext>();
        For(typeof(IAuditableRepository<>)).Use(typeof(GenericAuditableRepository<>));

        // Policies (are used to map Constructor args)
        Policies.Add<GenericRepositoryConfiguredInstancePolicy>();
        Policies.Add<UnitOfWorkConfiguredInstancePolicy>();
    }
}

Host Builder Code:

private IHostBuilder CreateHostBuilder(string[] args)
{
    var builder = new HostBuilder()
                        .ConfigureAppConfiguration((hostingContext, config) =>
                        {
                            config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
                        })
                        .UseServiceProviderFactory<ServiceRegistry>(new LamarServiceProviderFactory())
                        .ConfigureServices((hostContext, services) =>
                        {
                            var connectionString = hostContext.Configuration.GetConnectionString(JsonSettings.ConnectionStrings.WorkflowComponentDb);
                            
                            services.AddLamar(IoC.Build());
                            services.AddScoped<IWindowsIdentityHelper, WindowsIdentityHelper>();
                            
                            // This is supposedly SCOPED by-default?
                            // And while, this passes-back OPTIONS correctly...it isn't passing a "singleton object" throughout the request
                            services.AddDbContext<ProjectManagementDbContext>((provider, options) => 
                            {
                                options.UseSqlServer(connectionString);
                            });
                            services.AddDbContext<WorkflowComponentDbContext>((provider, options) =>
                            {
                                options.UseSqlServer(connectionString);
                            });
                            
                            // This doesnt work either b/c it hands-back a new instacne of the Factoty each time (I tested this)....
                            //services.AddDbContextFactory<WorkflowComponentDbContext, WorkflowComponentDbContextFactory>((provider, options) =>
                            //{
                            //  options.UseSqlServer(connectionString);
                            //}, ServiceLifetime.Scoped);
                        });
    return builder;
}

LOW-TECH OPTION: Pass-in the IContainer
I really don't want to do this...but can

// -----
// NOTE: Some code omitted for brevity
public class WorkflowComponentUnitOfWork : IUnitOfWork
{
    // OPTION: I could pass the IContainer to build some dependecies?
    public WorkflowComponentUnitOfWork(DbContext dbContext, IContainer container)
    {
        DbContext = dbContext;

        ContextType = new GenericAuditableRepository<ContextType>(DbContext);
        ContextType.AuditResolver = container.GetRequiredService<IAuditResolverOf<ContextType>>();

        ObjectState = new GenericAuditableRepository<ObjectState>(DbContext);
        ObjectState.AuditResolver = container.GetRequiredService<IAuditResolverOf<ObjectState>>();

        ObjectStateEvent = new GenericAuditableRepository<ObjectStateEvent>(DbContext);
        ObjectStateEvent.AuditResolver = container.GetRequiredService<IAuditResolverOf<ObjectStateEvent>>();

        Workflow = new GenericAuditableRepository<Workflow>(DbContext);
        Workflow.AuditResolver = container.GetRequiredService<IAuditResolverOf<Workflow>>();

        WorkflowEvent = new GenericAuditableRepository<WorkflowEvent>(DbContext);
        WorkflowEvent.AuditResolver = container.GetRequiredService<IAuditResolverOf<WorkflowEvent>>();

        WorkflowTransition = new GenericAuditableRepository<WorkflowTransition>(DbContext);
        WorkflowTransition.AuditResolver = container.GetRequiredService<IAuditResolverOf<WorkflowTransition>>();
    }

    public virtual void SubmitChanges()
    {
        DbContext.SaveChanges();
    }
}

LOW-TECH OPTION: Call "save changes" across all repository's
I really don't want to do this...but can

// -----
// NOTE: Some code omitted for brevity
public class WorkflowComponentUnitOfWork : IUnitOfWork
{
    [SetterProperty]
    public IAuditableRepository<ContextType> ContextType { get; set; }

    [SetterProperty]
    public IAuditableRepository<ObjectState> ObjectState { get; set; }

    [SetterProperty]
    public IAuditableRepository<ObjectStateEvent> ObjectStateEvent { get; set; }

    [SetterProperty]
    public IAuditableRepository<Workflow> Workflow { get; set; }

    [SetterProperty]
    public IAuditableRepository<WorkflowEvent> WorkflowEvent { get; set; }
    
    [SetterProperty]
    public IAuditableRepository<WorkflowTransition> WorkflowTransition { get; set; }

    // OPTION: I could call "Save Changes" across each Repository
    public virtual void SubmitChanges()
    {
        ContextType.SaveChanges();
        ObjectState.SaveChanges();
        ObjectStateEvent.SaveChanges();
        Workflow.SaveChanges();
        WorkflowEvent.SaveChanges();
        WorkflowTransition.SaveChanges();
    }
}

UPDATES:
Using the following does not work...

For<DbContext>().Use<WorkflowComponentDbContext>().Scoped();

【问题讨论】:

    标签: c# entity-framework


    【解决方案1】:

    As an answer to your question: The documentation here (https://jasperfx.github.io/lamar/guide/ioc/lifetime.html) adds an extra .Scoped() to the For&lt;I&gt;().Use&lt;T&gt;().. Looks like you should add this to your Lamar-definition of ProjectManagementDbContext.

    Additionally: Why do you use services.AddDbContext&lt;ProjectManagementDbContext&gt;(..)andFor&lt;DbContext&gt;().Use&lt;ProjectManagementDbContext&gt;(). There is an overload for AddDbContext&lt;T1, T2&gt;(..).

    As an advice (from my own error): The way you have to setup your DI would mean, thatallDbContexts share state. Consider refactoring your code to be able use transient DbContexts (from the names given maybe your repositories are just wrapping DbSets ... but that's just a guess).

    【讨论】:

    • RE: "Services.AddDbContext & For<DbContext>": This application takes-in multiple UnitOfWork's. Meaning, it talks to 2 separate databases. That said...the "For<DbContext>" piece tells Lamar maps which concrete to plug for DbContext. Meanwhile..."Services.AddDbContext" is used to gather DbContextOptions and set Host Scope.
    • RE: "All DbContexts share state": Yes, that is correct...all Repositiry's should use the same DbContext instance. Otherwise, Change Tracking will only work within 1 Repository at-a-time...and you have to call "Save Changes" across all Repository's manually (I have tested this).
    • RE: "Services.AddDbContext & For<DbContext>": That said...If you have a better setup I'm definitely interested in seeing it. Thanks ahead of time.
    • Regarding ""For<DbContext>" piece tells Lamar...": You can do that as well with the mentioned overload of AddDbContext. Just use AddDbContext&lt;DbContext, ProjectManagementDbContext&gt;. No need to configure things in two places. Regarding "if you have a better setup": that would need more insight to your requirements. But generally speaking having just one system being responsible for DI to me seems better. Why do you need both?
    • RE: "Why do you need both": This is my 1st SWAG at Core DI & I would LOVE to be able to configure as much as possible in Lamar (and simply consume THAT). However, I have not been successful setting DbContextOptions in Lamar (I figured it was because the "proper values" exist only in the Host). As such...I've been using Lamar for class definition & Host for SCOPE.
    猜你喜欢
    • 2018-11-07
    • 2022-12-01
    • 2022-12-02
    • 2021-01-18
    • 2022-12-26
    • 2020-11-16
    • 1970-01-01
    • 2022-11-09
    • 2022-12-01
    相关资源
    最近更新 更多