7

I have a appsettings.json that look like this

{
  "AppName": "my-app-service",
  "MyModel": {
    "MyList": [{
      "CountryCode": "jp"
    },{
      "CountryCode": "us"
    }]
  }
}

Now, i have a POCO file, (MyList.cs is ommited, it has a single string key countryCode)

public class MyModel
{
    public IList<MyList> MyList;
}

I wish to make it so that MyModel can be injected anywhere in my code, how do I do that? I see that people do this in their setup.cs

    services.Configure<MyModel>(Configuration.GetSection("MyModel"));

How ever it looks like when I use IOption<MyModel> in my code in the contructors, I just get null value. Where am I wrong?

Zhen Liu
  • 7,550
  • 14
  • 53
  • 96
  • Can you please share your code that is converting the json to the model please? Otherwise you may have to read it out into an object using Newtonsoft JSON. – Slipoch Jul 29 '20 at 23:06
  • @Slipoch That is the question I am trying to ask. How do I convert the JSON to "MyModel" directly through this Configuration magic? – Zhen Liu Jul 29 '20 at 23:10
  • What you describe should work (Configure + IOptions). If you want to validate that it's binding correctly you can use `var o = new MyModel(); Configuration.GetSection("MyModel").Bind(o);` and then inspect `o` which should now be populated. You might need a reference to `Microsoft.Extensions.Configuration.Binder` if you don't already have it via transitive references. – pinkfloydx33 Jul 29 '20 at 23:22
  • 3
    Though now looking at your class more carefully, `MyList` is a **field**. The binder only works with **properties** so add `{ get; set; }` and you should be good to go – pinkfloydx33 Jul 29 '20 at 23:23
  • 1
    @pinkfloydx33 wow this is the biggest gotcha ever. Thanks! – Zhen Liu Jul 29 '20 at 23:33

2 Answers2

13

You are correct: calling Configure<T> sets up the options infrastructure for T. This include IOptions<T>, IOptionsMonitor<T> and IOptionsSnapshot<T>. In its simplest forms, configuring the value uses an Action<T> or as in your example binding to a specific IConfiguration. You may also stack multiple calls to either form of Configure. This then allows you to accept an IOptions<T> (or monitor/snapshot) in your class' constructor.

The easiest way to determine if binding to an IConfigurationSection is working as you intend it to is to manually perform the binding and then inspect the value:

var opts = new MyClass();
var config = Configuration.GetSection("MyClass");
// bind manually
config.Bind(opts);
// now inspect opts

The above is dependant on the Microsoft.Extensions.Configuration.Binder package which you should already have as a transitive dependency if you are referencing the Options infrastructure.

Back to your issue: the binder will only bind public properties by default. In your case MyClass.MyList is a field. To get the binding to work you must change it to a property instead.

If you wanted/needed to bind non-public properties you could pass an Action<BinderOptions>:

// manually
var opts = new MyClass();
var config = Configuration.GetSection("MyClass");
// bind manually
config.Bind(opts, o => o.BindNonPublicProperties = true);

// DI/services 
var config = Configuration.GetSection("MyClass");
services.Configure<MyClass>(config, o => o.BindNonPublicProperties = true);

Even with BinderOptions there is still no way to bind to fields. Also note that there is varying behavior for things like collection interfaces, arrays and get only properties. You may need to play around a bit to ensure things are binding as you intend.

pinkfloydx33
  • 11,863
  • 3
  • 46
  • 63
2

If you have some appsettings.json like :

{ 
  "SomeConfig": {
  "Key1": "Value1",
  "Key2": "Value2",
  "Key3": "Value3"
  } 
} 

Then you can have your POCO as :

public struct SomeConfig
{
    public string Key1 { get; set; }

    public string Key2 { get; set; }

    public string Key3 { get; set; }
}

After this you need to put services.Configure<SomeConfig>(Configuration.GetSection("SomeConfig")); entry in public void ConfigureServices(IServiceCollection services)

Now in any class where you want to use it :

private readonly ILogger logger;
private readonly SomeConfig someConfigurations;

public SampleService(IOptions<SomeConfig> someConfigOptions, ILogger logger)
{
   this.logger = logger;
   someConfigurations = someConfigOptions.Value;
   logger.Information($"Value of key1 : '{someConfigurations.Key1}'");
}