- ABP Framework version: v9
- Database System: EF Core (PostgreSQL)
- Tiered (for MVC) or Auth Server Separated (for Angular): yes
Hello i have a very general question about how the unitofwork is working in Abp. If i use multiple db context with separate connection strings in my modular monolith app and if i want to do multiple operations with one unitofwork. Is it going to be a problem? since each of the modules are going to have different databases, i want to ask how does transaction is going to be handled. If i raise a domain event and as a side effect in the same scope i want to also insert a record on another database server? Or if the app has a separate databases for each module on one database server. Thank you for the assistance.
8 Answer(s)
-
1
Hi
There no such feature neither in ABP Framework and Entity Framework Core.
Normally, transaction between different databases is not possible but some specific cases. As far as I know, it can be possible in SqlServer somehow but making it stable for all scenarios is another tough topic. You can go with this implementation but you have to implement it by yourself. https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/distributed-transactions
We suggest using EventBus for this kind of scenarios. You can publish an event to update the other database and go on.
If you use LocalEventBus, it will be a local transaction, and whenever an exception occurs in the consumer side, the transaction from the publisher side will be rolled back.
If you use DistributedEventBus, it will be a distributed transaction, and whenever an exception occurs in the consumer side, it'll be retried until it becomes successful. You should handle this in your consumer side and make it eventually consistent.
Note:
Make sure you're using Outbox / Inbox for Transactional Events feature for the distributed event bus.
-
0
Hello Enis, Thanks for the reply. I understand the concept. I just want to give one example since i don't know how abp would react to it. Let's say I have one separate database for each module. Let's assume CreateOrder() appservice have been called from Ordering Module and as a side effect of the module i raised a local event OrderCreated. On Ticketing Module i want to issue a ticket for that event. OrderCreatedEventHandler catched the event and trying to insert a new ticket to Ticketing Database(which is separate from Ordering Database). If you can not reach the Ticketing Database at that time what is happening? Is UnitOfWork rolling back so the Ordering insert is also rolled back or eventual consistency is broken. I believe it is gonna be broken as you mentioned you can not create transaction between separate databases. Just correct me if i am wrong.
And if it is broken. What can i do to solve the problem? My solution could be using inbox / outbox pattern in that case. Even if it is a local event if you can implement inbox / outbox pattern, then you can raise the same event from inbox messages again after some time and correct the state for Ticketing module. Is Abp supports that case? I know Abp supports it for DistributedEventBus. Can i use it also for LocalEventBus? And is there any sample for it?
PS:
If you use LocalEventBus, it will be a local transaction, and whenever an exception occurs in the consumer side, the transaction from the publisher side will be rolled back.
so you say even if it is a different database, unitofwork is going to rollback the transaction? are you sure about it? Isn't unitofwork calling savechanges() for each dbcontext and closing the transaction at the end of unitofwork?
-
0
You can follow this documentation: https://abp.io/docs/latest/framework/infrastructure/event-bus/local#transaction-exception-behavior
It refers;
Event handlers are always executed in the same unit of work scope, that means in the same database transaction with the code that published the event. If an event handler throws an exception,
It doesn't support multiple databases or connection strings. Runs all your operations in the same unit of work. That means there is no single database transaction that depends on each other. When you face a problem they'll rollback separately according to your scenario. You may visit: https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work#unit-of-work
If you use different repositories and DbContexts within the same Unit of Work (UOW), it will still roll back both while using LocalEventBus or directly coding in the same method with 2 different repositories or DbContext instances. This is a feature of Unit of Work and not a native database transaction
so you say even if it is a different database, unitofwork is going to rollback the transaction?
Unit Of Work is database agnostic, it doesn't know which database are you using. If one of your repositories throws an exception while committing it'll try rollback all.
- When an exception occurs during the unit of work:
- The RollbackAsync() method is called
- It attempts to rollback ALL registered database APIs and transaction APIs
- Each rollback operation is wrapped in a try-catch to ensure all DBs get rollback attempts
- For multiple DbContexts:
- Each DbContext is registered as a separate database API
- They can be connected to different databases
- When one fails, the UnitOfWork will attempt to rollback all of them So to answer your question: Yes, both DbContexts will be rolled back even if they're connected to different databases. This is by design to maintain data consistency across your entire business transaction.
Here's a sequence of what happens:
1. DbContext1 (Database1) - Succeeds 2. DbContext2 (Database2) - Throws Exception 3. UnitOfWork catches exception 4. UnitOfWork calls RollbackAllAsync() 5. Both DbContext1 and DbContext2 get rollback attempts
This behavior ensures atomicity across multiple databases - either all operations succeed, or none of them do. This is particularly important for distributed transactions where you need to maintain consistency across different data stores.
- When an exception occurs during the unit of work:
-
0
ok i got it Enis. Thank you for the explanation. So when you call unitOfWork.complete() it closes all the dbcontexts inside the unitofwork so before it comes to there if there is an exception all the dbcontexts are gonna be rolled back.
Just one more question. Does it have any performance problem since it is dealing with multiple db contexts with separate connection strings in one request? Cause i have seen in so many examples, they do not deal with unitofwork in multiple databases instead they have implemented inbox/outbox pattern and closing the transactions for each modules. So you can think they use separate unit of work for each module and use inbox/outbox pattern.
-
0
Just one more question. Does it have any performance problem since it is dealing with multiple db contexts with separate connection strings in one request? Cause i have seen in so many examples, they do not deal with unitofwork in multiple databases instead they have implemented inbox/outbox pattern and closing the transactions for each modules. So you can think they use separate unit of work for each module and use inbox/outbox pattern.
Short answer is yes.
This unit of work and rollback logic requires execution of all the db context operations in the same transaction and to end the request all the db context operations should be completed. HTTP Request lasts until all the operations are completed. It brings some drawbacks like performance issues. Also, it depends on the database operations and your deployment environment. But still it costs some performance instead of using single db context.
Even if you use
LocalEventBus
, still all the operations will be executed in the same transaction. So it won't make any difference.But using Inbox/Outbox pattern will make some performance difference. Cause it will not require execution of all the operations in the same transaction. It will just send the messages to the outbox table and that's it. So it will not wait for the operation to complete.
But this case brings some other drawbacks like complexity and handling errors and retry mechanisms. ABP implements retry logic with Inbox/Outbox pattern, but still, you'll need to maintain and check the fails regularly. Some problems can't be eventually consistent. (Like you said, when database is not reachable, retry logic doesn't help until it is reachable again.)
-
0
Ok. So in my project i use LocalEventBus (for internal modules) and DistributedEventBus (for other api projects running with my main app). If i want to implement Inbox / Outbox pattern to my internal modules (with local events) i believe that is not possible with abp out of the box. Am i right about it? Inbox / Outbox pattern is only designed for distributed event bus?
If i didn't have any other api in my project i know that i could use DistributedEventBus. But that option no longer exists as i see it. I am aware with other complexities that it would bring if i deal with one dbcontext. But performance can be sth i can choose in that case. I believe i should implement that myself.
public virtual async Task CompleteAsync(CancellationToken cancellationToken = default) { if (_isRolledback) { return; } PreventMultipleComplete(); try { _isCompleting = true; await SaveChangesAsync(cancellationToken); DistributedEvents.AddRange(GetEventsRecords(DistributedEventWithPredicates)); LocalEvents.AddRange(GetEventsRecords(LocalEventWithPredicates)); while (LocalEvents.Any() || DistributedEvents.Any()) { if (LocalEvents.Any()) { var localEventsToBePublished = LocalEvents.OrderBy(e => e.EventOrder).ToArray(); LocalEventWithPredicates.Clear(); LocalEvents.Clear(); await UnitOfWorkEventPublisher.PublishLocalEventsAsync( localEventsToBePublished ); } if (DistributedEvents.Any()) { var distributedEventsToBePublished = DistributedEvents.OrderBy(e => e.EventOrder).ToArray(); DistributedEventWithPredicates.Clear(); DistributedEvents.Clear(); await UnitOfWorkEventPublisher.PublishDistributedEventsAsync( distributedEventsToBePublished ); } await SaveChangesAsync(cancellationToken); LocalEvents.AddRange(GetEventsRecords(LocalEventWithPredicates)); DistributedEvents.AddRange(GetEventsRecords(DistributedEventWithPredicates)); } await CommitTransactionsAsync(cancellationToken); IsCompleted = true; await OnCompletedAsync(); } catch (Exception ex) { _exception = ex; throw; } }
so i should implement my own logic for local events here instead so i shouldn't publish the events maybe bg job can publish it after the transaction complete accordingly? That is going to be difficult for each module since each module needs to have the same code duplication for inbox / outbox pattern.
-
0
If i want to implement Inbox / Outbox pattern to my internal modules (with local events) i believe that is not possible with abp out of the box. Am i right about it?
Yes, you are correct LocalEventBus do not support the Inbox/Outbox pattern for local events by default. This pattern is indeed designed for distributed event scenarios.
If you want to implement the Inbox/Outbox pattern for internal modules using LocalEventBus, you will need to create a custom implementation. You may inherit and override LocalEventBus according your needs or you may create a new provider by implementing DistributedEventBusBase in a new class.
By the way, if you don't configure any distributed event bus provider,
IDistributedEventBus
works in memory like local event bus. So you can use this interface without configuring any provider such as rabbitmq or kafka.But still it does not use outbox pattren by default, you see below it doesn't do anything with
useOutbox
parameter https://github.com/abpframework/abp/blob/69fde715c730b5796d32f7622ac7dc7782b6d320/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs#L138-L148You can manually override one of them if you really need to use outbox/inbox with local events.
But as a recommendation;
- I can suggest using RabbitMQ with ABP's default inbox/outbox implementation -- or --
- I can suggest going with LocalEventBus and investigate performance, if a bottleneck detected, than you can take an action to switch other solutions
-
0
ok thank you for explanation. I think that abp is easing so many things but at the same time it becomes very difficult for simple implementations. Maybe i should just create my own implementation for domain events instead of using local event bus. Anyway i will think about it. IDistributedEventBus is not the solution for me since i also use rabbitmq for my distributed messaging. I am closing this discussion over here. Thanks anyway.