Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Microsoft.Extensions.Configuration binding dictionary with colons in key

I am having trouble using Microsoft.Extensions.Configuration to bind a dictionary that contains keys that have colon : in them.

I have done an example that has a dictionary "GoodErrorMappings" which do not contain any colon in the key. These are mapped correctly.

I have created another dictionary "BadErrorMappings" that has a colon in the key. This dictionary seems to not be mapped correctly after it sees the first colon in the dictionary key.

I have had a quick look in the source and cannot see an obvious way to override the colon as a delimiter.

Any help would be appreciated.

Doco: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration

Assembly version: "Microsoft.NETCore.App" "1.1.0"

C# Code:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;

namespace OptionsTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json");

            var config = builder.Build();

            var services = new ServiceCollection().AddOptions();

            services.Configure<ApiConfig>(x => config.GetSection("ApiConfig").Bind(x));

            var apiConfig = services.BuildServiceProvider().GetService<IOptions<ApiConfig>>().Value;

            Debug.WriteLine(string.Format("\r\nGoodErrorMappings: {0}", JsonConvert.SerializeObject(apiConfig.GoodErrorMappings, Formatting.Indented)));
            Debug.WriteLine(string.Format("\r\nBadErrorMappings: {0}", JsonConvert.SerializeObject(apiConfig.BadErrorMappings, Formatting.Indented)));

            Console.ReadLine();
        }
    }

    public class ApiConfig
    {
        public Dictionary<string, ErrorMapping> GoodErrorMappings { get; set; } = new Dictionary<string, ErrorMapping>();
        public Dictionary<string, ErrorMapping> BadErrorMappings { get; set; } = new Dictionary<string, ErrorMapping>();
    }

    public class ErrorMapping
    {
        public int HttpStatusCode { get; set; }
        public int ErrorCode { get; set; }
        public string Description { get; set; }
    }
}

AppSettings.json:

{
  "ApiConfig": {
    "GoodErrorMappings": {
      "/SOMEVALUE/BLAH.123": {
        "httpStatusCode": "500",
        "errorCode": "110012",
        "description": "Invalid error description 1"
      },
      "/SOMEVALUE/BLAH.456": {
        "httpStatusCode": "500",
        "errorCode": "110013",
        "description": "Invalid error description 2"
      }
    },
    "BadErrorMappings": {
      "/SOMEVALUE/BLAH:123": {
        "httpStatusCode": "500",
        "errorCode": "110012",
        "description": "Invalid error description 1"
      },
      "/SOMEVALUE/BLAH:456": {
        "httpStatusCode": "500",
        "errorCode": "110013",
        "description": "Invalid error description 2"
      }
    }
  }
}

Output:

GoodErrorMappings: {
  "/SOMEVALUE/BLAH.123": {
    "HttpStatusCode": 500,
    "ErrorCode": 110012,
    "Description": "Invalid error description 1"
  },
  "/SOMEVALUE/BLAH.456": {
    "HttpStatusCode": 500,
    "ErrorCode": 110013,
    "Description": "Invalid error description 2"
  }
}

BadErrorMappings: {
  "/SOMEVALUE/BLAH": {
    "HttpStatusCode": 0,
    "ErrorCode": 0,
    "Description": null
  }
}
like image 661
duyker Avatar asked Dec 21 '16 09:12

duyker


1 Answers

The reason for what is happening is the colon has special meaning in configuration binding.

The colon can be used to identify collections when provided within a key string. I've demonstrated it in the following code change to your sample app. I also updated your BadErrorMappings to be an array in your binding since that is what the colon separator is doing.

program.cs

public class Program
    {
        public static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json");

            var config = builder.Build();

            var services = new ServiceCollection().AddOptions();

            services.Configure<ApiConfig>(x => config.GetSection("ApiConfig").Bind(x));
            services.Configure<Fruit>(x => config.GetSection("Fruit").Bind(x));

            var serviceProvider = services.BuildServiceProvider();
            var apiConfig = serviceProvider.GetService<IOptions<ApiConfig>>().Value;
            var fruit = serviceProvider.GetService<IOptions<Fruit>>().Value;

            Console.WriteLine(string.Format("\r\nGoodErrorMappings: {0}", JsonConvert.SerializeObject(apiConfig.GoodErrorMappings, Formatting.Indented)));
            Console.WriteLine(string.Format("\r\nBadErrorMappings: {0}", JsonConvert.SerializeObject(apiConfig.BadErrorMappings, Formatting.Indented)));
            Console.WriteLine(string.Format("\r\nFruit: {0}", JsonConvert.SerializeObject(fruit, Formatting.Indented)));

            Console.ReadLine();
        }
    }

    public class Fruit : List<string>
    {
    }

    public class ApiConfig
    {
        public Dictionary<string, ErrorMapping> GoodErrorMappings { get; set; } = new Dictionary<string, ErrorMapping>();
        public Dictionary<string, ErrorMapping[]> BadErrorMappings { get; set; } = new Dictionary<string, ErrorMapping[]>();
    }

    public class ErrorMapping
    {
        public int HttpStatusCode { get; set; }
        public int ErrorCode { get; set; }
        public string Description { get; set; }
    }

appsettings.json

{
  "ApiConfig": {
    "GoodErrorMappings": {
      "/SOMEVALUE/BLAH.123": {
        "httpStatusCode": "500",
        "errorCode": "110012",
        "description": "Invalid error description 1"
      },
      "/SOMEVALUE/BLAH.456": {
        "httpStatusCode": "500",
        "errorCode": "110013",
        "description": "Invalid error description 2"
      }
    },
    "BadErrorMappings": {
      "/SOMEVALUE/BLAH:123": {
        "httpStatusCode": "500",
        "errorCode": "110012",
        "description": "Invalid error description 1"
      },
      "/SOMEVALUE/BLAH:456": {
        "httpStatusCode": "500",
        "errorCode": "110013",
        "description": "Invalid error description 2"
      }
    }
  },
  "Fruit:0": "Apple",
  "Fruit:1": "Orange" 
}

You can see the aspnet team leveraging this within their unit tests as well so this is intended behavior.

Example: https://github.com/aspnet/Configuration/blob/dev/test/Config.Binder.Test/ConfigurationCollectionBindingTests.cs#L396-L423

like image 119
Dave Avatar answered Nov 14 '22 23:11

Dave