A pattern that's common for some apps is the need to share settings across multiple projects. For example, imagine you have both an ASP.NET Core RazorPages app and an ASP.NET Core Web API app in the same solution:

Image of project structure

Each of the apps will have its own distinct configuration settings, but it's likely that there will also be settings common to both, like a connection string or logging settings for example.

Sensitive configuration settings like connection strings should only be stored outside the version control repository (for example in UserSecrets or Environment Variables) but hopefully you get the idea.

Rather than having to duplicate the same values in each app's appsettings.json, it can be useful to have a common shared .json file that all apps can use, in addition to their specific appsettings.json file.

In this post I show how you can extract common settings to a SharedSettings.json file,how to configure your projects to use them both when running locally with dotnet run, and how to handle the the issues that arise after you publish your app!

The initial setup

If you create a new ASP.NET Core app from a template, it will use the WebHost.CreateDefaultBuilder(args) helper method to setup the web host. This uses a set of "sane" defaults to get you up and running quickly. While I often use this for quick demo apps, I prefer to use the long-hand approach to creating a WebHostBuilder in my production apps, as I think it's clearer to the next person what's going on.

As we're going to be modifying the ConfigureAppConfiguration call to add our shared configuration files, I'll start by modifying the apps to use the long-hand WebHostBuilder configuration. This looks something like the following (some details elided for brevity)

public class Program  
{
    public static void Main(string[] args) => BuildWebHost(args).Run();

    public static IWebHost BuildWebHost(string[] args) =>
        new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                // see below
            })
            .ConfigureLogging((ctx, log) => { /* elided for brevity */ })
            .UseDefaultServiceProvider((ctx, opts) => { /* elided for brevity */ })
            .UseStartup<Startup>()
            .Build();
}

We'll start by just using the standard appsettings.json files, and the environment-specific appsettings.json files, just as you would in a default ASP.NET Core app. I've included the environment variables in there as well for good measure, but it's the JSON files we're interested in for this post.

.ConfigureAppConfiguration((hostingContext, config) =>
{
    var env = hostingContext.HostingEnvironment;

    config.AddJsonFile("appsettings.json", optional: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

    config.AddEnvironmentVariables();
})

To give us something to test, I'll add some configuration values to the appsettings.json files for both apps. This will consist of a section with one value that should be the same for both apps, and one value that is app specific. So for the Web API app we have:

{
    "MySection": {
        "SharedValue": "This value is shared across both apps",
        "AppSpecificValue": "Value for Api"
    }
}

while for the Razor app we have:

{
    "MySection": {
        "SharedValue": "This value is shared across both apps",
        "AppSpecificValue": "Value for Razor app"
    }
}

Finally, so we can view the actual values received by the app, we'll just dump the configuration section to the screen in the Razor app with the following markup:

@page
@using Microsoft.Extensions.Configuration
@inject IConfiguration _configuration;

@foreach (var kvp in _configuration.GetSection("MySection").AsEnumerable())
{
    <p>@kvp.Key : @kvp.Value</p>
}

which, when run, gives

Razor app showing configuration section

With our apps primed and ready, we can start extracting the common settings to a shared file.

Extracting common settings to SharedSettings.json

The first question we need to ask is where are we going to actually put the shared file? Logically it doesn't belong to either app directly, so we'll move it outside of the two app folders. I created a folder called Shared at the same level as the project folders:

Folder structure

Inside this folder I created a file called SharedSettings.json, and inside that I added the following JSON:

{
    "MySection": {
        "SharedValue": "This value is shared across both apps",
        "AppSpecificValue": "override me"
    }
}

Note, I added an AppSpecificValue setting here, just to show that the appsettings.json files will override it, but you could omit it completely from SharedSettings.json if there's no valid default value.

I also removed the SharedValue key from each app's appsettings.json file - the apps should use the value from SharedSettings.json instead. The appsettings.json file for the Razor app would be:

{
    "MySection": {
        "AppSpecificValue": "Value for Razor app"
    }
}

If we run the app now, we'll see that the shared value is no longer available, though the AppSpecificValue from appsettings.json is still there:

Setting missing

Loading the SharedSettings.json in ConfigureAppConfiguration

At this point, we've extracted the common setting to SharedSettings.json but we still need to configure our apps to load their configuration from that file as well. That's pretty straight forward, we just need to get the path to the file, and add it in our ConfigureAppConfiguration method, right before we add the appsettings.json files:

.ConfigureAppConfiguration((hostingContext, config) =>
{
    var env = hostingContext.HostingEnvironment;

    // find the shared folder in the parent folder
    var sharedFolder = Path.Combine(env.ContentRootPath, "..", "Shared");

    //load the SharedSettings first, so that appsettings.json overrwrites it
    config
        .AddJsonFile(Path.Combine(sharedFolder, "SharedSettings.json"), optional: true)
        .AddJsonFile("appsettings.json", optional: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

    config.AddEnvironmentVariables();
})

Now if we run our app again, the setting's back:

The setting is back

Great, it works!

Or does it?

While this works fine in development, we'll have a problem when we publish and deploy the app. The app is going to be looking for the SharedSettings.json file in a parent Shared folder, but that won't exist when we publish - the SharedSettings.json file isn't included in any project files, so as it stands you'd have to manually copy the Shared folder across when you publish. Yuk!

Publishing the SharedSettings.json file with your project.

There's a number of possible solutions to this problem. The one I've settled on isn't necessarily the best or the most elegant, but it works for me and is close to an approach I was using in ASP.NET.

To publish the SharedSettings.json file with each app, I create a link to the file in each app as described in this post, and set the CopyToPublishDirectory property to Always. That way, I can be sure that when the app is published, the SharedSettings.json file will be there in the output directory:

Publish directory

However, that leaves us with a different problem. The SharedSettings.json file will be in a different place depending on if you're running locally with dotnet run (in ../Shared) or the published app with dotnet MyApp.Api.dll (in the working directory).

This is where things get a bit hacky.

For simplicity, rather than trying to work out in which context the app's running (I don't think that's directly possible), I simply try and load the file from both locations - one of them won't exist, but as long as we make the files "optional" that won't be an issue:

.ConfigureAppConfiguration((hostingContext, config) =>
{
    var env = hostingContext.HostingEnvironment;

    var sharedFolder = Path.Combine(env.ContentRootPath, "..", "Shared");

    config
        .AddJsonFile(Path.Combine(sharedFolder, "SharedSettings.json"), optional: true) // When running using dotnet run
        .AddJsonFile("SharedSettings.json", optional: true) // When app is published
        .AddJsonFile("appsettings.json", optional: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

    config.AddEnvironmentVariables();
})

It's not a particularly elegant solution, but it does the job for me. With the code in place we can now happily share settings across multiple apps, override them with app-specific values, and have the correct behaviour both when developing and after publishing.

Summary

This post showed how you can use a shared configuration file to share settings between multiple apps in a solution. By storing the configuration in a central JSON file accessible by both apps, you can avoid duplicating settings in appsettings.json.

Unfortunately this solution is a bit hacky due to the need to cater to the file being located at two different paths, depending on whether the app has been published or not. If anyone has a better solution, please let me know in the comments!

The sample code for this post can be found on GitHub.