.NET Core use Configuration to bind to Options with Array
Asked Answered
C

4

42

Using the .NET Core Microsoft.Extensions.Configuration is it possible to bind to a Configuration to an object that contains an array?

ConfigurationBinder has a method BindArray, so I'd assume it would work.

But when I try it out I get an exception:

System.NotSupportedException: ArrayConverter cannot convert from System.String.

Here's my slimmed down code:

public class Test
{
   private class ExampleOption
   { 
      public int[] Array {get;set;}
   }

   [Test]
   public void CanBindArray()
   {
       // ARRANGE
       var config =
            new ConfigurationBuilder()
            .AddInMemoryCollection(new List<KeyValuePair<string, string>>
            {
                new KeyValuePair<string, string>("Array", "[1,2,3]")
            })
            .Build();

        var exampleOption= new ExampleOption();

        // ACT
        config.Bind(complexOptions); // throws exception

       // ASSERT
       exampleOption.ShouldContain(1);
   }
}
Coremaker answered 15/6, 2016 at 2:26 Comment(1)
The sample property "Array" is a little bit tricky for explanations ;) I would have corrected it but there was already another answer and a comment which I could not correct.Grocer
G
68

The error is in your input definition. The sample sets a key "Array" to a string value of "[1,2,3]" (in the C# based InMemoryCollection) and makes the assumption it is parsed JSON style. That is wrong. It is just not parsed.

The encoding convention of array values in the config system is by repeating the key with a colon and an index behind it. The following sample works like you intend to do:

var config = new ConfigurationBuilder()
        .AddInMemoryCollection(new List<KeyValuePair<string, string>>
        {
            new KeyValuePair<string, string>("Array:0", "1"),
            new KeyValuePair<string, string>("Array:1", "2"),
            new KeyValuePair<string, string>("Array:2", "3")
        })
        .Build();

The colon-key-repeating scheme happens also if JSON file is used (here by an additional call to AddJsonFile) ...

{
  "mySecondArray":  [1, 2, 3]
}

the resulting combined configuration will contain the keys which follow the same pattern as illustrated for in-memory usage above:

Count = 8
[0]: {[mySecondArray, ]}
[1]: {[mySecondArray:2, 3]}
[2]: {[mySecondArray:1, 2]}
[3]: {[mySecondArray:0, 1]}
[4]: {[Array, ]}
[5]: {[Array:2, 3]}
[6]: {[Array:1, 2]}
[7]: {[Array:0, 1]}

The config system is agnostic to storage formats like JSON/INI/XML/... and is essentially just a string->string dictionary with colon making up a hierarchy within the key.

Bind is then able to interpret some of the hierarchy by conventions and therefore binds also arrays, collections, objects and dictionaries. Interestingly for arrays, it does not care about the numbers behind the colon but just iterate the children of the configuration section (here "Array") and take the values of the children. The sorting of the children again, takes the numbers into consideration but also sorts strings as a second option (OrdinalIgnoreCase).

Grocer answered 15/6, 2016 at 19:51 Comment(4)
The config system of ASP.NET is really a nice and useful piece of technology also outside of ASP.NET. Should be in everyone's toolbox.Grocer
Here Here! Quick tangent: Have you seen any documentation/blogs on how to wire up IOptions using 3rd party DI? I built a prototype using Ninject, but not sure I've got full support for everything, especially ChangeTokens.Coremaker
No Idea. I am weak on the options topic.Grocer
You can use the same technique for non-value types as well, you just need to append the property to the key. So if your class contained Id and Name, you'd have two key entries: "Array:0:Id" and "Array:0:Name". (Learned after struggling with using a JSON string as a value and getting null returned).Social
P
18

With recent additions to C# language it is cleaner to use the newer syntax:

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string>
    {
        { "Array:0", "1" },
        { "Array:1", "2" },
        { "Array:2", "3" },
    })
    .Build();

or you can use other new syntax

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string>
    {
        ["Array:0"] = "1",
        ["Array:1"] = "2",
        ["Array:2"] = "3",
    })
    .Build();
Pirali answered 20/2, 2019 at 18:33 Comment(1)
With the additions it should look like this: ["Array:0"] = "1", ....Edible
F
7

You can configure ExampleOptionwith code in ConfigureServices method:

 public void ConfigureServices(IServiceCollection services)
 {
      services.Configure<ExampleOption>(myOptions =>
      {
          myOptions.Array = new int[] { 1, 2, 3 };
      });
 }

or if you want to use json configuration file

appsettings.json:

{
  "ExampleOption": {
     "Array": [1,2,3]
  }
}

ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<ExampleOption>(Configuration.GetSection("ExampleOption"));
}
Fussbudget answered 15/6, 2016 at 7:45 Comment(3)
Is it possible to do this without the Options Extensions? Using only ConfigurationBinder? And when I use "Array":"[1,2,3]" in the In Memory Configuration, that's when I get the ArraryConverter exception I cited. Are you saying it works in a Json file?Coremaker
i don't know if there is a way to solve without Options Extensions but in a json file it works as expected.Fussbudget
Thanks for answering here. It's good to know that the array initialization syntax is nice and simple for Json files; but doesn't fully align with what I was looking for in this question (using AddInMemoryCollection)Coremaker
M
0

here is a simpler approach where you can do it without necessarily re-initializing configuration. this approach also avoids the messy "options pattern".

#region SET DEMO STATE
var demoMode = true; // switch this and rebuild to change demo state
var inMemorySettings = new List<KeyValuePair<string, string?>>
    {
        new KeyValuePair<string, string?>("DemoMode", demoMode.ToString())
    };
builder.Configuration.AddInMemoryCollection(inMemorySettings);
// HOW TO USE: `configuration["DemoMode"]`
#endregion

Just like that, you can add/inject properties into existing configuration with code. You can even do it in a single line, but it would be less readable. You can then use dependency injection as usual and access this property with configuration["DemoMode"], as if it was a property in appsettings.json.

Note: This is useful if you don't want to expose some settings to appsettings.json.

Mirellamirelle answered 14/11, 2023 at 8:32 Comment(1)
the "Messy Options Pattern" facilitates strong typing of configuration, which is what I was looking to use. Your solution looks to use a Dictionary<string,string>. While the binding is simpler, it uses magic string (for keys) and stores all config values as strings - both of which I am looking to avoid.Coremaker

© 2022 - 2024 — McMap. All rights reserved.