When developing .NET Web APIs, we almost always need to load some configuration to function properly. This can be database connection strings, email configuration, Azure blob storage configuration, etc.
That configuration can come from JSON files, Environment variables, secrets.json file, Azure Keyvault, and many other providers. We can also write our own configuration provider that will load configuration from a custom source.
If you load configuration from many sources and overriding them through the configuration pipeline, you found it hard to debug. You sometimes do not know where the configuration came from or what the value that the Web API will use is. That’s why I’ve written this simple configuration extension function in C# that prints the final configuration that came out of the configuration pipeline.
You normally have something like this to build a configuration from different sources:
1 2 3 4 5 6 7 |
Assembly appAssembly = Assembly.GetExecutingAssembly(); IConfiguration Configuration = new ConfigurationBuilder() .SetBasePath(Environment.CurrentDirectory) .AddJsonFile("application.json", optional: false, reloadOnChange: true) .AddUserSecrets(appAssembly, optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); |
There are two classes below: ConfigurationExtensions and ConfigurationInfo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
namespace Mjc.Extensions.Configuration { public static class ConfigurationExtensions { /// <summary> /// Process configuration at start up /// </summary> /// <param name="configuration">Collection of configuration</param> /// <param name="serviceName">Name of the service, which is used as a prefix of the configurations</param> /// <returns></returns> public static List<ConfigurationInfo> ProcessConfigurationProviders(this IConfigurationRoot configuration, string serviceName) { var logs = new List<ConfigurationInfo>(); if (string.IsNullOrEmpty(configuration["ASPNETCORE_ENVIRONMENT"])) { logs.Add(new ConfigurationInfo("ASPNETCORE_ENVIRONMENT", "not set", "EnvironmentVariable")); } else { logs.Add(new ConfigurationInfo("ASPNETCORE_ENVIRONMENT", configuration["ASPNETCORE_ENVIRONMENT"], "EnvironmentVariable")); var countEmptyVariables = 0; foreach (var pair in configuration.AsEnumerable() .Where(f => f.Key.StartsWith(serviceName)) .Where(f => !string.IsNullOrWhiteSpace(f.Value))) { var listOfProvidersForValue = new List<string>(); foreach (var provider in configuration.Providers) { if (provider.TryGet(pair.Key, out var tempValue)) { if (tempValue == pair.Value) { listOfProvidersForValue.Add(provider.ToString()); } } } logs.Add(new ConfigurationInfo(pair.Key, $"{pair.Value.Substring(0, 1)}***", listOfProvidersForValue.Last())); if (pair.Value == "<secret>") { countEmptyVariables++; } } if (countEmptyVariables > 0) { logs.Add(new ConfigurationInfo("!!! WARNING, Empty variables present !!!")); } } return logs; } /// <summary> /// Print configuration information to console /// </summary> /// <param name="configurationInfo"></param> public static void Print(this List<ConfigurationInfo> configurationInfo) { Console.WriteLine(new string('-', 160)); Console.WriteLine($"{"Configuration key",-80}{"Configuration provider",-70}{"Masked value",-10}"); Console.WriteLine(new string('-', 160)); configurationInfo.ForEach(item => Console.WriteLine(item.ToString())); } } public class ConfigurationInfo { public ConfigurationInfo(string key) { Key = key; Value = ""; Provider = ""; } public ConfigurationInfo(string key, string value, string provider) { Key = key; Value = value; Provider = provider; } public string Key { get; } public string Value { get; } public string Provider { get; } public override string ToString() { return string.Format("{0,-80}{1,-70}{2,-10}", Key, Provider, Value); } } } |
As you can see, we take IConfiguration and service name as an input parameter in the ProcessConfigurationProviders and then construct a flat list of configurations from all the providers. We can extend this by filtering out only specific configurations, printing only in specific environments, or mask the output values (remember configuration can also contain secrets, and you do not want those in logs).
Example of calling the code above.
1 2 |
var validationLogs = configuration.ProcessConfigurationProviders(nameof(MyService)); validationLogs.Print(); |
And this is the output. You get a table of configurations, their flat names, from which provider it came from, and the masked value, so we do not expose them in the logs files. This was run on my development machine. Now imagine that you are running this application in the Azure cloud and you use Azure KeyVault. You will immediately see where the specific secret is coming from in case you need to debug the application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Loading settings from appsettings.json... Loading from secrets.json... Loading environment variables... ---------------------------------------------------------------------------------------------------------------------------------------------------------- Configuration key Configuration provider Masked value ---------------------------------------------------------------------------------------------------------------------------------------------------------- ASPNETCORE_ENVIRONMENT EnvironmentVariable Development MyService:Swagger:Username JsonConfigurationProvider for 'appsettings.json' (Required) <*** MyService:Swagger:Password JsonConfigurationProvider for 'appsettings.json' (Required) <*** MyService:Serilog:MinimumLevel:Override:System JsonConfigurationProvider for 'appsettings.json' (Required) W*** MyService:Serilog:MinimumLevel:Override:Microsoft JsonConfigurationProvider for 'appsettings.json' (Required) I*** MyService:Serilog:MinimumLevel:Default JsonConfigurationProvider for 'appsettings.json' (Required) I*** MyService:Metrics:Username JsonConfigurationProvider for 'secrets.json' (Optional) m*** MyService:Metrics:Password JsonConfigurationProvider for 'secrets.json' (Optional) m*** MyService:ApiKey JsonConfigurationProvider for 'secrets.json' (Optional) 1*** MyService:AllowedHosts JsonConfigurationProvider for 'appsettings.json' (Required) **** !!! WARNING, Empty variables present !!! |
I hope your configuration debugging got just a bit easier :). I might also pack this into a NuGet package and extend it a bit. There is a lot of room for improvement.
Ta-Da!
Leave a Reply
You must belogged in to post a comment.