devlife
devlife

Reputation: 16145

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

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?

Upvotes: 147

Views: 143197

Answers (7)

Post Impatica
Post Impatica

Reputation: 16373

Updated

Better solution is now here

MS talking about in in .net 8 here

Old Answer

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<List<IDP>>();

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++;
}

Upvotes: 71

Ogglas
Ogglas

Reputation: 69928

You can use this with options pattern as well.

{
  "Foo": {
    "Bar": [
      "1",
      "2",
      "3"
    ]
  }
}

Class:

public class Foo
{
    public required List<string> Bar { get; set; }
}

Program.cs:

builder.Services.Configure<Foo>(builder.Configuration.GetSection("Foo"));

Usage:

public class Test2Model : PageModel
{
    private readonly Foo _options;

    public Test2Model(IOptions<Foo> options)
    {
        _options = options.Value;
    }
}

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-8.0#the-options-pattern

Upvotes: 0

Michael Freidgeim
Michael Freidgeim

Reputation: 28425

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

In other case a class had public fields instead of properties.

//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
    } 

Upvotes: 7

edelwater
edelwater

Reputation: 2802

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

Upvotes: 0

Marc Shepherd
Marc Shepherd

Reputation: 162

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. :)

Upvotes: 14

Victor Hurdugaci
Victor Hurdugaci

Reputation: 28425

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

Upvotes: 54

Kirill Rakhman
Kirill Rakhman

Reputation: 43791

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>>()

Upvotes: 251

Related Questions