-
Notifications
You must be signed in to change notification settings - Fork 67
Options builder to chain Configure calls of the same named TOptions #238
Conversation
@jdom, |
/// Represents a builder that configures a named TOptions instance. | ||
/// </summary> | ||
/// <typeparam name="TOptions">The type of options being requested.</typeparam> | ||
public interface IOptionsBuilder<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.
Is IOptionsBuilder
a good name? Because it is meant to forward calls to the IServiceCollection
. An alternative is to make it agnostic of it, and then have the concrete implementation flush
the calls to the service collection, but I would think that's a little less intuitive in case you mix using the builder and using Configure directly on the service collection
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'd prefer if the only thing you can do with this builder is call Configure as opposed to allowing arbitrary access to IServiceCollection.
i.e. the only operations that would be allowed on the builder would be Configure/PostConfigure (or anything else specifically targeting the builder)
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 there any major benefit to having an interface for this as opposed to only the OptionsBuilder class itself, you can still add extension methods in the same way. There should be nothing that is worth replacing in the base class really.
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 this is all sugar on top of existing IServiceCollection
methods
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.
There isn't really a benefit for the interface. As it is, it's just syntactic sugar on top of IServiceCollection
, which is the main challenge I'm addressing. I started this thinking I would store delegates and then have an explicit call that would bind them to the IServiceCollection
, but that's what I though would be un-intuitive with no real gain (other than decoupling the builder from the DI container temporarily, which IMO is not useful).
So I can change this to be just a public type for us to hook up extension methods.
Still, the specific extension methods would need to access the IServiceCollection
to be able to forward calls to it, or do you suggest some other approach?
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 that's fine, we typically hang onto the IServiceCollection in our builders too, and you will definitely have to hang onto the Name as well.
The main benefit I see is strongly tying a specific named options instance to this builder, so you can just call Configure/PostConfigure without the generic or specifying the name.
I'm still trying to see if we could use this instead of things like ConfigureApplicationCookie
that we have today.
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.
Hehe, configuring the cookie is the only example I saw of using named options in the wild, and I'm not terribly fond of the approach, and hence this proposal. In Orleans we'll have many cases of configuring named services/options.
But even for our singleton service abstractions that can be fulfilled with 1 of different implementations, where each of them might have slightly different typed options, this can be of a lot of use, as the end user will not have to discover which strongly typed options to use, as well as giving the flexibility to chain multiple calls (and not just pick only 1 of the Action<TOptions>
or IConfiguration
overloads, in case we provide the extension methods with these overloads).
public void CanSupportDefaultName() | ||
{ | ||
var services = new ServiceCollection().AddOptions(); | ||
var builder = services.CreateOptionsBuilder<FakeOptions>(); |
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.
Nit: maybe shorten CreateOptionsBuilder
=> BuildOptions
?
/// <typeparam name="TOptions">The options type to be configured.</typeparam> | ||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> | ||
/// <returns>The <see cref="OptionsBuilder{TOptions}"/> so that configure calls can be chained in it.</returns> | ||
public static OptionsBuilder<TOptions> BuildOptions<TOptions>(this IServiceCollection services) 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.
I'm not sure about this 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.
What about these alternatives?
Configure<TOptions>()
(as it would be the only of these overloads that don't take in an extra argument, but it might be confusing that they have different return values)CreateOptionsBuilder<TOptions>()
ConfigureOptions<TOptions>()
(although there is already a non-generic overload with this name that takes a type, so it might be very confusing)BuildOptions<TOptions>()
(just putting it here for contrasting)ConfiguratorFor<TOptions>()
(probably might make sense to renameOptionsBuilder
toOptionsConfigurator
if we choose this extension method name, and maybe even regardless, as this helper doesn't build from scratch, just helps configure).UseConfigurator<TOptions>()
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.
BTW, reflecting on this, my preference would be option 5.
Any preferences for naming, or further feedback on the approach? |
So I thought about this a bit more, and I think we can maybe do something more drastic, akin to what we did with authentication... Where we add So startup would look more like:
This lets us scope options apis to OptionsBuilder and not pollute IServiceCollection, and we can still hang the apis specific to a particular options type (hiding the name for example) to an instance of Thoughts @davidfowl @ajcvickers @jdom ? |
@HaoK your suggestion will work for us, so we'll gladly take it, although I'm not sure it is better IMO. The current approach just adds a single extension method to IServiceCollection, with an overload for taking the name (so technically 2 methods with the same name).
While your approach would favor more this:
Granted, it doesn't prevent the original code, but you can see that where it's most useful is not by chaining different option builders together, but chaining the addition of services with their options, and the user doesn't need to discover which options to configure, or what name to correlate with the named service. |
So I'd expect you to just have
|
The |
AddOptions<T> now calls AddOptions() implicitly.
This reverts commit a47db5a.
Looks good to me, any concerns @davidfowl @ajcvickers ? |
/// <param name="optionsBuilder">The options builder to add the services to.</param> | ||
/// <param name="config">The configuration being bound.</param> | ||
/// <returns>The <see cref="OptionsBuilder{TOptions}"/> so that additional calls can be chained.</returns> | ||
public static OptionsBuilder<TOptions> Configure<TOptions>(this OptionsBuilder<TOptions> optionsBuilder, IConfiguration config) 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.
Actually now that we have a Builder to hang this off of, we don't have to call it Configure, we should call this method Bind
to give it more meaning
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 would gladly do that change, but let's clarify the bigger picture first. Does this also apply to the overload that takes and Action<TOptions>
. And would we rename PostConfigure
to PostBind
?
Or maybe those overloads should be renamed to Apply
and PostApply
? Or Update
?
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 we probably should switch to something different, its going to be too insane to keep adding all these overloads as longer term I wouldn't be surprised if there were more lifecycles either, like Validation.
Which probably would mean something closer to:
Bind(IConfiguration, OptionsLifecycle.[Configure/PostConfigure/Validation])
.
To level we'd still have
Configure
PostConfigure
(Validate in the future)
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 looks like we don't have PostConfigure(IConfiguration) today anyways, so I don't think you need to worry about it for the purposes of this PR... We can tackle this in a different issue/PR
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.
Its just sugar for AddOptions<T>.PostConfigure(o => config.Bind(o))
anyways.
@HaoK Looks okay to me. |
Thanks @jdom I'll merge this into dev now |
Addresses #237
This allows to add a named service to the application, as well as allow the end-user to configure its options, without the need to discover what are the TOptions it needs to chain calls to, nor having to pass the name on every .
Configure<TOptions>(name, ...)
call.This results in the user being able to do things like this for extensions that use the builder:
Notice how the 2 calls to the Configure method automatically infer the
TOptions
type, and do not need to pass the name (even thought they are configuring a named option.Also, the author of the
AddNamedXyz
extension method does not need to reference theMicrosoft.Extensions.Options.ConfigurationExtension
nuget package just in case the end user cares to use theIConfiguration
overload. If the end user references it, then the relevantConfigure
overload will automatically light up on the options builder.