abp icon indicating copy to clipboard operation
abp copied to clipboard

Repositories DbContext instances

Open Ahmedrz89 opened this issue 3 months ago • 9 comments

Is there an existing issue for this?

  • [x] I have searched the existing issues

Description

Hi guys thank you for the great product

I have been trying to figure out a solution for something and I could find any workaround the problem is that the default repositories dbcontext instances are different from the dbcontext instance that is injected in the same scope using the AppDbContext class or whatever it is named in the project, which is creating alot of problems for example when using masstransit outbox feature where internally they are injected the dbcontext by class and they relay on the transaction of this context to add the necessary outbox and inbox states as part of the same transaction so later a service can send that to message broker but the default way to insert and update entities in abp is to inject the IRepository<SomeEntity, Guid> and use that to insert entities and later the unit of work will save changes and commit the transaction which won't contain the entities of the masstransit outbox entities which uses a different instance internally and even injecting the AppDbContext in the appservice assuming it will return the same dbcontext instance that was used internally by masstransit so we can manually call savechanges on it that doesn't work as well because even the injected instance is not the same as the masstransit internal one what should we do to make this work and make sure everything is using the same dbcontext to committing works for everything

thanks

Reproduction Steps

No response

Expected behavior

No response

Actual behavior

No response

Regression?

No response

Known Workarounds

No response

Version

9.3.2

User Interface

MVC

Database Provider

EF Core (Default)

Tiered or separate authentication server

Tiered

Operation System

Windows (Default)

Other information

No response

Ahmedrz89 avatar Sep 13 '25 14:09 Ahmedrz89

This is because ABP manages the database context instance itself.

You might consider trying this:

context.Services.AddTransient<YourDbContext>(sp =>
{
    return sp.GetRequiredService<IDbContextProvider<YourDbContext>>().GetDbContext();
});

If you can provide a simple project to reproduce, I can help you better

realLiangshiwei avatar Sep 13 '25 16:09 realLiangshiwei

thanks for your response I tried that, it won't work because the provider will also inject the DbContext and this code will stack overflow out project is massive and won't be easy to share but it is easily reproducible just start with a clean abp template and a simple service to try to insert through dbcontext public interface ITest { public Task Publish<T>(T message) where T : class; }

public class Test : ITest, IScopedDependency
{
    readonly GatewayDbContext _dbContext;

    public Test(YourDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task Publish<T>(T message) where T : class
    {
        
        //insert anything here through the dbcontext and it won't persist during the uow commit
    }
}

you will lose the inserts that you do here without explicit SaveChangesAsync inside the class you can't in anyway save changes externally which is needed to merge the inserts from different libraries that internally inject DbContext like this I can create a new abp application with this, but how to share should I create a public repo in github for this ?

Ahmedrz89 avatar Sep 13 '25 22:09 Ahmedrz89

should I create a public repo in github for this ?

Sure, Please.

maliming avatar Sep 14 '25 01:09 maliming

https://github.com/Ahmedrz89/abp_dbcontext

you can check the TestApplicationService

Ahmedrz89 avatar Sep 14 '25 02:09 Ahmedrz89

[Dependency(ServiceLifetime.Scoped, ReplaceServices = true)] [ReplaceDbContext(typeof(IIdentityDbContext))] [ReplaceDbContext(typeof(ITenantManagementDbContext))] [ConnectionStringName("Default")] public class GatewayDbContext : AbpDbContext<GatewayDbContext>, ITenantManagementDbContext, IIdentityDbContext, IHasEventOutbox, IHasEventInbox, IScopedDependency

I tried making DbContext scoped and it did fix the problem but you obviously made transient for a reason and I don't know what will that break, but it is assumed that dbcontext is scoped so not sure if this is a good solution or not

Ahmedrz89 avatar Sep 14 '25 03:09 Ahmedrz89

Please ignore my previous reply, it is wrong. ABP uses the transient lifecycle by default for DbContext, which is inconsistent with ASP.NETCore default behavior, which is scoped. ABP will ensure that the database context instance in the current UnitOfwork is consistent.

When you change the code as follows, it will work

public class Test : ITest, IScopedDependency
{
    private readonly IDbContextProvider<AbpSolution1DbContext> _dbContextProvider;

    public Test(IDbContextProvider<AbpSolution1DbContext> dbContextProvider)
    {
        _dbContextProvider = dbContextProvider;
    }


    public async Task TestInsert()
    {
        var dbContext = await _dbContextProvider.GetDbContextAsync();
        var missingTest = new TestAggregate
        {
            Name = "Test Name"
        };

        dbContext.Tests.Add(missingTest);

    }
}

realLiangshiwei avatar Sep 14 '25 05:09 realLiangshiwei

I know how the abp works an resolve dbcontext and I know using the provider resolve a consistent instance but that is not the problem, The problem is that third party libraries assume the instance is scoped and rely on the higher level save changes to join the request transaction. This is very common in third party libraries Masstransit being one and one of the best selling point of abp is the integration and extension capabilities but currently I couldn't find a good way to make these kind of libraries work and get a single transaction that combine db changes of abp and other third party libraries and that's why I though I should ask here, Making the dbcontext scoped obviously fixes the problem but I dont know what will that break and what effect it has on other parts of the application, can you elaborate on this? Thank you for your help and support

Ahmedrz89 avatar Sep 14 '25 14:09 Ahmedrz89

I checked the code for masstransit, it does the operation through the same DbContext

You can try to replace the EntityFrameworkScopedBusContextProvider implementation

for example:

public class AbpEntityFrameworkScopedBusContextProvider<TBus, TDbContext>  :
    IScopedBusContextProvider<TBus>,
    IDisposable
    where TBus : class, IBus
    where TDbContext : DbContext,IEfCoreDbContext
{
    public AbpEntityFrameworkScopedBusContextProvider(TBus bus, IDbContextProvider<TDbContext> dbContextProvider, IBusOutboxNotification notification,
        Bind<TBus, IClientFactory> clientFactory, Bind<TBus, IScopedConsumeContextProvider> consumeContextProvider,
        IScopedConsumeContextProvider globalConsumeContextProvider, IServiceProvider provider)
    {
        var dbContext = dbContextProvider.GetDbContext();
        if (consumeContextProvider.Value.HasContext)
            Context = new ConsumeContextScopedBusContext(consumeContextProvider.Value.GetContext(), clientFactory.Value);
        else if (globalConsumeContextProvider.HasContext)
        {
            Context = new EntityFrameworkConsumeContextScopedBusContext<TBus, TDbContext>(bus, dbContext, notification, clientFactory.Value, provider,
                globalConsumeContextProvider.GetContext());
        }
        else
            Context = new EntityFrameworkScopedBusContext<TBus, TDbContext>(bus, dbContext, notification, clientFactory.Value, provider);
    }

    public void Dispose()
    {
        switch (Context)
        {
            case IDisposable disposable:
                disposable.Dispose();
                return;
        }
    }

    public ScopedBusContext Context { get; }
}
context.Services.ReplaceScoped<IScopedBusContextProvider<IBus>,AbpEntityFrameworkScopedBusContextProvider<IBus, YourDbContext>>();

Also, I recommend using the ABP EventBus Outbox system.

realLiangshiwei avatar Sep 15 '25 03:09 realLiangshiwei

Hi Dear thanks for the effort I tried that but it seems masstransit keeps the instance beyond the request and I started getting context disposed exceptions and also masstransit dbcontext provider logic is not consistent and it has many other classes that needs to change like EntityFrameworkOutboxContextFactory.cs

and what I did is disabled the masstransit outbox and created a distributed event in abp that uses the abp outbox and provided a handler that will push to broker without an outbox that solves half of the problem when using the IPublishEndpoint but now the state machine handling is broken and I will try to selectively enable outbox only for sagas and not for publish endpoint

I will try that and hopefully in theory should work

but thanks again for the great effort and I will let you know what works incase someone else stumble with the same issue

Ahmedrz89 avatar Sep 15 '25 09:09 Ahmedrz89