How to extract a list from appsettings.json in .net core

C#asp.net Core.Net Core

C# Problem Overview


I have an appsettings.json file which looks like this:

{
    "someSetting": {
        "subSettings": [
            "one",
            "two",
            "three"
         ]
    }
}

When I build my configuration root, and do something like config["someSetting:subSettings"] it returns null and the actual settings available are something like this:

config["someSettings:subSettings:0"]

Is there a better way of retrieving the contents of someSettings:subSettings as a list?

C# Solutions


Solution 1 - C#

Assuming your appsettings.json looks like this:

{
  "foo": {
    "bar": [
      "1",
      "2",
      "3"
    ]
  }
}

You can extract the list items like so:

Configuration.GetSection("foo:bar").Get<List<string>>()

Solution 2 - C#

You can use the Configuration binder to get a strong type representation of the configuration sources.

This is an example from a test that I wrote before, hope it helps:

    [Fact]
    public void BindList()
    {
        var input = new Dictionary<string, string>
        {
            {"StringList:0", "val0"},
            {"StringList:1", "val1"},
            {"StringList:2", "val2"},
            {"StringList:x", "valx"}
        };

        var configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.AddInMemoryCollection(input);
        var config = configurationBuilder.Build();

        var list = new List<string>();
        config.GetSection("StringList").Bind(list);

        Assert.Equal(4, list.Count);

        Assert.Equal("val0", list[0]);
        Assert.Equal("val1", list[1]);
        Assert.Equal("val2", list[2]);
        Assert.Equal("valx", list[3]);
    }

The important part is the call to Bind.

The test and more examples are on GitHub

Solution 3 - C#

In .NetCore this is what I did:

Normal Setup:

In your appsettings.json create a configuration section for your custom definitions:

    "IDP": [
    {
      "Server": "asdfsd",
      "Authority": "asdfasd",
      "Audience": "asdfadf"
    },
    {
      "Server": "aaaaaa",
      "Authority": "aaaaaa",
      "Audience": "aaaa"
    }
  ]

Create a class to model the objects:

public class IDP
{
    public String Server { get; set; }
    public String Authority { get; set; }
    public String Audience { get; set; }

}

in your Startup -> ConfigureServices

services.Configure<List<IDP>>(Configuration.GetSection("IDP"));

> Note: if you need to immediately access your list within your ConfigureServices method > you can use... > > var subSettings = Configuration.GetSection("IDP").Get>();

Then in your controller something like this:

Public class AccountController: Controller
{
    private readonly IOptions<List<IDP>> _IDPs;
    public AccountController(IOptions<List<Defined>> IDPs)
    {
        _IDPs = IDPs;
    }
  ...
}

just as an example I used it elsewhere in the above controller like this:

       _IDPs.Value.ForEach(x => {
            // do something with x
        });

Edge Case

In the case that you need multiple configs but they can't be in an array and you have no idea how many sub-settings you will have at any one time. Use the following method.

appsettings.json

"IDP": {
    "0": {
      "Description": "idp01_test",
      "IDPServer": "https://intapi.somedomain.com/testing/idp01/v1.0",
      "IDPClient": "someapi",
      "Format": "IDP"
    },
    "1": {
      "Description": "idpb2c_test",
      "IDPServer": "https://intapi.somedomain.com/testing/idpb2c",
      "IDPClient": "api1",
      "Format": "IDP"
    },
    "2": {
      "Description": "MyApp",
      "Instance": "https://sts.windows.net/",
      "ClientId": "https://somedomain.com/12345678-5191-1111-bcdf-782d958de2b3",
      "Domain": "somedomain.com",
      "TenantId": "87654321-a10f-499f-9b5f-6de6ef439787",
      "Format": "AzureAD"
    }
  }

Model

public class IDP
{
    public String Description { get; set; }
    public String IDPServer { get; set; }
    public String IDPClient { get; set; }
    public String Format { get; set; }
    public String Instance { get; set; }
    public String ClientId { get; set; }
    public String Domain { get; set; }
    public String TenantId { get; set; }
}

Create Extension for Expando Object

public static class ExpandObjectExtension
    {
        public static TObject ToObject<TObject>(this IDictionary<string, object> someSource, BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public)
               where TObject : class, new()
        {
            Contract.Requires(someSource != null);
            TObject targetObject = new TObject();
            Type targetObjectType = typeof(TObject);

            // Go through all bound target object type properties...
            foreach (PropertyInfo property in
                        targetObjectType.GetProperties(bindingFlags))
            {
                // ...and check that both the target type property name and its type matches
                // its counterpart in the ExpandoObject
                if (someSource.ContainsKey(property.Name)
                    && property.PropertyType == someSource[property.Name].GetType())
                {
                    property.SetValue(targetObject, someSource[property.Name]);
                }
            }

            return targetObject;
        }
    }

ConfigureServices

var subSettings = Configuration.GetSection("IDP").Get<List<ExpandoObject>>();

var idx = 0;
foreach (var pair in subSettings)
{

    IDP scheme = ((ExpandoObject)pair).ToObject<IDP>();
    if (scheme.Format == "AzureAD")
    {
        // this is why I couldn't use an array, AddProtecedWebApi requires a path to a config section
        var section = $"IDP:{idx.ToString()}";
        services.AddProtectedWebApi(Configuration, section, scheme.Description);
        // ... do more stuff
        
    }
    idx++;
}

Solution 4 - C#

var settingsSection = config.GetSection["someSettings:subSettings"];
var subSettings = new List<string>;

foreach (var section in settingsSection.GetChildren())
{
    subSettings.Add(section.Value);
}

This should give you the values you need, stored in subSettings

Apologies for bringing up a semi-old thread. I had difficulty finding an answer as a good amount of methods are deprecated, like Get and GetValue. This should be fine if you only need a simple solution without the configuration binder. :)

Solution 5 - C#

In my case configuration

 services.Configure<List<ApiKey>>(Configuration.GetSection("ApiKeysList"));

wasn't loaded because the properties were read-only and there were no default constructor

//Not working

  public class ApiKey : IApiKey
    {
        public ApiKey(string key, string owner)
        {
            Key = key;
            OwnerName = owner;
        }
        public string Key { get;  }
        public string OwnerName { get;}
    } 

//Working

    public class ApiKey : IApiKey
    {
        public ApiKey(){}//Added default constructor
        public ApiKey(string key, string owner)
        {
            Key = key;
            OwnerName = owner;
        }
        public string Key { get; set; }        //Added set property
        public string OwnerName { get; set; }  //Added set property
    } 

Solution 6 - C#

Just getting the whole section will populate the List property; in a settings class.

services.Configure<Settings>(configuration.GetSection("Another:Some:Example"));

But... do remember that if defaults are set in the settings class for that List... that the configuration settings will be additive and so not overwriting the original values.

So these defaults will remain and so are really "no way you can delete them via any additional configuration"

 public List<string> NonEditableStuff { get; set; } = new() { "XYZ1", "LTOY3" };

Also, if you also have turned on the Ini file provider might be be handy to know that to specify the list there the keys do not really matter as long as they are unique, so it makes sense to keep the key and the values there the same to end up in the list.

[Another:Some:Example:NonEditableStuff]
value=value
whatever2=whatever2

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiondevlifeView Question on Stackoverflow
Solution 1 - C#Kirill RakhmanView Answer on Stackoverflow
Solution 2 - C#Victor HurdugaciView Answer on Stackoverflow
Solution 3 - C#Post ImpaticaView Answer on Stackoverflow
Solution 4 - C#Marc ShepherdView Answer on Stackoverflow
Solution 5 - C#Michael FreidgeimView Answer on Stackoverflow
Solution 6 - C#edelwaterView Answer on Stackoverflow