Using scoped services (ie DbContext) from two scopes without service locator

124 views Asked by At

I need to write some service with method that needs to perform two units of work, where second one will be preformed even if first failed and was rolled back. Creating and rolling back transaction is not enough, because DbContext still remembers changes from first unit of work. Therefore I need two DbContexts. Because I use other services to perform the work itself, I need to get the DbContext to them and i want it to be injected, not manually created or obtained via service locator.

Usual approach is to inject IServiceScopeFactory, create scope and from it resolve services involved in the work (DbContext to manage transaction and some service to do the work using this DbContext). What I don't like about this, is that nowhere in signature of the containing service or it's methods is information that those services are needed, and that my service knows about IServiceScopeFactory therefore knows at least something about used DI framework.

I'd like solution that allows my services to know as little as possible about DI framwork (it is allowed to know that conceptually scopes exist, but should not depend on any class/interface from DI framework).

example of what I don't want:

public class MyDbContext:DbContext { 
}

public class SomeService
{
    private IServiceScopeFactory _serviceScopeFactory;
    public SomeService(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }

    public void SomeMethod()
    {
        using(var scope = _serviceScopeFactory.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
            var dao = scope.ServiceProvider.GetRequiredService<DaoService>();
            using (var tx = dbContext.Database.BeginTransaction())
            {
                try
                {
                    dao.FirstWork();
                    dbContext.SaveChanges();
                    tx.Commit();
                }
                catch
                {
                    tx.Rollback();
                } 
            }
        }

        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
            var dao = scope.ServiceProvider.GetRequiredService<DaoService>();
            using (var tx = dbContext.Database.BeginTransaction())
            {
                try
                {
                    dao.SecondWork();
                    dbContext.SaveChanges();
                    tx.Commit();
                }
                catch
                {
                    tx.Rollback();
                }
            }
        }
    }
}

public class DaoService
{
    private MyDbContext _db;

    public DaoService(MyDbContext db)
    {
        _db = db;
    }

    public void FirstWork(){}
    public void SecondWork() { }
}

public class Test
{
    public void SomeTest()
    {
        new SomeService(/*now i need to prepare DI and register services, but i don't know which servicies I need*/null);
    }
}
2

There are 2 answers

2
Alpedar On

Inject some kind of scope factory with result type of IDisposable (or marker interface, but I dislike those) and service factory method for each dependency i need resoved from the scope, that resolve service from the scope object.

This way I can use DI framework or manualy create needed object and when manualy creating them, I cannot miss a dependency, because I know about it, because i must provide the service factory.

public class MyDbContext : DbContext
{
}

public delegate IDisposable CreateScope();
public delegate T CreateScopedService<T>(IDisposable scope);

public class SomeService
{
    private CreateScope _scopeFactory;
    private CreateScopedService<MyDbContext> _dbContextFactory;
    private CreateScopedService<DaoService> _daoFactory;

    public SomeService(CreateScope scopeFactory, CreateScopedService<MyDbContext> dbContextFactory, CreateScopedService<DaoService> daoFactory)
    {
        _scopeFactory = scopeFactory;
        _dbContextFactory = dbContextFactory;
        _daoFactory = daoFactory;
    }

    public void SomeMethod()
    {
        using (var scope = _scopeFactory())
        {
            var dbContext = _dbContextFactory(scope);
            var dao = _daoFactory(scope);
            using (var tx = dbContext.Database.BeginTransaction())
            {
                try
                {
                    dao.FirstWork();
                    dbContext.SaveChanges();
                    tx.Commit();
                }
                catch
                {
                    tx.Rollback();
                }
            }
        }

        using (var scope = _scopeFactory())
        {
            var dbContext = _dbContextFactory(scope);
            var dao = _daoFactory(scope);
            using (var tx = dbContext.Database.BeginTransaction())
            {
                try
                {
                    dao.SecondWork();
                    dbContext.SaveChanges();
                    tx.Commit();
                }
                catch
                {
                    tx.Rollback();
                }
            }
        }
    }
}

public class DaoService
{
    private MyDbContext _db;

    public DaoService(MyDbContext db)
    {
        _db = db;
    }

    public void FirstWork() { }
    public void SecondWork() { }
}

public class Test
{
    public void SomeTest()
    {
        new SomeService(() =>
        {
            MyDbContext ctx = new MyDbContext();
            return new TestScope
            {
                DaoService = new DaoService(ctx),
                DbContext = ctx
            };
        }, 
        d => d is TestScope scope ? scope.DbContext : null!,
        d => d is TestScope scope ? scope.DaoService : null!
        );
    }
}

public class TestScope : IDisposable
{
    public required MyDbContext DbContext { get; set; }
    public required DaoService DaoService { get; set; }

    public IDisposable CreateScope()
    {
        return this;
    }

    public void Dispose()
    {
    }
}
0
Guru Stron On

From pure technical perspective (without getting into details WHY, it actually seems that you having a bit more XY problem here) there are at least two approaches you can try.

  1. Use DbContext factory approach.

    Basically you swap the services.AddDbContext<AppDbContext>(...) registration and go with services.AddDbContextFactory<AppDbContext>(...) one and then resolve it in the service (actually you can still resolve AppDbContext too when needed) and create contexts on the fly:

    // IDbContextFactory<AppDbContext> _contextFactory
    ...
    using (var context = _contextFactory.CreateDbContext()) // you should dispose the factory created contexts
    {
        // ...
    }
    
  2. You can tackle the "because DbContext still remembers changes from first unit of work" issue via EF Core change tracker-specific APIs i.e.:

    // after first transaction
    context.ChangeTracker.Clear();