Repositories DbContext instances
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
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
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 ?
should I create a public repo in github for this ?
Sure, Please.
https://github.com/Ahmedrz89/abp_dbcontext
you can check the TestApplicationService
[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
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);
}
}
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
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.
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