Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

How to use the IOptions pattern for configuration in ASP.NET Core RC2

$
0
0
How to use the IOptions pattern for configuration in ASP.NET Core RC2

Almost every project will have some settings that need to be configured and changed depending on the environment, or secrets that you don't want to hard code into your repository. The classic example is connection strings and passwords etc which in ASP.NET 4 were often stored in the <applicationSettings> section of web.config.

In ASP.NET Core this model of configuration has been significantly extended and enhanced. Application settings can be stored in multiple places - environment variables, appsettings.json, user secrets etc - and easily accessed through the same interface in your application. Further to this, the new configuration system in ASP.NET allows (actually, enforces) strongly typed settings using the IOptions<> pattern.

While working on an RC2 project the other day, I was trying to use this facility to bind a custom Configuration class, but for the life of me I couldn't get it to bind my properties. Partly that was down to the documentation being somewhat out of date since the launch of RC2, and partly down to the way binding works using reflection. In this post I'm going to go into demonstrate the power of the IOptions<> pattern, and describe a few of the problems I ran in to and how to solve them.

Strongly typed configuration

In ASP.NET Core, there is now no default AppSettings["MySettingKey"] way to get settings. Instead, the recommended approach is to create a strongly typed configuration class with a structure that matches a section in your configuration file (or wherever your configuration is being loaded from):

public class MySettings  
{
    public string StringSetting { get; set; }
    public int IntSetting { get; set; }
}

Would map to the lower section in the appsettings.json below.

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "MySettings": {
    "StringSetting": "My Value",
    "IntSetting": 23 
  }
}

Binding the configuration to your classes

In order to ensure your appsettings.json file is bound to the MySettings class, you need to do 2 things.
1. Setup the ConfigurationBuilder to load your file
2. Bind your settings class to a configuration section

When you create a new ASP.NET Core application from the default templates, the ConfigurationBuilder is already configured in Startup.Startup to load settings from environment variables, appsettings.json, and in development environments, from user secrets:

public Startup(IHostingEnvironment env)  
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

    if (env.IsDevelopment())
    {
        builder.AddUserSecrets();
    }

    builder.AddEnvironmentVariables();
    Configuration = builder.Build();
}

If you need to load your configuration from another source then this is the place to do it, but for most common situations this setup should suffice. There are a number of additional configuration providers that can be used to bind other sources, such as xml files for example.

In order to bind a settings class to your configuration you need to configure this in the ConfigureServices method of Startup.cs:

public void ConfigureServices(IServiceCollection services)  
{
    services.Configure<MySettings>(options => Configuration.GetSection("MySettings").Bind(options));
}

Note: The syntax for model binding has changed from RC1 to RC2 and was one of the issues I was battling with. The previous method, using services.Configure<MySettings>(Configuration.GetSection("MySettings")), is no longer available

You may also need to add the configuration binder package to the dependencies section of your project.json:

"dependencies": {
  ...
  "Microsoft.Extensions.Configuration.Binder": "1.0.0-rc2-final"
  ...
}

Update: As mentioned by Larry Ruckman, you can now use the old binding syntax if you add the package Microsoft.Extensions.Options.ConfigurationExtensions with version 1.0.0-rc2-final to your project.json

Using your configuration class

When you need to access the values of MySettings you just need to inject an instance of an IOptions<> class into the constructor of your consuming class, and let dependency injection handle the rest:

public class HomeController : Controller  
{
    private MySettings _settings;
    public HomeController(IOptions<MySettings> settings)
    {
        _settings = settings.Value
        // _settings.StringSetting == "My Value";
    }
}

The IOptions<> service exposes a Value property which contains your configured MySettings class.

It's important to note that there doesn't appear to be a way to access the raw IConfigurationRoot through dependency injection, so the strongly typed route is the only way to get to your settings.

Complex configuration classes

The example shown above is all very nice, but what if you have a very complex configuration, nested types, collections, the whole 9 yards?

public class MySettings  
{
    public string StringSetting { get; set; }
    public int IntSetting { get; set; }
    public Dictionary<string, InnerClass> Dict { get; set; }
    public List<string> ListOfValues { get; set; }
    public MyEnum AnEnum { get; set; }
}

public class InnerClass  
{
    public string Name { get; set; }
    public bool IsEnabled { get; set; } = true;
}

public enum MyEnum  
{
    None = 0, 
    Lots = 1
}

Amazingly we can bind that using the same configure<MySettings> call to the following, and it all just works:

{
  "MySettings": {
    "StringSetting": "My Value",
    "IntSetting": 23,
    "AnEnum": "Lots",
    "ListOfValues": ["Value1", "Value2"],
    "Dict": {
      "FirstKey": {
        "Name": "First Class",
           "IsEnabled":  false 
      }, 
      "SecondKey": {
        "Name": "Second Class"
      } 
    }
  }
}

When values aren't provided, they get their default values, (e.g. MySettings.Dict["SecondKey].IsEnabled == true). Dictionaries, lists and enums are all bound correctly. That is until they aren't...

Models that won't bind

So after I'd beaten the RC2 syntax change in to submission, I thought I was home and dry, but I still couldn't get my configuration class to bind correctly. Getting frustrated, I decided to dive in to the source code for the binder and see what's going on (woo, open source!).

It was there I found a number of interesting cases where a model's properties won't be bound even if there are appropriate configuration values. Most of them are fairly obvious, but could feasibly sting you if you're not aware of them. I am only going to go into scenarios that do not throw exceptions, as these seem like the hardest ones to figure out.

Properties must have a public Get method

The properties of your configuration class must have a getter, which is public and must not be an indexer, so none of these properties would bind:

private string _noGetter;  
private string[] _arr;

public string NoGetter { set { _noGetter = value; } }  
public string NonPublicGetter { set { _noGetter = value; } }  
public string this[int i]  
{
    get { return _arr[i]; }
    set { _arr[i] = value; }
}

Properties must have a public Set method...

Similarly, properties must have a public setter, so again, none of these would bind:

public string NoGetter { get; }  
public string NonPublicGetter { get; private set; }  

...Except when they don't have to

The public setter is actually only required if the value being bound is null. If it's a simple type like a string or and int, then the setter is required as there's no way to change the value. You can create readonly properties with default values, but they just won't be bound. For properties which are complex types, you don't need a setter, as long as the value has a value at binding time:

public MyInnerClass ComplexProperty { get; } = new MyInnerClass();  
public List<string> ListValues { get; } = new List<string>();  
public Dictionary<string, string> DictionaryValue1 { get; } = new Dictionary<string,string>();  
private Dictionary<string, string> _dict = new Dictionary<string,string>();  
public Dictionary<string, string> DictionaryValue2 { get { return _dict; } }  

The sub properties of the MyInnerClass object returned by ComplexProperty would be bound, values would be added to the collection in ListValues, and KeyValuePairs would be added to the dictionaries.

Dictionaries must have string keys

This is one of the gotchas that got me! While integers, are obviously perfectly valid keys to dictionaries usually, they are not allowed in this case thanks to this snippet in ConfigurationBinder.BindDictionary:

var typeInfo = dictionaryType.GetTypeInfo();

// IDictionary<K,V> is guaranteed to have exactly two parameters
var keyType = typeInfo.GenericTypeArguments[0];  
var valueType = typeInfo.GenericTypeArguments[1];

if (keyType != typeof(string))  
{
    // We only support string keys
    return;
}

Don't expose IDictionary

This is another one that got me accidentally. While coding to interfaces is nice, the model binder uses reflection and Activator.CreateInstance(type) to create the classes to be bound. If your properties are interfaces or abstract then the binder will throw when trying to create them.

If you are exposing your properties as a readonly getter however, then the binder does not need to create the property and you might think the configuration class would bind correctly. And that is true in almost all cases. Unforunately while the binder can bind any properties which are a type that derives from IDictionary<,>, it will not bind an IDictionary<,> property directly. This leaves you with the following situation:

public interface IMyDictionary<TKey, TValue> : IDictionary<TKey, TValue> { }

public class MyDictionary<TKey, TValue>  
    : Dictionary<TKey, TValue>, IMyDictionary<TKey, TValue>
{
}

public class MySettings  
{
  public IDictionary<string, string> WontBind { get; } = new Dictionary<string, string>();
  public IMyDictionary<string, string> WillBind { get; } = new MyDictionary<string, string>();
}

Our wrapper type IMyDictionary which is really just an IDictionary will be bound, whereas the directly exposed IMyDictionary will not. This doesn't feel right to me and I've raised an issue with the team.

Make properties Implementing ICollection also expose an Add method

Types deriving from ICollection<> are automatically bound in the same way as dictionaries, however the ICollection<> interface exposes no methods to add an object to the collection, only methods for enumerating and counting. It may seem strange then that it is this interface the binder looks for when checking whether a property can be bound.

If a property exposes a type that implements ICollection<> (and is not an ICollection<> itself, as for IDictionary above, though that makes sense in this case), then it is a candidate for binding. In order to add an item to the collection, reflection is used to invoke an Add method on the type:

var addMethod = typeInfo.GetDeclaredMethod("Add");  
addMethod.Invoke(collection, new[] { item });  

If an add method on the exposed type does not exist (e.g. it could be a ReadOnlyCollection<>), then this property will not be bound, but no error will be thrown, you will just get an empty collection. This one feels a little nasty to me, but I guess the common use case is you will be exposing List<> and IList<> etc. Feels like they should be looking for IList<> if that is what they need though!

Summary

The strongly typed configuration is a great addition to ASP.NET Core, providing a clean way to apply the Interface Segregation Principle to your configuration. Currently it seems more convoluted to retrieve your settings than tin ASP.NET 4, but I wouldn't be surprised if they add some convenience methods for quickly accessing values in a forthcoming release.

It's important to consider the gotchas described if you're having trouble binding values (and you're not getting an exceptions thrown). Pay particular attention to your collections, as that's where my issues arose.

Resources


Viewing all articles
Browse latest Browse all 743

Trending Articles