Binding .NET Core Unrecognized configuration properties

521 Views Asked by At

Suppose we have the following config file

{
  "TeamRoster:People:0:Name": "Bryan",
  "TeamRoster:People:0:City": "Toronto",
  "TeamRoster:People:0:Interests": "Code",

  "TeamRoster:People:1:Name": "Sebastian",
  "TeamRoster:People:1:City": "Vancouver",
  "TeamRoster:People:1:ApartmentType": "Condo"

}

And the following POCOs to represent this config:

public class TeamRoster
{
    public Person[] People { get; set; }
}

public class Person
{
   public string Name { get; set; }
   public string City { get; set; }
}

Assuming I'm using a ConfigurationBuilder to read the dotnet core config with something like:

IConfiguration config =  
     new ConfigurationBuilder()
           .AddJsonFile("file.json")
           .Build();

var myConfig = config.GetSection("TeamRoster").Get<TeamRoster>();

How can I capture the additional properties that weren't part of the Person object (Interests, ApartmentType)?

My reasoning is beyond the trivial example above. In my model, I might have dozens of additional attributes (eg like polymorphic types) and I'd want to capture them without having to define them in the config object. Ideally, I'd want to do this like [JsonExtendedData] but as far as I understand, even though the data is expressed as Json that's just the source. It's read into a config object and then bound.

Is there a way to customize the .net core binding?

1

There are 1 best solutions below

0
On BEST ANSWER

To shed some light on this, IConfigurationRoot within Microsoft.Extensions.Configuration abstracts the configuration data from its underlying configuration sources. Out of the box, configuration data can come from Json, Azure KeyVault, Command Line, Environment Variables, Xml, Memory, Ini files, User secrets and individual files (key per file).

Each of these configuration sources loads their data into a dictionary where hierarchical values are separated using ':'. When you fetch data from a IConfiguration object, it attempts to resolve the value from each data source in reverse order. Thus the last added configuration source has the highest precedence.

The mapping of IConfiguration objects to POCO objects is done using the ConfigurationBinder. While the ConfigurationBinder does support resolution of values using TypeConverters, this scenario is only helpful for mapping individual string values within the config dictionary, not entire objects.

The ConfigurationBinder has native support for binding collections, so in theory the following two approaches can be used for the following config.

Separate Fixed Values from Dynamic Values

Represent the data so that the dynamic data is separate from the static data.

{
   "auth": {
      "providers": [
         {
            "name": "clientsecret",
            "loginUrl": "https://login.microsoftonline.com/mytenant/oauth2",
            "fields": {
               "client_id": "xxx",
               "client_secret": "yyy"
               "resource": "https://management.azure.com",
               "grant_type": "client_credentials"
            }
         },
         ...
      ]
   }
}

When binding the dynamic fields to a Dictionary<string,string>, the Binder maps fields as Key/Value pairs. The key advantage for this approach is we're able to leverage TypeConverters and type-safety with the fixed fields.

public class AuthProvider
{
    // FIXED FIELDS
    public string Name { get; set; }
    public Uri LoginUrl { get; set; }

    // DYNAMIC FIELDS
    public Dictionary<string,string> Fields { get; set; }
}

Map all Data as Dynamic

The slightly more hackish route is to describe all the fields in the data as siblings and then bind the entire data as a Dictionary.

{
   "auth": {
     "providers": [
       {
         "name": "clientsecret",
         "loginurl": "https://somewhere",
         "client_id": "xxx",
         "client_secret": "yyy",
         "scope": "https://url",
         "grant_type": "client_credentials"
       }
     ]
   }
}

We can simply take advantage of binding directly to a dictionary. Note we use lose the type-converter for the Uri field:

public class AuthProvider : Dictionary<string,string>
{
  public string Name => this["name"]
  public string LoginUrl => this["loginUrl"]

  public Dictionary<string,string> Fields
  {
     get 
     {
       var propertyNames = GetType().GetProperties().Select(x => x.Name).ToArray();

       var keyPairs = this.Where(kvp => !propertyNames.Contains(kvp.Key));

       return 
          new Dictionary<string,string>(
                keyPairs, 
                StringComparer.OrdinalIgnoreCase);
     }
  }
}