Joe Schmoe
Joe Schmoe

Reputation: 1768

Handling key names with periods in .Net Core AppSettings / configuration

Consider following appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",

  "NumberOfRetries": 5,
  "Option1": "abc",
  "Option2":  "def"

}

In order to read NumberOfRetries following class can be used successfully:

public class AppSettings
{
    public int NumberOfRetries { get; set; }
    public string Option1 { get; set; }
    public string Option2 { get; set; }
}

with following code in Startup.cs:

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        services.AddOptions();

        services.Configure<AppSettings>(Configuration);
    }

Now, let's say key name is Number.Of.Retries instead of NumberOfRetries- with periods in the middle.

How can the AppSetings class (or the approach itself) be modified to support that? Can't exactly put periods in property name.

Upvotes: 8

Views: 3636

Answers (3)

killswitch
killswitch

Reputation: 365

A new "ConfigurationKeyNameAttribute" was added in .NET 6 to solve this problem: https://github.com/dotnet/runtime/issues/36010

Upvotes: 4

Joe Schmoe
Joe Schmoe

Reputation: 1768

OK, I figured it out.

Ideally I would like to just use JsonPropertyName like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace WebAPICore
{
    public class AppSettings
    {
        [JsonPropertyName("Number.Of.Retries")]
        public int NumberOfRetries { get; set; }
        public string Option1 { get; set; }
        public string Option2 { get; set; }
    }
}

but it doesn't work. Why? Don't they use JSON parser for this?

So the solution I ended up with looks like this:

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        services.AddOptions();

        services.Configure<AppSettings>(Configuration); // This maps every key that matches existing property name

        services.PostConfigure<AppSettings>(appSettings =>// This maps keys where names don't match existing property names
        {
            appSettings.NumberOfRetries = Configuration.GetValue<int>("Number.Of.Retries");
        }); 
    }

Upvotes: 3

Patrick Magee
Patrick Magee

Reputation: 2989

I see your point, i did a quick look up, there is the ability to provide custom logic on how your Options are configured. I did a quick prototype...

void Main()
{
    string json = @"{
      ""Logging"": {    
        ""LogLevel"": {
        ""Default"": ""Information"",
          ""Microsoft"": ""Warning"",
          ""Microsoft.Hosting.Lifetime"": ""Information""

        }
      },
      ""AllowedHosts"": ""*"",
      ""Number.Of.Retries"":  5
    }";

    using (var doc = System.Text.Json.JsonDocument.Parse(json, new JsonDocumentOptions {  AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip } ))
    {
        using(var stream = new MemoryStream())
        {
            using (var writer = new Utf8JsonWriter(stream))
            {
                doc.WriteTo(writer);
                writer.Flush();
            }       

            stream.Position = 0;

            // Usable code here
            IConfigurationRoot configuration = new ConfigurationBuilder().AddJsonStream(stream).Build();

            var services = new ServiceCollection();

            services.AddOptions<AppSettings>();

            // There is an option to configure it manually here, if it does not fit the convention
            services.Configure<AppSettings>((options) =>
            {
                options.NumberOfRetries = configuration.GetValue<int>("Number.Of.Retries");
            });

            var container = services.BuildServiceProvider();

            using (var scope = container.CreateScope())
            {
                var appSettings = scope.ServiceProvider.GetRequiredService<IOptions<AppSettings>>();

                Console.WriteLine(appSettings.Value.NumberOfRetries);
            }
        }
    }
}

public class AppSettings
{
    public int NumberOfRetries { get; set; }
}  

If you have a specific pattern of settings, you can create a custom settings binder for your own "convention", i have provided a very basic sample, which handles the '.' in the settings.


void Main()
{
    string json = @"{
      ""Logging"": {    
        ""LogLevel"": {
        ""Default"": ""Information"",
          ""Microsoft"": ""Warning"",
          ""Microsoft.Hosting.Lifetime"": ""Information""

        }
      },
      ""AllowedHosts"": ""*"",
      ""Number.Of.Retries"":  5
    }";

    using (var doc = System.Text.Json.JsonDocument.Parse(json, new JsonDocumentOptions {  AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip } ))
    {
        using(var stream = new MemoryStream())
        {
            using (var writer = new Utf8JsonWriter(stream))
            {
                doc.WriteTo(writer);
                writer.Flush();
            }       

            stream.Position = 0;

            // Usable code here
            IConfigurationRoot configuration = new ConfigurationBuilder().AddJsonStream(stream).Build();

            var services = new ServiceCollection();
            services.AddOptions<AppSettings>(); 
            services.AddSingleton<IConfiguration>(configuration);
            services.ConfigureOptions<CustomConfigureOptions>();

            var container = services.BuildServiceProvider();

            using (var scope = container.CreateScope())
            {
                var appSettings = scope.ServiceProvider.GetRequiredService<IOptions<AppSettings>>();

                Console.WriteLine(appSettings);
            }
        }
    }
}

public class AppSettings
{
    public int NumberOfRetries { get; set; }
}

public class CustomConfigureOptions : IConfigureOptions<AppSettings>
{
    private readonly IConfiguration configuration; 

    public CustomConfigureOptions(IConfiguration configuration)
    {
        this.configuration = configuration;
    }

    public void Configure(AppSettings options)
    {
        foreach(var pair in configuration.AsEnumerable())
        {
            foreach(var property in typeof(AppSettings).GetProperties())
            {
                if (property.Name.Equals(pair.Key.Replace(".", "")))
                {
                    property.SetValue(options, configuration.GetValue(property.PropertyType, pair.Key));
                }
            }
        }
    }
}


Upvotes: 1

Related Questions