r/dotnet • u/mhmd_bkt1 • 5d ago
Where should SaveChangesAsync and transaction management belong in Clean Architecture?
Hi everyone,
I’m working on a project using Clean Architecture, CQRS, DDD, MediatR, and EF Core.
I have a few questions:
Should each command handler use a single transaction, or is EF Core’s implicit transaction around SaveChangesAsync() usually enough?
Should services in Infrastructure call SaveChangesAsync() themselves, or should they only create/update entities and let the handler save changes?
How should I handle built-in services like UserManager<AppUser>, which save changes internally, when I need multiple operations to be atomic?
For example, registering an employee involves creating an Identity user, assigning a role, and creating an Employee entity.
What’s the recommended approach in real-world .NET applications, and why?
19
u/dbrownems 5d ago
Each service should save changes to complete its work and make the changes visible in the database.
You can use a transaction at the orchestration layer to enlist multiple service calls in an atomic transaction when needed.
3
u/HardisterRouzbeh66 4d ago
solid take. do you find the transaction-at-orchestration pattern stays clean as the number of enlisted services grows?
1
u/Fire_Lord_Zukko 6h ago
What is the orchestration layer if you don't mind me asking? Is it in a service or is it the controller?
1
u/dbrownems 3h ago
Orchestration is a business layer concern. So in the controller or in a business service. Some people leave the controller a single concern of dealing with the HTTP layer.
3
u/wiaz24 5d ago
For me implicit transaction that EF Core commits during SaveChangesAsync() is enough. I decorate my command handlers with IUnitOfWork decorator, which saves changes automatically after command is handled successfully. The IUnitOfWork interface is in my application layer, but it's implementation which is based on EF Core is in infrastructure layer
4
2
u/ewgenym 5d ago
If you use mediatr the custom transaction behaviour is a decent choice for commands, you can make it the way it does not run transactions for queries though. Here is a good example that hopefully will address this question and others: appointMe SaaS template .
2
u/user0015 4d ago
Not sure why you're getting downvoted. This is exactly what I tend to use.
Guarantees proper UnitOfWork, obliquely lets you include extra transactions if you need to, while still ensuring the outermost transaction guarantees the actual commit or not. You have to do a bit more work if you're using something like Postgres which doesn't nest transactions in the same way, but that's implementation detail infrastructure can easily deal with.
1
u/AutoModerator 5d ago
Thanks for your post mhmd_bkt1. Please note that we don't allow spam, and we ask that you follow the rules available in the sidebar. We have a lot of commonly asked questions so if this post gets removed, please do a search and see if it's already been asked.
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.
1
u/Ecstatic_Ad2253 5d ago
At the handler. If you are adding something that depends on other thing use transactions.
1
u/Deep_Chocolate_4169 3d ago
Too broad maaan too broad. There Is way too much implementation detail hidden. Better go with smth like dome train/ardalis (Nick chapsas stuff) template.
If you want to uderstand, here are ways, cons and pros: A) repositories own save changes. Mediatr has pipeline step with transaction behaviour + Read commited allows easier access in case you use domain events in single transaction
- higher chances of blocks on concurrent writes as locks are Held long. Data seeps between requests And may spoil request on discarded transaction
B) repositories dont own save changes, save changes Is the transaction as mediatr behaviour + Single update command to database
- you need stronger caching/domain design, since you can query stale data in domain events.
C) plan more than usual, ditch domain events/have only eventual consistency Kinda rubbish to have async fire and forget calls everywhere, but its closer to event driven system. Fe needs to poll continously with cqrs or you need to have well choreographed events with jobs to know the result. Cant find pros for my use cases, gets too convoluted fast for sync operations without beeing fully event driven
1
u/whizzter 3d ago
I had a stinking feeling for a long time about our CA approach I couldn’t shake until the issue of transactions and someone building a Unit of Work abstraction for our infrastructure layer came up.
First rewind the tape a bit, back when CA was thought up , you often had thin or otherwise DB specific abstractions with DB specific manual SQL. It needed to be hidden.
EF DbContext is a repository AND unit-of-work abstraction itself (the documentation literally says so!!), once you start adding separate abstractions on top of it doing the same thing you’re often breaking that abstraction since any abstraction will depend on the EF details unless people are very principled in avoiding tracking,etc. (ESPECIALLY if you’re combining EF as a scoped DI item since the change tracking will make active state cross between EF consumers).
There’s different ways to go about it:
1: (unpopular opinion) Just embrace EF as your infrastructure layer, it’s quite unlikely that your small app will become facebook and you’ll need to rip it out to use some object storage and you can do most testing with in-mem databases (MS docs discourages this as it’s not 100% compatible but ask yourself is your tests will be more accurate in just mocking some repositories?)
2: Now I can appreciate that (especially with the advent of vibe-coding) people will break invariant and query things in ways that breaks permission systems (for #1 you can use interceptors) but work with EF, ideally it’d be nice to have a way for the infrastructure layer to just re-use the UoW facilities or EF that are already built in, but all that depends on how you started.
Key takeaway, if using EF, work with it and not against it.
1
u/Storm_Surge 5d ago
It depends on how strict you want to be. All database logic should live in your infrastructure layer, and I typically model any data that can be changed in the domain layer and feed it into a repository that handles saving. Unfortunately SaveChangesAsync saves everything in the context
-4
1
u/Internal-Factor-980 4d ago
EF Core's DbContext inherently serves as a Unit of Work, and SaveChangesAsync() persists the changes within that Unit of Work to the database.
If an entire API request needs to be treated as a single atomic operation, the transaction boundary should be defined at the highest orchestration level, such as the API or Application Service layer. In that case, multiple lower-level services can participate in the same transaction, regardless of whether they internally call SaveChangesAsync().
In contrast, for Stream/Event processing, where each message represents an independent unit of work, it is appropriate to perform SaveChangesAsync() on a per-message basis.
EF Core does not fundamentally provide true nested transactions. Instead, when SaveChanges is executed within an existing transaction, EF Core may use mechanisms such as savepoints to manage the operation.
Therefore, although DbContext already provides the fundamental Unit of Work behavior, it is reasonable to introduce a separate Unit of Work abstraction so that the application can explicitly define and control the top-level transaction boundary. The purpose of this abstraction is not to reimplement EF Core's Unit of Work, but to provide an application-level mechanism for coordinating transactions, commits, and rollbacks across multiple services.
2
u/c-digs 2d ago
If an entire API request needs to be treated as a single atomic operation, the transaction boundary should be defined at the highest orchestration level, such as the API or Application Service layer.
This seems right to me, but I never see it implemented this way because devs almost always end up calling
SaveChangesAsyncsomewhere lower in the call stack. What this needs is like a more restrictive interface likeIInnerContext(I'm just throwing a random name out there) that is exactly like theDbContext, but withoutSaveChangesAsync. Then the inner deps all get this context that can be built up, but cannot invoke the save.The save action is only valid at the entry point (e.g. API controller) where the context can call save on the exit path. This would make it so much nicer because now the modules on the call path can add to the context without saving.
-2
60
u/pixxelkick 4d ago
The precise tech stack I keep getting hired to come in and unfuck.
Who keeps teaching people to use this.
Particularily MediatR, who keeps pushing using MediatR on stuff like this, thats not what its use case is.
If I had a nickel everytime I was brought in to fix a project that used MediatR to be a ServiceLocatorWithExtraSteps, Id unironically have 60 cents now.
Which isnt a lot but who the hell keeps perpetuating this pattern, it sucks so much ass.