-
Notifications
You must be signed in to change notification settings - Fork 67
Options 2.0 Iteration 3 - Breaking change edition: Takeover IOptions #179
Conversation
Semantics of name: |
Oh, wow, breaking changes.... |
/// Used to cache TOptions instances. | ||
/// </summary> | ||
/// <typeparam name="TOptions">The type of options being requested.</typeparam> | ||
public interface IOptionsCache<TOptions> where TOptions : class |
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.
Why is this public? Where would you ever use 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.
People were asking for this to be exposed so they can plug in their own caching logic, i.e. using the memory cache to store options and control their own eviction policies, #167
Our default cache will at least have to support the existing reload on change behavior that will refresh any options that are bound to config. I'll be taking a look at that next.
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 think that should be fairly straightforward, and is just hooking up the config OnReload changeTokens to call Remove on the IOptionsCache
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.
The hope is that with this new factoring with IOptionsCache, we can obsolete/delete IOptionsMonitor/IOptionsSnapshot, to only have one IOptions to rule them all. (with IOptionsFactory + potentially in a later PR IOptionsValidator as ringwraiths)
Assuming we can make breaking changes like this, this would eliminate the issues around having both old and new interfaces and trying to explain what service works with which interface that the previous iteration had... |
@@ -7,11 +7,20 @@ namespace Microsoft.Extensions.Options | |||
/// Used to retreive configured TOptions instances. | |||
/// </summary> | |||
/// <typeparam name="TOptions">The type of options being requested.</typeparam> | |||
public interface IOptions<out TOptions> where TOptions : class, new() | |||
public interface IOptions<TOptions> where TOptions : class, new() |
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 imagine that we might have a place that requires to have not only the options value, but also the API to operate with options (Get, Add, Remove etc). I also agree that this interface is backward-compatible.
But I think that this interface violates with Interface Segregation principle. If I understand it right, methods (Get, Add, Remove) are required for Auth stuff. But for just retrieving options values isn't required.
Maybe create separated interface IOptionsService
with methods (Get, Add, Remove). For basic use-cases inject it in IOptions
implementation. For Auth - inject the service itself and operate with options.
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.
Well before, the reason Add was on the main interface was because it needed to do validation using the validator before inserting into the cache. Now that validation is no longer part of this PR, consumers can directly insert what they want into the cache (using the options factory to create it), so for now, its probably fine dropping Add/Remove
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.
Do you think that having Get(name)
method in end-user interface is fine?
Should options consumer decide which options is needed? Or DI engine should have an ability and all required information for choosing the right named options ? (and consumer will just consume)
@@ -7,11 +7,20 @@ namespace Microsoft.Extensions.Options | |||
/// Used to retreive configured TOptions instances. | |||
/// </summary> | |||
/// <typeparam name="TOptions">The type of options being requested.</typeparam> | |||
public interface IOptions<out TOptions> where TOptions : class, new() | |||
public interface IOptions<TOptions> where TOptions : class, new() |
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.
public interface IOptions where TOptions : class, new() [](start = 4, length = 65)
Have we considered having INamedOptions extend from IOptions and adds the Get method? With the removal of Add and Remove methods, that would mean we would not break current implementors of IOptions while still adding new functionality in 2.0 for those that implement INamedOptions. It also should not result in two parallel sets of APIs, like the previous PR had. And it seems like the two interfaces and their relationship are a reasonable separation of concerns that we might make even if designing this from scratch.
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.
That's closer to the previous iterations where we had a new Options 2.0 interface basically. I definitely prefer not breaking ideally, since already this is a PITA fixing the options usages all over the place. So I thought the concern around potential confusion was related to the usage of the new OptionsFactory/OptionsCache, if we normalizing both IOptions + IOptionsGet to use the new implementation, that sounds good to me as well.
Should we consider a more generic name than INamedOptions since we potentially are looking into adding the validation stuff back soon, I'd hate to rename it right away...
My preference would be to name it something that implies additional logic is being applied on top of IOptions, i.e. IOptionsManager/IOptionsService.
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.
@HaoK IOptions<out T>
would remain what it is and INamedOptions<out T>: IOptions<T>
would be used to consume named options. If we add validation again, we should add it to both. Then I don't see the advantage of having a new name that implies anything different.
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 don't have a problem with splitting Get into its own interface, I just really don't like INamedOptions (which I already used in a previous iteration, it did not grow on me at all...)
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.
Also are we still trying to eliminate IOptionsSnapshot/IOptionsMonitor assuming there are sane workarounds which can be implemented via IOptionsFactory/Cache?
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.
So the behavior difference between
IOptions
andIOptionsSnapshot
is that IOptions will never change, its a cached singleton that never gets updated.
So IOptions
might have been a singleton, and aTOptions
once returned will never be mutated (by the Options feature at least) but but a new TOptions
instance with new values will be returned after the cache is invalidated. Is this correct? If yes, I agree using a method is more appropriate than the Value
property. Not sure that determines that the method should always take a name (seems to be an orthogonal question).
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.
Yup, the behavior of the new IOptions.Value implementation that is backed by the IOptionsCache is fully dependent on what's in the cache.
The names stuff is more or less orthogonal, as currently .Value
is just returning .Get(string.Empty)
name
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.
still think maybe we should consider just adding Get(name) to IOptions, but not break IConfigureOptions as even though we are changing the IOptions interface), there's not many implementations of IOptions today, while implementing your own IConfigureOptions was a major extensibility point which we should avoid breaking
Ah, so you are actually proposing keeping the Value
property and adding the method alongside it, correct? Otherwise we would break all consumers of IOptions
. I don't like it being a property if it will potentially return a different instance sometimes when invoked, but it may be just me.
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.
Yeah well that's why I was initially proposing we leave IOptions alone entirely and keep the old semantics, and introduce IOptionsManager with the new semantics and no .Value.
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.
If we wanted to be crystal clear about what's old and new, we could leave Options alone entirely, and add all the new stuff in a new Options.Manager package, which would reuse only a few things like IConfigureOptions/Configure
Do we need this anymore? Seems like OptionsService can handle this. If we do IOptions and INamedOptions, then maybe this implements IOptions and OptionsService implements INamedOptions. (And we can discuss naming. :-) ) Refers to: src/Microsoft.Extensions.Options/OptionsWrapper.cs:10 in 3cdfe99. [](commit_id = 3cdfe99, deletion_comment = False) |
return services; | ||
} | ||
|
||
public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class |
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.
ConfigureAll [](start = 41, length = 12)
What does ConfigurAll do? Also, looks like it will result in null ref exception as currently implemented.
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.
It should just result in a new instance of ConfigureOptions { Name = null, action = action }
. Which then eliminates the name check in Configure via (if Name == null || name == Name)
@@ -37,7 +39,18 @@ public static IServiceCollection AddOptions(this IServiceCollection services) | |||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> | |||
/// <param name="configureOptions">The action used to configure the options.</param> | |||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> | |||
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) | |||
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class | |||
=> services.Configure(string.Empty, configureOptions); |
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.
string.Empty [](start = 34, length = 12)
Looks like we're saying un-named (legacy) options is just a named options where the name is the empty string. We should consider if this is the right "default name" to use.
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.
Yup, there's a lot of flexibility here, so we can change this as needed.
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.
Also, we also have the option of making legacy ConfigureOptions to mean configuring ALL instances.
{ | ||
if (options == null) | ||
{ | ||
throw new ArgumentNullException(nameof(options)); | ||
} | ||
|
||
Action.Invoke(options); | ||
// Null name is used to configure all named options. | ||
if (Name == null || name == Name) |
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.
Do we need this capability?
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.
ConfigureAll isn't critical, but its a pretty nice to have, otherwise they would have to duplicate common initialization in every name. I'm happy to cut it for now if you want.
{ | ||
throw new ArgumentNullException(nameof(createOptions)); | ||
} | ||
return _cache.GetOrAdd(name, new Lazy<TOptions>(createOptions)).Value; |
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.
new Lazy [](start = 41, length = 18)
Why does this need to be Lazy? GetOrAdd is called with a delegate when you want the TOptions. If the options have not yet been requested, then GetOrAdd will find nothing, and the delegate will be executed to create the options and put them in the dictionary. If the options have already been created, then GetOrAdd will just return them immediately. I'm missing then why Lazy is needed.
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 believe this is needed to ensure threadsafety, see https://andrewlock.net/making-getoradd-on-concurrentdictionary-thread-safe-using-lazy/
I looked at how MVC was accomplishing this so I just mimic'd their code, I don't have the full rationale so this is something we can spend some more cycles exploring if needed
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.
That's not really about being thread-safe; it's about the two different patterns for initializing something in a thread-safe way. The one that is generally preferred is that if the object you are initializing is not yet initialized, then you don't try to prevent two things from creating it, but ultimately only one object wins and gets stored. This is usually fine. There are occasions where the initialization code itself cannot run concurrently (usually indicates a different design issue with the initialization code) or it is important that it only ever run one because it has side effects. It does not seem to be that either of these things should be necessary here.
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.
Sure so we are ok with the factory occasionally running more than once, since only one will 'win' and get stored. That said, today we are using LazyInitialzer to guard against this I believe... https://github.com/aspnet/Options/blob/dev/src/Microsoft.Extensions.Options/OptionsCache.cs#L41
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.
Personally I am ok either way. I am cool with using Lazy<T>
for this. @ajcvickers do you feel strongly we shouldn't?
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 chatted with @ajcvickers offline the other day, he also said he didn't feel strongly and was okay with Lazy to prevent re initialization.
{ | ||
throw new ArgumentNullException(nameof(name)); | ||
} | ||
return _cache.GetOrAdd(name, () => _factory.Create(name)); |
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.
_cache.GetOrAdd(name, () => _factory.Create(name)); [](start = 19, length = 51)
Initial ideas for async. Separate out the creation and population of options into a new service--something like IOptionsSource. It depends on IOptionsFactory and IOptionsCache. For the normal, non-async, lazy route, OptionsService just depends on this and does what is happening here. But for those who want greater control, the IOptionsSource can be resolved directly and then a Create or Initialize method called that executes the factory, if needed, and stores the result in the cache. On this interface there can be an async overload that will call an async method on the factory. The idea being that the experience and patterns stay the same for normal users, but if you need async. then you can force async resolve explicitly before requesting the options.
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.
IOptionsSource
that you mentioned looks the same like IOptions.Value
implementation with injected factory. Factory starts working only when we access Value
.
The other case if we need Task<TOptions> ValueAsync
member. Extend the current interface? or create the inherited (IOptionsAsync
) one (for advanced users/cases).
re OptionsWrapper, that existed as a weird hack for non DI scenarios, we still have them a bit (particularly DataProtection uses this for the code paths where they are new'ed up. I will take a look at those call sites when we apply this change to see if we can do a round of cleanup and fix up those usages, but my guess is we will still need this. Its basically a dumb implementation of IOptions |
@ajcvickers So to summarize the set of potential changes for the next iteration:
|
} | ||
|
||
[Fact] | ||
public void FactoryCanConfigureAllOptions() |
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.
See here for an example of ConfigureAll
usage
I love the idea of extending interfaces (
Agreed. Moreover, if we will break this interface for named and unnamed interfaces, then it will be much harder to control the order of
I think we definitely can. Moreover, if it will be implemented by creating new interfaces over the existed, it will be PR without any changes of existing code (e.g. w/o any breaking changes) and will only add the Async stuff. (for now looks like that's real) |
Updated:
|
@@ -19,7 +19,7 @@ public class ConfigureFromConfigurationOptions<TOptions> : ConfigureOptions<TOpt | |||
/// <param name="name">The name of the options instance.</param> | |||
/// <param name="config">The IConfiguration instance.</param> | |||
public ConfigureFromConfigurationOptions(string name, IConfiguration config) |
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.
Is parameter name
still used? Or can be removed.
/// Implementation of IOptionsFactory. | ||
/// </summary> | ||
/// <typeparam name="TOptions">The type of options being requested.</typeparam> | ||
public class OptionsManager<TOptions> : IOptionsManager<TOptions>, IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new() |
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.
IOptionsManager<TOptions>
already implements IOptions<TOptions>
. Does it require to be duplicated?
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.
Nope, its redundant, good catch I'll remove it
@@ -19,7 +19,18 @@ public static class OptionsConfigurationServiceCollectionExtensions | |||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> | |||
/// <param name="config">The configuration being bound.</param> | |||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> | |||
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) | |||
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class | |||
=> services.Configure<TOptions>(string.Empty, config); |
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.
Probably need to use Options.DefaultName
instead of string.Empty
.
@@ -37,7 +40,18 @@ public static IServiceCollection AddOptions(this IServiceCollection services) | |||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> | |||
/// <param name="configureOptions">The action used to configure the options.</param> | |||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> | |||
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) | |||
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class | |||
=> services.Configure(string.Empty, configureOptions); |
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.
Options.DefaultName
instead of string.Empty
.
return services; | ||
} | ||
|
||
public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class | ||
=> services.Configure(name: null, configureOptions: configureOptions); |
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.
Not sure if we still need ConfigureAll
method. Probably unnamed Configure
does the same.
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.
Unnamed configure only affects Options.DefaultName
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.
Let's imagine the following:
services.Configure<MyOptions>(config1);
services.ConfigureAll<MyOptions>(config2);
services.Configure<MyOptions>("name", config3);
According the current realization OptionsFactory
we will get:
var options1 = Get<MyOptions>(); // will be applied: config1 and config2
var options2 = Get<MyOptions>("name"); // will be applied: config1, config2 and config3
If I'm not missing something, then ConfigureAll
does and works the same as unnamed Configure
.
Or you're seeing another case?
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.
That's not how it works today. options1 is correct, options2 results in config2 and config3 only.
See
public void NamedSnapshotsConfiguresInRegistrationOrder() |
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.
Actually the relevant test is
public void CanConfigureAllDefaultAndNamedOptions() |
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.
You're right. My bad.
But IMHO, unnamed Configure
and ConfigureAll
can be merged into one method.
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.
Nah, Configure has existing meaning today, its configuring the 'default' instance. Giving it a weird sideeffect of configuring all named instances is super weird.
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.
You'd basically be unable to use existing code today which has Configures with named options since every instance would pickup the default configuration.
@@ -19,4 +19,8 @@ | |||
<PackageReference Include="xunit" Version="$(XunitVersion)" /> | |||
</ItemGroup> | |||
|
|||
<ItemGroup> | |||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" /> |
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.
Required service? Am i missing something?
I wanted to go over the matrix of scenarios for options being initialized and changed to make sure I understand what people want to do, and see if my understanding maps to this. In terms of changes, we have:
In terms of initialization:
Does this cover all the cases? Taking initialization first. For options that require a request to be initialized, the IOptionsFactory will need to be scoped. This allows it to depend on other scoped services. For cases where the request is not needed it can be registered as singleton. For changes:
So the only case that requires special handling if the case where options are the same for all requests, but might change and that change should be seen by all subsequent requests. This can be handled by invalidating the cache such that the next service that requests the options will create the options again and all subsequent requests will get those new options. So then the only thing needed to make this work nicely is one or more mechanisms to nicely trigger invalidation of the cache. For example, for example, when using configuration binding, the cache should invalidate if the configuration changes. Did I understand all that correctly? If not, what am I missing. |
Yup, I believe that's an accurate summary. Right now the default OptionsFactory/new Options services are both stateless, so I added them as transient currently rather than scoped (is there a reason to favor scoped instead for stateless implementations?). My main motivation for the new design was to make it straight forward to accomplish all of these (without the old IOptionsMonitor/IOptionsSnapshot interfaces), and as you pointed out, the only 'harder' scenario is the invalidation case, which boils down to implementing the appropriate cache invalidation logic. |
One minor clarifications about this point:
So its likely that its not the factory that's depending on scoped services, but rather one of the IConfigureOptions that it uses to build the options that using request services. I'm pretty sure its always a mistake to register the factory as a singleton because of this. |
Updated PR with what we discussed (pending approval to takeover IOptions), moved
|
Updated the PR implementing configuration reload of options snapshot:
|
{ | ||
_cache = cache; | ||
_factory = factory; | ||
|
||
foreach (var source in changeSources) |
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 a little bit weird, basically we are relying on someone using IOptionsSnapshot to 'turn on' change tracking. But I think this is ok, because if no one is listening for changes, then we shouldn't turn on the cache invalidation...
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 isn't new though, we had the same issue in the past, someone needed to use the snapshot/monitor to actually run the change token wireup...
/// <param name="cache">The cache to use.</param> | ||
/// <param name="factory">The factory to use to create options.</param> | ||
/// <param name="changeSources">The change token sources used to detect options changes.</param> | ||
public OptionsSnapshot(IOptionsCache<TOptions> cache, IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> changeSources) |
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.
As a consequence of IOptionsFactory<TOptions>
being transient, every time we resolve an IOptionsSnapshot<TOptions>
we are now creating a new instance of IOptionsFactory<TOptions>
, regardless of whether we are going to call Create() on it or not. I don't want to speculate too much about how much this costs because I don't know much about the internals of different DI containers, but it sounds worth measuring the impact of this vs. getting the factory from DI only if needed.
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.
E.g. if the IEnumerable<IConfigureOptions<TOptions>>
argument in the constructor of the factory was also re-calculated and instantiated every time, then I believe the cost would be significant.
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.
Yeah I can imagine performance being a big issue when using non singleton IConfigureServices here
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 imagine it possibly being an issue if it introduces lots of extra allocations on every request. A possible solution would be to depend on the IServiceProvider
and use service location to get the factory lazily. But we should only do this if the issue is real 😄 the code is nicer this way.
@divega @ajcvickers any outstanding concerns preventing me from merging this initial iteration in today? |
I'm trying to get in all the new additive changes in this week, so when I'm back from vacation I can do all the breaking changes for auth 2.0 together |
Current iteration changes checked in bf76cdb |
State of the new world:
Configuring options
Consuming options
Invalidating the cache / updating/reloading options
@ajcvickers @divega @davidfowl @glennc