-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Slimmer IOptions<T> #49852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Slimmer IOptions<T> #49852
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,22 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Licensed to the .NET Foundation under one or more agreements. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The .NET Foundation licenses this file to you under the MIT license. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Diagnostics.CodeAnalysis; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace Microsoft.Extensions.Options | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal class UnnamedOptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IOptions<TOptions> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where TOptions : class | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly Lazy<TOptions> _lazy; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public UnnamedOptionsManager(IOptionsFactory<TOptions> factory) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _lazy = new Lazy<TOptions>(() => factory.Create(Options.DefaultName)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public TOptions Value => _lazy.Value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks simple enough. Can you remind me what costs this is trying to address?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation is shared between "named" and "unnamed" Options. When using "named" options, we need to use a cache (ConcurrentDictionary):
In the simple case where you never use a named option, we still create the dictionary, and check it to get the value. Here, when you never use a named option, we can simply use a Lazy, saving the dictionary alloc and access.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is why I was asking though. If the goal is to make this cheaper when it's never used, and we're going to be creating a lot of these so that it matters, there are cheaper ways. Lazy still allocates, and in the current usage, there's not only the allocation for the Lazy itself, there's the internal LazyHelper that'll be allocated to serve as the lock object for the double-checked locking it employs by default, and there's the closure allocation to capture the factory, and there's the delegate allocation for the method on the closure. If it's never accessed, yeah, it's a tad cheaper, but not as much as you might think (and if it is accessed, you've basically doubled the allocation cost). [Benchmark]
public object CreateLazy()
{
return CreateLazyWithClosure(s => new ConcurrentDictionary<string, string>(1, 0));
static Lazy<T> CreateLazyWithClosure<T>(Func<string, T> factory) =>
new Lazy<T>(() => factory(""));
}
[Benchmark]
public object CreateDict() => new ConcurrentDictionary<string, string>(1, 0);
[Benchmark]
public object CreateLazyAndAccess() => ((Lazy<ConcurrentDictionary<string, string>>)CreateLazy()).Value;
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it better to use LazyInitializer and keep the state in the implementation?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I re-read your statement, I thought you were saying the ConcurrentDictionary was cheaper. You're just saying they are cheaper ways to avoid the lazy overhead. I thought about it a little but figured I'd let you comment and tell me what the optimal way was 😉
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really hope people aren't doing this, but I gotta ask: do we know if people get
IOptions<T>, and then cast/as it either toOptionsManager<T>orIOptionsSnapshot<T>?I see
OptionsManager<>is public, which made me think of this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like this:
https://github.com/KevinDockx/HttpCacheHeaders/blob/7c7f0fe94b70e4273f0a8011c8daf78adb220c13/test/Marvin.Cache.Headers.Test/Extensions/ServiceExtensionFacts.cs#L75-L81
I know this is test code, but I can imagine other doing this unnatural thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an acceptable breaking change IMO. It's unfortunate that OptionsManager is public but I don't think it's reasonable to tie our hands as a result. This is one of the benefits of using DI (swapping the implementation).
Good find though!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a query and found some packages using OptionsManager directly but none were casting the default implementation.