Тёмный

Unit of Work Is Even Better With MediatR + TransactionScope 

Milan Jovanović
Подписаться 102 тыс.
Просмотров 29 тыс.
50% 1

Опубликовано:

 

30 сен 2024

Поделиться:

Ссылка:

Скачать:

Готовим ссылку...

Добавить в:

Мой плейлист
Посмотреть позже
Комментарии : 127   
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Want to master Clean Architecture? Go here: bit.ly/3PupkOJ Want to unlock Modular Monoliths? Go here: bit.ly/3SXlzSt P.S. I missed a _minor_ thing in the video - passing *TransactionScopeAsyncFlowOption.Enabled* to the TransactionScope constructor. This is necessary to be able to use it with async/await.
@SuperLabreu
@SuperLabreu Год назад
quick question: is transactionscope's default isolation level the same as the one used by ef core implicit transactions? it's different from sql server's default level....
@krzysztofhandzlik9273
@krzysztofhandzlik9273 Год назад
Exatcly as mentioned, this has a "magic" smell and persisting changes to database is implicit. Same with transaction, what if i don't want all changes to be packed into one transaction. Depends on team, imho code readability is much more important than DRY. Thanks for video :)
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Why in the world would you want to have multiple transactions in one API call though?
@krzysztofhandzlik9273
@krzysztofhandzlik9273 Год назад
@@MilanJovanovicTech Performance reason.
@Sclark2006
@Sclark2006 Год назад
​@@krzysztofhandzlik9273can you show an example of being more performant with multiple transactions? Because that supposed performance breaks the concept of transaction. If you are going to talk about massive updates or deletes, then you should consider a different architecture, like message queues and worker services.
@krzysztofhandzlik9273
@krzysztofhandzlik9273 Год назад
@@Sclark2006 "you should consider a different architecture" i do but nobody wants to pay for it :D
@evaldas5059
@evaldas5059 Год назад
I’m glad to hear that you haven’t encountered issues related with high scale applications yet, but I would strongly suggest to avoid having multiple SaveChanges() in single “transaction” as you call it, however in that case technically it’s no longer a transaction
@pilotboba
@pilotboba Год назад
I will say that the EF Core team highly discourages the use of TransactionScope. :)
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Source? I was trying to find something without luck
@timur2887
@timur2887 5 месяцев назад
@@MilanJovanovicTech implicit transactions are faster than excplicit.
@thomasjespersen
@thomasjespersen Год назад
I love this approach, and I apply the same principles. Here are a few comments/suggestions: 1. You can use a generic constraint to run this exclusively for commands. Here's how you can do it: public sealed class UnitOfWorkPipelineBehavior : IPipelineBehavior where TRequest : ICommand where TResponse : IResult 2. In other videos, you advise against throwing exceptions, suggesting to return the result instead. In such a case, you would need to commit changes only if the result is successful. I do it like this public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { var response = await next(); if (response is IResult {IsSuccess: true}) { await _unitOfWork.CommitAsync(cancellationToken); } return response; } 3. If you're dealing with nested commands and foreign keys, you risk invoking SaveChanges() in "reverse" order, where a row with the foreign key is saved before the row with the primary key is inserted (even if you use TransactionScope). In this situation, you would want to call SaveChanges() only in the outer UnitOfWork pipeline behavior. I've created a simple solution to this issue by using a scoped ConcurrentCounter (see link below) 4. If you are using SQLite for testing, you should be aware that TransactionScope is not an option as it uses distributed transactions, which are currently unsupported. Once again, I use my ConcurrentCounter to ensure I only call SaveChanges once. SQLite's InMemory database is superior to Entity Framework because it is a real relational database, and it understands foreign key constraints. 5. You could consider disabling EntityFramework change tracker. Since you are calling repository.Update() you do not not need to have change tracker enabled at all. If you disable it you also do not risk saving tracked entities unintentionally. Also, this is faster if you use EntityFramework for querying. 6. For generating numerical ID, I'm using "Snowflake Transaction IDs" which are generated IDs code (like Guids), but have the benefits of being chronological across servers in a web farm (so you don't need to sort rows in your database), smaller and IMO more aesthetically pleasing - GUIDs are ugly :). You see my solution to all this here: github.com/PlatformPlatform/platformplatform/blob/main/shared-kernel/ApplicationCore/Behaviors/UnitOfWorkPipelineBehavior.cs
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Absolutely enjoyed the few hours I spent going through your repository. I might need to make something like this myself. 😁 I agree with all of your points. Except the issue with counting save changes calls. To me, one use case is one transaction. Any side effects I prefer pushing into domain events, which are processed asynchronously. 6. Have you used ULIDs?
@salomon1471
@salomon1471 Год назад
How aboutwhen you use the _dbcontext to handle concurrency? And in the few cases you would like to raise it to serializeable to avoid phantom reads?
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Introduce some sort of flag to set the isolation level?
@arunnair7584
@arunnair7584 Год назад
EF Core's DBContext obviates the need for UnitOfWork and Repositories. If you see examples given by Microsoft, they implement UnitOfWork, just as a wrapper around DBContext. This is because UnitOfWork is executed inside the SaveChanges method. I can't find the exact link where Microsoft states this, but I am sure reading about it.
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Would be great if you can share the article
@jezielmoura4795
@jezielmoura4795 Год назад
Great vídeo, but i believe that it cannot be used on very scenarios. A command can use others services for persistence on same application that use ef core, like other api, file system, storage system... Is commands, but not depend of ef core and this behavior is applied on all commands. I like that you share very possibilities and i learning a lot of new things, but is very important the viewers understand that some things only can applied especified scenarios. Thanks for share spectacular contents.
@MilanJovanovicTech
@MilanJovanovicTech Год назад
No database transaction will save you from an external system failure. That wasn't the point of the video at all.
@Kasiux
@Kasiux 6 месяцев назад
Why are we not using a PostProcessor from MediatR when we are just doing something after invoking next()?
@MilanJovanovicTech
@MilanJovanovicTech 6 месяцев назад
I prefer the full pipeline behaviors, much more flexible. For this particular example, how would you commit the transaction?
@Kasiux
@Kasiux 6 месяцев назад
@@MilanJovanovicTech Wrapping the execution of the next delegate is something that could not be done with PostProcessors, but the first examples you should could have been. But I'd rather not use a middleware for that.
@Tamer_Ali
@Tamer_Ali Год назад
Thanks Milan for your video. I asked about HiLo strategy long time ago. I hope you talk about it in one of your upcoming videos
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Sure! I can cover it in a quick video 😁
@estebangarzonguerrero8578
@estebangarzonguerrero8578 9 месяцев назад
I love your videos Milan, I learn many concepts from you, and the best thing is that you simplify the ideas when you explain a topic
@MilanJovanovicTech
@MilanJovanovicTech 9 месяцев назад
Glad you like them!
@ugurkzlkaya5688
@ugurkzlkaya5688 Год назад
Managing all transactions with mediatr pipelines is correct way in applications? If called another command in command handler, this pipeline works two times(I solved with correlationId, but i am not sure). Moreover, dbcontex object is not thread safe. It can be problem in multithread apps. Have you solved these problems before? I dont know how to deal. Thanks
@MilanJovanovicTech
@MilanJovanovicTech Год назад
I think two commands in a transaction is a design smell
@aliwa91
@aliwa91 Год назад
This approach can use for track user activity
@MilanJovanovicTech
@MilanJovanovicTech Год назад
How so?
@efimov90
@efimov90 Год назад
Can i have several transactiuon Scopes in memory? Will it work if i holding 2 unit of works in 2 window or tabs in my desktop application? For example: i have 1 tab with order and other with product. Will it work if i create scope for each tab?
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Yes, you can create as many as you want - how the queries inside will behave I have no idea
@verzivull
@verzivull 9 месяцев назад
you can mark handlers with dummy interface , so it could be easy to search late on friday, when prod gives critical error
@MilanJovanovicTech
@MilanJovanovicTech 9 месяцев назад
We don't do production errors on Fridays here
@eduardrivas6964
@eduardrivas6964 Год назад
Hi Milan, I hope you're great. I was checking out your channel and find out several playlists but there's no specific order to watch the videos. Is there any particular order you suggest? Not to get lost and confused, I love your way of explaining things but it's not really clear where to begin. At this point in my life, what I want to learn is how to implement good design principles, patterns and clean architecture, when do I need each scenario, etc, but I just can't figure out where to begin with all the info you've uploaded so far. It's a bit overwhelming. Do you have a roadmap or something? Thanks!
@MilanJovanovicTech
@MilanJovanovicTech Год назад
I tried to add to the playlists whenever I can. Your best bet is going in chronological order.
@LordErnie
@LordErnie Месяц назад
Wouldn't you say that double dispatch, in your given case, forces the upper layers to always have access to data persistence objects? I mean it works, but wouldn't you rather just write a wrapper or an abstraction to enforce those rules instead? It's nice to have it right there on the model, but it really stings a bit. Like a work-around for something that you know isn't correct but it works and doesn't violate anything so you leave it be. Rich domain models are nice, but isn't there a boundry to what we consider logic for an entity and logic for data persistance? Even if the resource itself is desynced (your in memory representation of the list isn't accurate, the state in your database is different), wouldn't you say that it would be better to just hook your entity to some event handler that would update the collection (and its contents if needed)? It involves more complexity but it removes the responsibility from the domain model, which shouldn't be bothered with the outside world. Any thoughts on this?
@MilanJovanovicTech
@MilanJovanovicTech Месяц назад
"but it really stings a bit" - it does, because the example I used here kinda sucks. My bad on that. Sometimes I try to hard to share some concept without coming up with a problem the requires it. Your analysis is pretty spot on.
@ramytawfik9168
@ramytawfik9168 Год назад
Question Please according to the latest approach as you have used the transaction scope in the UnitOfWorkBehaviour Now in the handler we need the Id of the Order firstly from the database and then pass it to the Order Summary So i think this approach does not meet this usecase we still do not persist the changes in the db to get the id from the db and pass it to the Order Summary ?!
@MilanJovanovicTech
@MilanJovanovicTech Год назад
The Id is generated in memory
@ferventurart
@ferventurart Месяц назад
How it does work with Transactional Outbox Pattern and how avoid the nested transactions error?
@MilanJovanovicTech
@MilanJovanovicTech Месяц назад
Works well with Outbox. But what do you mean by nested transactions?
@antonofka9018
@antonofka9018 Год назад
TransactionScope does not support asynchronous disposal. It's important to know that the implicit committing of the transaction will be done synchronously and block the thread. This issue is unlikely to get fixed any time soon.
@MilanJovanovicTech
@MilanJovanovicTech Год назад
I don't think that makes it bad to use, but something to be aware of for sure
@baebcbcae
@baebcbcae 5 месяцев назад
the TransactionScope is really strange here because EF does it by default if you call SaveChanges once... It would be more helpful if you showed how to organize code to call multiple mediatr commands/save changes in different variations within transactions to achieve atomic behavior
@MilanJovanovicTech
@MilanJovanovicTech 5 месяцев назад
Each command is a unit of work. Any time you need multiple calls to SaveChanges, you need an explicit transaction.
@malignantw2
@malignantw2 Год назад
Great stuff. But keep in mind that if you are testing your handlers directly without establishing mediator pipeline behaviours then you have to call UnitOfWork manually in those tests. That seems a little unintuitive.
@MilanJovanovicTech
@MilanJovanovicTech Год назад
That's a really good point there!
@MrSkoban
@MrSkoban Год назад
I really like this approach, I was wondering how can I solve this duplication problem until you came out with this video, thanks! P.S: Is there any possibility that you can make a future video or provide me some resources about querying nested entities in DDD from EF Core? I'm using repository patterns and "Include" and "ThenInclude" queries, but it would be awesome to know what is the approach like, how should I model my domains and how should I map my models 😃
@MilanJovanovicTech
@MilanJovanovicTech Год назад
I think Include/ThenInclude is the way to go 🤷‍♂️ Why reinvent the wheel?
@robertok4646
@robertok4646 Год назад
TransactionScope does not work with EnableRetryOnFailure. Another issue is what if you call command inside command and so on ... when you have logic spreaded through modules ? Then you can meet with nested transactions ...
@MilanJovanovicTech
@MilanJovanovicTech Год назад
If it's all part of the same ambient transaction - what would be the issue?
@robertok4646
@robertok4646 Год назад
@@MilanJovanovicTech I think the solution could be to create separate Command: TransactionCommand with the payload of source command then call mediator on that command and finally call ExecutionStrategy using TransactionScope. This way you can: 1. Use EnableRetryOnFailure 2. use Single Transaction. What do you think?
@pilotboba
@pilotboba Год назад
I also have a question. In the pipeline behavior, is there no need to pass parameters to the next() delegate like the cancelation token? Does the pipeline take care of the somehow?
@MilanJovanovicTech
@MilanJovanovicTech Год назад
The delegate encapsulates the arguments
@swozzares
@swozzares Год назад
I've been using "transaction = context.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted)", then .Commit() or .Rollback(). You can get the context via DI.
@antonofka9018
@antonofka9018 Год назад
This won't work with multiple db contexts. Though I'm not sure they would all pick up on the transaction scope either.
@MilanJovanovicTech
@MilanJovanovicTech Год назад
That's a good alternative.
@rodrigodearcayne
@rodrigodearcayne 6 месяцев назад
@@antonofka9018they would. We do that extensively.
@gonzalorf
@gonzalorf Год назад
The transactional components in the Application project don't seem appropriate. They should probably be placed in the infrastructure layer. Am I right?
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Debatable. If you're using a SQL database which has an inherent concept of transactions, why would you go out of your way to hide that?
@gonzalorf
@gonzalorf Год назад
@@MilanJovanovicTech Not trying to hide. Just thinking about who should be responsible for that concern.
@emanuelcordovamontiel1017
@emanuelcordovamontiel1017 4 месяца назад
Hi, Milan. Thank you for this video. Could you share the source code, please?
@MilanJovanovicTech
@MilanJovanovicTech 4 месяца назад
Will see
@Tamer_Ali
@Tamer_Ali 8 месяцев назад
what do you think about the following approach of Unit of Work? public class UnitOfWork : IUnitOfWork { private readonly DbContext _context; private Lazy _users; public UnitOfWork(DbContext context) { _context = context; _users = new Lazy(() => new UserRepository(_context)); } public IUserRepository Users => _users.Value; //.... } services.AddScoped();
@MilanJovanovicTech
@MilanJovanovicTech 8 месяцев назад
Don't like it because it's a carbon copy of a DbContext, but more complex. Not so fun for unit testing.
@Zelo246
@Zelo246 Год назад
Thanks for the great content! I appreciate your explanation in the videos How many more videos are left in this series? and do you know the percentage of completion for this series? because I've been studying this series in a short time
@MilanJovanovicTech
@MilanJovanovicTech Год назад
It's a never-ending series, so until I get bored... 😅 But I'm going to start moving to Microservices and distributed systems in the near term (after I launch my course)
@pilotboba
@pilotboba Год назад
Another sort of tenant of commands is that they have no return value. :) So, the endpoint can return 202 Accepted as long as no error occurred.
@MilanJovanovicTech
@MilanJovanovicTech Год назад
I like to break that rule on occasion, because it makes sense to return a result for some command
@pilotboba
@pilotboba Год назад
@@MilanJovanovicTech yep. Guess this is a great example of the flexibility of client generated keys. Hi lo like you mentuoned is fine. But we tend to go with guids now.
@timur2887
@timur2887 5 месяцев назад
Transaction sope might make its transactions a little bit longer than they might be, imo
@MilanJovanovicTech
@MilanJovanovicTech 5 месяцев назад
I've seen that happen even with an explicit transaction
@YasSin
@YasSin Год назад
Useful Milan.
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Glad you liked it!
@nove1398
@nove1398 Год назад
Interesting perspective on the behaviour for transaction scoping. Nice share
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Glad you think so!
@yohm31
@yohm31 Год назад
Using IDs from the database into your domain objects is a TERRIBLE idea 😱
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Why is that?
@foonlam7134
@foonlam7134 11 месяцев назад
If you are creating a new project then you can implement what you want but if you are using a legacy database then you will have to deal with it.
@yohm31
@yohm31 11 месяцев назад
@@MilanJovanovicTech because ypu you create a strong dependency between the business logic and a particular database implementation). Plus: database IDs does not migrate easily.
@rodrigodearcayne
@rodrigodearcayne 6 месяцев назад
@@yohm31true, but on the other hand if you have a table where you expect to insert a lot of rows you should consider integer PK as GUIDs would result in tremendous rearrangement of data pages on bulk inserts and that would have a negative impact on performance.
@timur2887
@timur2887 5 месяцев назад
@@rodrigodearcayne to avoid this you should generate only sequential guids, not just Guid.NewGuid()
@sandragredo4673
@sandragredo4673 Год назад
Daj neki link za ucenje? Odakle krenuti?
@MilanJovanovicTech
@MilanJovanovicTech Год назад
github.com/kgrzybek/modular-monolith-with-ddd
@estebangarzonguerrero8578
@estebangarzonguerrero8578 9 месяцев назад
I have trouble when I try to persist the data (my saveChangesAsync doesn't work)... how can I access your repository code? If it has a cost, we can talk in private, thanks for your time Milan J!!
@MilanJovanovicTech
@MilanJovanovicTech 9 месяцев назад
Check my Patreon - you can access the source code for all videos
@iliyan-kulishev
@iliyan-kulishev 4 месяца назад
Wonderful video, esp. the part with TransactionScope. Before adding that we had not only the problem of having to call SaveChanges in a handler, but also the possibility of calling SaveChanges in the repositories by mistake, carelessness etc, since the DbContext is injected into them.
@MilanJovanovicTech
@MilanJovanovicTech 4 месяца назад
I rarely found it to be a problem in practice (multiple SaveChanges calls) since teams are usually mindful, but this is a s simple way to move that responsibility elsewhere
@NavneetKumar-fs2mc
@NavneetKumar-fs2mc 10 месяцев назад
Is this good idea to use EF for post calls and Dapper for get calls in a performance centric big application? Unit of Work With MediatR + TransactionScope even if across project ask is not command as in your example? In my project in few cases it is definably needed but not everywhere as your example.
@MilanJovanovicTech
@MilanJovanovicTech 10 месяцев назад
I've done something similar in the past, works fine
@DavidSmith-ef4eh
@DavidSmith-ef4eh Месяц назад
Maybe golang is not that bad... I guess I can get used to the if(err != nil)... too much abstraction in c# if you ask me.
@MilanJovanovicTech
@MilanJovanovicTech Месяц назад
Go has some nice concepts
@DavidSmith-ef4eh
@DavidSmith-ef4eh Месяц назад
​@@MilanJovanovicTech it's simple. When I see what people do in c#, I am not sure if its the right approach. That is abstracting everything away and hiding it in a magic reflection based wire up system.
@MilanJovanovicTech
@MilanJovanovicTech Месяц назад
@@DavidSmith-ef4eh The advantage of having a language feature... It's not quite the same.
@DavidSmith-ef4eh
@DavidSmith-ef4eh Месяц назад
@@MilanJovanovicTech true, nobody forces people to abuse them.
@keithjairam8452
@keithjairam8452 Год назад
really great content. Keep it up.
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Thanks Keith!
@microtech2448
@microtech2448 Год назад
What's wrong with calling SaveChangesAsync explicitly within add/update repository methods except some extra line of code?
@MilanJovanovicTech
@MilanJovanovicTech Год назад
You'd be going to the database multiple times in one HTTP request
@microtech2448
@microtech2448 Год назад
Alright, but we already make multiple DB calls when it comes to fetching data like customer detail, order details etc before saving anything in the DB. I think we can't reduce those query calls but to save a couple of save calls, we are using unitofwork?
@fredericmerouze4876
@fredericmerouze4876 Год назад
great video milan
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Thanks a lot, Frederic! :)
@krislesage7884
@krislesage7884 7 месяцев назад
I would love to see the same example with Marten 🙂
@MilanJovanovicTech
@MilanJovanovicTech 7 месяцев назад
It can be pretty much the same, since Marten uses PostgreSQL
@pouriyababaali7040
@pouriyababaali7040 10 месяцев назад
i think this break DDD rules that a one transcation belong a one aggergate and couldnt change other aggergate ! but i think you didnt need in this video to consider it !
@MilanJovanovicTech
@MilanJovanovicTech 10 месяцев назад
How so?
@pouriyababaali7040
@pouriyababaali7040 10 месяцев назад
@@MilanJovanovicTech in ddd concept each aggregate represents a single transaction boundary, with this approach you affect 3 aggrrgate root in 3 diffrent transaction and thats not true i think ! Actually why you need to keep track of domain event that not created for this approch I just say my ideas base on ddd concepts in one of my projects
@kodindoyannick5328
@kodindoyannick5328 7 месяцев назад
Thanks Milan for this great content.
@MilanJovanovicTech
@MilanJovanovicTech 7 месяцев назад
You're welcome!
@LeoJFitz
@LeoJFitz 7 месяцев назад
Why don't we still call the author Milan Overengineerović?
@MilanJovanovicTech
@MilanJovanovicTech 7 месяцев назад
That's funny, anon
@lucek15951
@lucek15951 Год назад
This is so good! When I saw the title I was aware what will be in the video, but was never thinking about that myself. You are extending my thinking so much! Thanks!
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Awesome, Kamil! I'm glad you enjoyed the video! 😁
@MAHDIHESARI
@MAHDIHESARI Год назад
Is it always recommended to use MediatR?
@MilanJovanovicTech
@MilanJovanovicTech Год назад
Only if you want to use it
@MAHDIHESARI
@MAHDIHESARI Год назад
@@MilanJovanovicTech I mean what are the cons?
@antonofka9018
@antonofka9018 Год назад
​@@MAHDIHESARI Subjectively cleaner controllers, also the pipeline feature is pretty useful.
@timur2887
@timur2887 5 месяцев назад
@@antonofka9018 looks like pipelines may affect on overall application`s performance...
@Hunter-1984-X
@Hunter-1984-X 10 месяцев назад
Is better: class UnitOfWork : IDisposable { private MyDbContext _dbContext; private TransactionScope _transaction; public UnitOfWork() { _dbContext = new MyDbContext(); _transaction = new TransactionScope(); } public void Complete() { _dbContext.SaveChanges(); _transaction.Complete(); } public void Dispose() { _dbContext.Dispose(); _transaction.Dispose(); } }
@MilanJovanovicTech
@MilanJovanovicTech 10 месяцев назад
Why?
@rodrigodearcayne
@rodrigodearcayne 6 месяцев назад
@@MilanJovanovicTechbecause this way the unit-of-work behavior is implemented in the UnitOfWork class
Далее
The Unit of Work Design Pattern Explained
12:37
Просмотров 24 тыс.
Don't Use Polly in .NET Directly. Use this instead!
14:58
Microservices with Databases can be challenging...
20:52
Why use Type and not Interface in TypeScript
14:12
Просмотров 208 тыс.
how Google writes gorgeous C++
7:40
Просмотров 890 тыс.