7
\$\begingroup\$

I have the following extension method to add themes support to my application:

public static void AddThemes(this IServiceCollection services, Action<ThemesOptions> setup) {
    services.Configure(setup);
}

Where ThemesOptions is defined as:

public class ThemesOptions {
    public IEnumerable<string> Themes { get; set; }
}

Now in my application's startup ConfigureServices method I can say:

services.AddThemes(options => {
    options.Themes = Configuration.GetSection("Themes").Get<ThemesOptions>().Themes;
});

I'm not sure that I like that I have to set every property for the options. Alternatively I tried:

services.AddThemes(options => {
    options = Configuration.GetSection("Themes").Get<ThemesOptions>();
});

And:

services.AddThemes(options => Configuration.GetSection("Themes"));

However when I inject IOptions<ThemesOptions> the Themes property is null.

An alternative to I changed my extension method to:

public static void AddThemes(this IServiceCollection services, IConfiguration configuration) {
    services.Configure<ThemesOptions>(configuration.GetSection("Themes"));
}

Now I can say the following within my application's Startup ConfigureServices method:

services.AddThemes(Configuration);

This worked fine, however, I feel the problem with this approach is that the extension method only allows the options to be set from the configuration.

I'd appreciate it if someone could confirm whether my first solution is correct and if it can be improved upon.

\$\endgroup\$
4
\$\begingroup\$

Instead of doing

services.AddThemes(options => {
    options = Configuration.GetSection("Themes").Get<ThemesOptions>();
});

you could use

services.AddThemes(options => {
    Configuration.GetSection("Themes").Bind(options);
});

which will programatically set each value of options based on what's in the configuration.

\$\endgroup\$
  • \$\begingroup\$ Thanks, I've changed your answer to be the accepted one as it specifically addresses the original question. \$\endgroup\$ – nfplee Jun 24 at 16:35
4
\$\begingroup\$

With your ThemesOptions defined as:

public class ThemesOptions {
    public IEnumerable<string> Themes { get; set; }
}

and Reference #1 Configure simple options with a delegate

public static void AddThemes(this IServiceCollection services, Action<ThemesOptions> configureOptions) {
    //Options bound and configured by a delegate
    services.Configure<ThemesOptions>(configureOptions);
}

Will allow the simple delegate to be used when configuring options services.

//Options bound and configured by a delegate
services.AddThemes(option => {
    option.Themes = new [] { "Theme1", "Theme2" /*, "SomeOtherTheme" */};
});

Now assuming an app settings json file like

{
  "option1": "value1_from_json",
  "option2": -1,
  "Themes": [
    "Theme1", "Theme2"
  ]      
}

and Reference #2 Suboptions configuration

you defined the following

public static void AddThemes(this IServiceCollection services, IConfiguration configuration) {
    // Bind options using a sub-section of the appsettings.json file.
    services.Configure<ThemesOptions>(configuration.GetSection("Themes"));
}

and called like

services.AddThemes(Configuration);

As you rightly stated, it works. Yes. That is because the ThemesOptions class defines a property Themes that holds a collection of strings which would match what GetSection("Themes") would return and thus bind.

Now let's look at the ones that did not work and why they don't

In both cases

services.AddThemes(options => {
    options = Configuration.GetSection("Themes").Get<ThemesOptions>();
});

And:

services.AddThemes(options => Configuration.GetSection("Themes"));

you are trying to set the value of a provided argument to the delegate which change the value of the object and in the second one, nothing is done with the option provided. When using the delegate option the value from the settings file are overridden by the configured delegate which is why Themes property of injected IOptions<ThemesOptions> is null

services.AddThemes(options => {
    options.Themes = Configuration.GetSection("Themes").Get<ThemesOptions>().Themes;
});

works because you are populating the members of the passed parameter which was initialized by the options builder to the delegate.

If this configuration section has a value, that will be used. Otherwise binding by matching property names against configuration keys recursively

Each call to Configure<TOptions> adds an IConfigureOptions<TOptions> service to the service container. When more than one configuration service is enabled, the last configuration source specified wins and sets the configuration value.

\$\endgroup\$
  • \$\begingroup\$ Is there no way to set the whole configuration without setting each property individually? \$\endgroup\$ – nfplee Jan 24 '18 at 21:47
  • \$\begingroup\$ @nfplee clarify what you mean by the whole configuration? are you talking about the one class Theme or all other options in the settings file? . \$\endgroup\$ – Nkosi Jan 24 '18 at 21:51
  • \$\begingroup\$ I'm using this as an example of a pattern. If in the example above ThemeOptions contained more than one property I'd like to be able to set every property in one line (ideally without using reflection). I had hoped that this was a common scenario. \$\endgroup\$ – nfplee Jan 25 '18 at 11:57
  • 1
    \$\begingroup\$ @nfplee if you look at the examples in the links provided you will see that is the convention used by core for options. If themes has multiple properties, the sub options route would populate all the matching properties on the options object \$\endgroup\$ – Nkosi Jan 25 '18 at 11:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.