0

I'm currently trying to use the default ConfigurationBinder from AspNetCore 6 especially IConfiguration.Get<T>()

Get<T>() is used to map a config file section to an object, but it doesn't work correctly with collections like arrays and lists.

It works fine if there are at least 2 items in the array but if there's only one item in the array then the mapping doesn't work. This seems to only affect XML files.

The array can be nested deep inside my sections.

The main problem seems to be that the config keys are generated like this:
collection1:item:myName for arrays with one element.
vs
collection2:item:0:myName for arrays with more than one element.

Has anyone a good idea how to accomplish the mapping to arrays inside XML config sections that might have one or more elements?

// Nugets:
// Microsoft.Extensions.Configuration
// Microsoft.Extensions.Configuration.Xml
// Microsoft.Extensions.Configuration.Binder
    
public void Test()
{
    var builder = new ConfigurationBuilder();
    builder.AddXmlFile("MyConfig.config");

    var config = builder.Build();

    var section1 = config.GetSection("collection1");
    var section2 = config.GetSection("collection2");

    var bound1 = section1.Get<MyObject>(); // ERROR: Mapped to empty List
    var bound2 = section2.Get<MyObject>(); // mapped correctly to 2 items
}

public class MyObject
{
    public List<MyItem> Item { get; set; }
}

public class MyItem
{
    public string MyName { get; set; }
    public string MyVal { get; set; }

    public override string ToString() => $"{MyName} = {MyVal}";
}

MyConfig.config

<configuration>
    <collection1 >
        <item myName="MyName1" myVal="MyString1" />
    </collection1>

    <collection2 >
        <item myName="MyName1" myVal="MyString1" />
        <item myName="MyName2" myVal="MyString2" />
    </collection2>
</configuration>
Charles
  • 2,721
  • 1
  • 9
  • 15

2 Answers2

1

Do not get objects yourself, configure a mapping:

services.Configure<MyObject>(configuration.GetSection("collection1"));

Then inject your settings where necessary as Options:

public class MyClass{
   public MyClass(IOptions<MyObject> options){
     ...
   }
}

This should handle the keys correctly.

That said, keep in mind that arrays in configurations can produce a lot of problems when using multiple stages, i.e. production and dev configurations files, see my answer here (as I see now from your question, the samples there are probably not 100% correct for an array with one element, but they definitely are for multiple elements).

On the other hand, is the number of elements really unbound or like 5 or 10 would be enough? You could add normal properties, e.g. 1 to 5, and collect them in an array in your MyObject, like:

public class MyObject{
  public string P1 {get;set;}
  public string P2 {get;set;}

  public List<string> GetProperties(){...}
}
Maxim Zabolotskikh
  • 3,091
  • 20
  • 21
  • 1
    Hello Maxim thank you for your response. If I inject the options the Result is the same, the collection is empty when the array has only one object. Behind the scenes the same functions are used to bind the objects. I've found out that this is a known bug and scheduled to be added for .NET 7 release. https://github.com/dotnet/runtime/issues/57325 – Charles Feb 16 '22 at 14:07
  • 1
    Good finding. At least it's xml provider specific - I rushed to check our code, because we do have arrays with single elements for 3d party nuget packages, but our providers are all JSON and there it seems to work correctly. This brings me to a crazy workaround: you could read XML from file, convert it to JSON with Newtonsoft JsonConvert.SerializeXmlNode, and then feed the text as MemoryStream to the ConfigurationBuilder with AddJsonStream. At least you could use your XML if you really need to till the fix is there. – Maxim Zabolotskikh Feb 17 '22 at 07:34
0

I think i found an easy solution to the Problem for now.

<configuration>
    <collection1 >
        <item name="MyName1" myVal="MyString1" />
    </collection1>

    <collection2 >
        <item name="MyName1" myVal="MyString1" />
        <item name="MyName2" myVal="MyString2" />
    </collection2>
</configuration>

If we give each element of the collection the attribute name then the mapping works fine as long as the name is unique.

Binding a single element to an array on ConfigurationBinder will be fixed in .NET 7.
https://github.com/dotnet/runtime/issues/57325

Charles
  • 2,721
  • 1
  • 9
  • 15