Skip to content
This repository has been archived by the owner on Nov 7, 2018. It is now read-only.

Options builder to chain Configure calls of the same named TOptions #238

Merged
merged 11 commits into from
Nov 10, 2017

Conversation

jdom
Copy link
Contributor

@jdom jdom commented Oct 9, 2017

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:

services.AddNamedXyz("name", optionsBuilder => 
   optionsBuilder.Configure(Configuration.GetSection("Something"))
   optionsBuilder.Configure(options => options.SomeSetting = "value for name"));

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 the Microsoft.Extensions.Options.ConfigurationExtension nuget package just in case the end user cares to use the IConfiguration overload. If the end user references it, then the relevant Configure overload will automatically light up on the options builder.

@dnfclas
Copy link

dnfclas commented Oct 9, 2017

@jdom,
Thanks for having already signed the Contribution License Agreement. Your agreement was validated by .NET Foundation. We will now review your pull request.
Thanks,
.NET Foundation Pull Request Bot

/// 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
Copy link
Contributor Author

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

Copy link
Member

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)

Copy link
Member

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.

Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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>();
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about these alternatives?

  1. 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)
  2. CreateOptionsBuilder<TOptions>()
  3. ConfigureOptions<TOptions>() (although there is already a non-generic overload with this name that takes a type, so it might be very confusing)
  4. BuildOptions<TOptions>() (just putting it here for contrasting)
  5. ConfiguratorFor<TOptions>() (probably might make sense to rename OptionsBuilder to OptionsConfigurator if we choose this extension method name, and maybe even regardless, as this helper doesn't build from scratch, just helps configure).
  6. UseConfigurator<TOptions>()

Copy link
Contributor Author

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.

@jdom
Copy link
Contributor Author

jdom commented Oct 28, 2017

Any preferences for naming, or further feedback on the approach?

@HaoK
Copy link
Member

HaoK commented Nov 3, 2017

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 OptionsBuilder, which has OptionsBuilder<T> Build<T>(string name, Action<OptionsBuilder<T>> build) method.

So startup would look more like:

  services.BuildOptions()
    .Build<Options1>("name", o => o.Configure(config)) // named instance
    .Build<Options1>(o => o.Configure(config)) // no name

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 OptionsBuilder<T>

Thoughts @davidfowl @ajcvickers @jdom ?

@jdom
Copy link
Contributor Author

jdom commented Nov 3, 2017

@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).
Your suggestion has only 1 total, but you have to chain it to be useful, otherwise BuildOptions() by itself doesn't add value. Also, at least in our scenarios, it's not that the end user will start configuring a bunch of named options in one go... they will do it while they are adding concrete service instances, so it makes sense that they they get the options builder/configurator at the time they add that specific service. For that I mean they'll do this:

services
    .AddFooService("service1", ob => ob.Configure(config).Configure(o => o.FooSetting = 3))
    .AddBarService("service2", ob => ob.Configure(config).Configure(o => o.BarSetting = "bar"))
    .AddBarService("service3", ob => ob.Configure(config));

While your approach would favor more this:

services
    .AddFooService("service1")
    .AddBarService("service2")
    .AddBarService("service3")
    .BuildOptions()
         .Build<FooServiceOptions>("service1", ob => ob.Configure(config).Configure(o => o.FooSetting = 3)
         .Build<BarServiceOptions>("service2", ob => ob.Configure(config).Configure(o => o.BarSetting = "bar"))
         .Build<BarServiceOptions>("service3", ob => ob.Configure(config)));

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.

@HaoK
Copy link
Member

HaoK commented Nov 3, 2017

So I'd expect you to just have AddFooService return OptionsBuilder<FooOptions>, and then you'd still be able to do something like:

services.AddFooService("service1").Configure(config).Configure(o => o.FooSetting = 3))

@HaoK
Copy link
Member

HaoK commented Nov 3, 2017

The BuildOptions block would be more for if you wanted to configure all the options in once place. AddXyz should still be free (and encouraged) to return the appropriate sugar to make it easy to configure things.

@HaoK
Copy link
Member

HaoK commented Nov 6, 2017

@jdom I'm going to prototype the OptionsBuilder in this PR: #239 as we can have these new Configure overloads return an OptionsBuilder<T> instead of IServiceCollection

@HaoK
Copy link
Member

HaoK commented Nov 7, 2017

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
Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Member

@HaoK HaoK Nov 9, 2017

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)

Copy link
Member

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

Copy link
Member

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.

@ajcvickers
Copy link

@HaoK Looks okay to me.

@HaoK
Copy link
Member

HaoK commented Nov 10, 2017

Thanks @jdom I'll merge this into dev now

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants