Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static IServiceCollection AddOptions(this IServiceCollection services)
throw new ArgumentNullException(nameof(services));
}

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
Copy link
Member

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 to OptionsManager<T> or IOptionsSnapshot<T>?

I see OptionsManager<> is public, which made me think of this.

Copy link
Member

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

        private static void ValidateServiceOptions<T>(TestServer testServer, Func<OptionsManager<T>, bool> validOptions) where T : class, new()
        {
            var options = testServer.Host.Services.GetService(typeof(IOptions<T>));
            Assert.NotNull(options);
            var manager = (OptionsManager<T>)options;
            Assert.True(validOptions(manager));
        }

I know this is test code, but I can imagine other doing this unnatural thing.

Copy link
Member Author

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!

Copy link
Member Author

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.

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
Expand Down
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;
}
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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):

/// <summary>
/// The default configured <typeparamref name="TOptions"/> instance, equivalent to Get(Options.DefaultName).
/// </summary>
public TOptions Value => Get(Options.DefaultName);
/// <summary>
/// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
/// </summary>
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
if (!_cache.TryGetValue(name, out TOptions options))
{
// Store the options in our instance cache. Avoid closure on fast path by storing state into scoped locals.
IOptionsFactory<TOptions> localFactory = _factory;
string localName = name;
options = _cache.GetOrAdd(name, () => localFactory.Create(localName));
}
return options;

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saving the dictionary alloc

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;
Method Mean Allocated
CreateLazy 23.00 ns 160 B
CreateDict 36.82 ns 192 B
CreateLazyAndAccess 82.91 ns 352 B

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}