Explaining default Singleton lifetime and the use of ValueTask
#210
Replies: 2 comments 3 replies
-
|
The first thing I’d like to say is that I really appreciate the work you’ve done. I hope you won’t take this discussion as a sign of disrespect or ingratitude. I find your arguments reasonable. However, I can't help but point out that ValueTask comes with certain limitations. Here are a few examples from the documentation where the behavior is undefined:
Additionally, I’d like to note that if you're using (or used to use with MediatR) Just an idea. Maybe we should consider having two types of handlers: I agree that you can do a thing Singleton-friendly, and |
Beta Was this translation helpful? Give feedback.
-
|
I don't think it's necessary to be scoped. If you really need to open a scope to control service lifetimes, our little friend IServiceScopeFactory is always there to lend a hand. My slight disagreement is with ValueTask being used in virtually all instances. Task overhead is typically dwarfed SUBSTANTIALLY by other concerns in most real world code. I believe in many cases this will actually force people to introduce even more allocation than normal in an attempt to use .AsTask on the return, possibly offsetting the performance you want in a majority of systems. I would normally discourage such a micro-optimization. I guess if you drop it under the policy of "library doesn't fit the needs of those systems" in those instances it's perfectly rational. Those who are willing to adopt the design it naturally produces would end up with better performing code. Given that, I think it's worth linking information on those differences as part of the documentation I think. Still, I applaud the effort to improve the adoption of better design decisions, as well as the transparency in your thoughts on the matter. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
This library uses
Singletonlifetime configuration by default, and usesValueTaskas part of the abstractions as opposed toTask.These decisions were performance-related and followed from a philosophy of "pay for what you use" and non-pessimization:
So in Mediator I made it a goal to make any slower configuration/implementation opt-in. If you just set up an application with a simple
AddMediator()call and implement a handler which does no IO or async stuff - it will have as close to zero performance overhead as I could manage. If I had made the default lifetimeTransienthowever, an instance of the handler would be allocated for every message sent for no reason at all. If I had madeTaskthe return type in the abstractions, the handler would have to allocate aTaskfor no reason at all. This is obviously not a very useful application that I'm using as an example, but I've seen plenty of real world applications that both work fine withSingletonand have sync paths in various message handlers.Over the years there have been questions and comments surrounding this, e.g.
and
And my answer here would still be that
Handle-method and doing the IO in an async local/private method.Singleton-friendly. Below are some examples:DbContext->DbContextFactoryHttpClient->IHttpClientFactory(if you useAddHttpClient) or just have a static clientIHttpContextAccessorIn other words - if you chose
Scopedbecause you happen to consume scoped services for no specific reason, your app is going to allocate lots of memory that is completely pointless (i.e. it is pessimized).If I'm developing a small app I likely don't care (locally) about a little pessimization. But developing larger scale systems and thinking as a library developer as is my capacity in this repo, I do think it is worthwhile to consider these things and advocate for efficient resource utilization.
Beta Was this translation helpful? Give feedback.
All reactions