.Net Core appsettings.json best practices – override dev settings (or vice versa)?

UPDATE: I wanted to append to this that I no longer use Azure App Configuration, only the KeyVault. I was disappointed that the pricing allows for 1 free AppConfig per subscription, and then appears to charge more than 1.20$CDN a day for each beyond the first. Which for me meant an extra 36$ a month for something that will only be used once a day (assuming I let the web app wind down at night). The price point just doesn’t make any sense whatsoever. I’d just as soon use multiple KeyVault, as their cost looks to be purely based on usage, and at a pittance per thousands of transactions. I don’t understand what the intended use case for AppConfig is to justify its price point.


I’ve gotten to the habit storing my configuration in Azure under an AzureAppConfig and/or an AzureKeyVault. It gives me a central location to manage my dev, staging/test, production settings to and doesn’t require me to complicate my deployment with manipulating appsettings files, or storing in them in some sort of deployment repo. It’s really only ever read from azure when the application starts (I didn’t need to be able to refresh them while my app was running). That being said, it made it a little interesting for the local dev story because I personally wanted the order of operations to be appsettings.json, appsettings.{environment}.json, AzureAppConfig, KeyVault, then finally secrets.json. That way, no matter what, I could override a setting from azure with my local secrets file (even if the setting I was overriding wasn’t technically a secret).

I basically ended up writing some custom code in program.cs to handle loading the config sources from Azure, then finish with looking for the JsonConfigurationSource that had a Path of "secrets.json", then bump that to be the last item in my IConfigurationBuilder.Sources.

For me, my files get used as follows

  • appsettings.json – Common settings that would need to be set for any environment, and will likely never change depending on the environment.
    appsettings.{environment}.json – Mostly just en empty JSON files that basically just name the AzureAppConfig & AzuerKeyVault resource names to connect to
  • AzureAppConfig – Basically for anything that would be different between Production, Staging/Testing, or local Development, AND is not a sensitive piece of information. API endpoints addresses, IP addresses, various URLs, error logging information, that sort of thing.
  • AzureKeyVault – Anything sensitive. Usernames, passwords, keys for external APIs (auth, license keys, connection strings, etc).

The thing is, even if you put a setting in appsettings.json, that doesn’t mean you can’t override it with appsettings.{enviroment}.json or somewhere else. I’ve frequently put a settings in the root setting file with a value of NULL, just to remind me that it’s a setting used in the app. So a better question might be, do you want to be able to run your app (as in no errors) with nothing but the base appsettings.json and secrets.json? Or would the contents from appsettings.{enviroment}.json always be needed to successfully spin up?

The other thing to look at based off of your question is validation for your configuration. Later versions of Microsoft.Extensions.Options offer various ways to validate your options so that you can try and catch instances where something was left empty/undefined. I typically decorate my POCO Options classes with data annotation attributes and then use ValidateDataAnnotations() to verify they get setup correctly.

For example

services.AddOptions<MailOptions>().Bind(configuration.GetSection("MailSettings")).ValidateDataAnnotations();

It’s worth noting this validation runs only when you try to request something like the MailOptions I use as an example above, from DI (so not at startup)
For this reason, I also created your my own IStartupFilter to preemptively request one or more of my Options classes from the service provider when the app starts up, in order to force that same Validation to run before the app even starts accepting requests.

public class EagerOptionsValidationStartupFilter : IStartupFilter
{
    public readonly ICollection<Type> EagerValidateTypes = new List<Type>();
    private readonly IServiceProvider serviceProvider;

    public EagerOptionsValidationStartupFilter(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        foreach (var eagerType in EagerValidateTypes)
        {
            dynamic test = serviceProvider.GetService(typeof(IOptions<>).MakeGenericType(eagerType));
            _ = test.Value;
        }

        return next;
    }
}

startup.cs

public void ConfigureServices(IServiceCollection services)
{

    services.AddTransient<IStartupFilter>(x =>
        new EagerOptionsValidationStartupFilter(x)
        {
            EagerValidateTypes = {
                typeof(MailOptions),
                typeof(OtherOptions),
                typeof(MoreImportantOptions)
            }
        });
}

Leave a Comment