Share config files between Web, Console and other types of .NET Core and .NET 6+ projects
Microsoft has always provided “pretty good” guidance on how to support multiple environment configs in a single project. Well mostly if it is an ASP.NET or ASP.NET Core project. For other types of projects you are usually on your own, but it is not very difficult.
Where it gets a bit hairy, is when you have several projects in the same solution and you need to share config values between those projects. Most nontrivial applications consist of the following projects:
In almost all of these projects, except library and unit tests, you need configuration files that usually have a lot of shared values, like Connection Strings, integration keys to 3rd party systems like Google Apps, Salesforce, and many others.
So how do you keep shared configs in one place, as opposed to duplicating them to every project and having to remember to modify all of them whenever there is a change.
Demo Project
Here is a link to a demo solution on GitHub that has all the code that I will go over in this article:
The solution consists of common project types that are usually included in any non-trivial applications. You can see what they do from their names:
ShareConfigsDemo.Console
ShareConfigsDemo.Core.IntegrationTests
ShareConfigsDemo.Core - library with all Business Logic
ShareConfigsDemo.Web - web app
So let’s get into the meat of things, or into soybean patties for our vegetarian readers.
Shared Config Linkage
There are several steps to allow sharing configs in your solution
Create Shared Config Folder
In this section, we will create one config folder that will hold all our shared config files.
ShareConfigDemo.sln
\Config
appsettings.json
appsettings.Development.json
appsettings.IntegrationTests.json
appsettings.Staging.json
appsettings.Production.json
\ShareConfigDemo.Web
ShareConfigDemo.Web.csproj
..
4. Open solution in visual studio
5. Right-click on Solution in “Solution Explorer”, click on “Add” and “New Solution Folder”
6. Name this new folder “Config” or another name that suits you better
7. Right-click on the new “Config” folder and select Add -> Existing Item…
8. In the popup go to your <solution dir>/Config folder, select all the config files in that folder (by selecting the first file, holding down “shift” and selecting the last file) and click on the “Add” button.
Now we have our main Config folder accessible from Visual studio so that we can easily modify shared config files. Next, let’s share these files with all the projects that need configs in this solution.
Link config files in every project that needs?them
For sharing files we use the file linking feature in Visual studio.
2. To link appsettings.config from the solution’s Config folder, right-click on the project’s “Config” folder, select the “Add” menu and then press on “Existing Item…”
3. In the “Add Existing Item” popup select the solution’s Config directory and all the config files that you would like to share (using the Shift trick from before).
IMPORTANT: Instead of pressing the “Add” button, click on a little drop-down next to “Add” and select “Add As Link” instead.
4. After you click on “Add as Link” you will see the files in Project’s Config folder in Visual Studio, but in Window’s explorer the actual physical folder will be empty.
Under a magnifying glass you can also see a little icon with a small arrow next to config files, that signifies that they are linked:
5. Click on each file in Project’s Config folder and in the Properties window, set “Copy to Output Directory” to “Copy if newer”:
6. Do the same 1–5 steps for all other projects in a solution that need to share configs like Console, IntegrationTests, Web, etc.
Now let’s test that appropriate config settings are loaded in each project type.
WARNING: Don’t stop reading just yet. For some of the project types, it will work out of the box, but for Web App, it will not. Follow this True Crime below.
Integration Tests
Let’s start with the IntegrationTests project. I am going to test that depending on Environment, the value of “Key1” will be different. Each env has a different value for “Key1”. For ex:
in appsettings.Development.Json
{
"Key1": "DevelopmentValue1"
}
in appsettings.Staging.Json
{
"Key1": "StagingValue1"
}
and so on...
To load Config I will use the Load() method of ConfigHelper.cs in ShareConfigsDemo.Core
public class ConfigHelper
{
public static IConfigurationRoot Load(Env? env = null)
{
var configurationBuilder = new ConfigurationBuilder();
AddJsonFiles(configurationBuilder, env);
return configurationBuilder.Build();
}
public static void AddJsonFiles(IConfigurationBuilder configurationBuilder, Env? env = null)
{
if (!env.HasValue)
env = EnvHelper.GetEnvironment();
configurationBuilder
.AddJsonFile($"Config/appsettings.json")
.AddJsonFile($"Config/appsettings.{env}.json");
}
}
The method ConfigHelper.Load takes optional environment variables which can be:
public enum Env
{
Development,
IntegrationTests,
QA,
Staging,
Production
}
If Env is not passed we get it using EnvHelper
public static class EnvHelper
{
public static Env GetEnvironment()
{
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
ArgumentNullException.ThrowIfNull(environmentName);
return (Env)Enum.Parse(typeof(Env), environmentName);
}
}
This method gets the environment from Environment Variable “ASPNETCORE_ENVIRONMENT” which is a common way to get the current env in MS Projects. Yes, there is also “DOTNET_ENVIRONMENT” which is a bit better since not every app is ASPNET, so feel free to change it if it prevents you from having a good night's sleep.
The NUnit test looks like this:
[Test]
[TestCase(Env.Development, "DevelopmentValue1")]
[TestCase(Env.IntegrationTests, "IntegrationTestsValue1")]
[TestCase(Env.Staging, "StagingValue1")]
[TestCase(Env.Production, "ProductionValue1")]
public void Load_ValidEnv_Key1ValueIsBasedOnEnv(Env env, string key1Value)
{
var config = ConfigHelper.Load(env);
Assert.AreEqual(key1Value, config.GetSection("Key1").Value);
}
Easy breezy, no?
Let’s run it… And SURPRISE! It works.
Console Project
For Console Project, we will get Environment from the “ASPNETCORE_ENVIRONMENT” environment variable and display the value of Key1 in the console window.
To easily switch between environments locally I will add all four environments as debug profiles.
Let’s right-click on Project (SharedConfigDemo.Console) in Solution Explorer and select properties. In properties, window select Debug and click on “Open debug launch profiles UI.
领英推荐
There is also a shortcut to “Debug Profiles UI” here:
“Launch Profiles” popup looks like this:
Rename the default profile to “Development” and add “ASPNETCORE_ENVIRONMENT=Development” in the Environment variables text box:
Now click on “Add Profile” and add profiles for any other environment that you would like to be able to use in your console application. Let’s add “Staging”. I usually just duplicate the “Development” profile to keep most of the settings the same, then rename it and change the Env variable:
Make sure you save the project after adding profiles because sometimes it doesn’t auto-save.
Now let’s add some code to display the value of “Key1” when the console app is executed:
using ShareConfigsDemo.Core;
var config = ConfigHelper.Load();
var key1Value = config.GetSection("Key1").Value;
Console.WriteLine($"Key1: {key1Value}");
Pretty straightforward.
Now I will select the console app as the startup project and select “Development” as the active profile:
So if I debug now I should see “DevelopmentValue1” in the console… Lo and behold:
And now if I change the Active Debug profile to Staging, I should see “StagingValue1”:
At this point, I am ready to mic drop and leave the stage, but we still got Web App Project to do.
ASP.NET Core Web App?Project
For the Web Project let’s start by adding debug profiles for Development and Staging the same way as we did for the Console app.
By default “ShareConfigsDemo.Web” already has “ASPNETCORE_ENVIRONMENT=Development” in env variables, so I just need to change the name to “Development” and add the “Staging” profile.
Let’s add the following code to IndexModel class in Index.cshmtl.cs:
public string? Key1Value { get; set; }
public void OnGet()
{
var config = ConfigHelper.Load();
Key1Value = config.GetSection("Key1").Value;
}
And in Index.cshmtl:
<div class="text-center">
<h1 class="display-4">Welcome</h1>
...
<p>Key1 Value: @Model.Key1Value</p>
...
</div>
Let’s debug with the “Development” profile and see if it works.
It does!
If you change Profile to Staging, you will see “StagingValue1”.
So everything is hunky-dory you say, let’s finish this up. “Not so fast”, I reply.
In most cases, you will load Configuration on Web App Startup. Let’s just double-check that it works just to be sure sure.
Web App Config Strikes?Back
In?.NET 6, the boilerplate startup looks like this:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
//bla bla bla ...
app.Run();
By default it will load configuration to the app’s Configuration from default appsettings.config and appsettings.<env>.config based on ASPNETCORE_ENVIRONMENT or DOTNET_ENVIRONMENT.
So let’s try displaying value of Key1 based on the app’s Configuration. We will add web app’s config using the standard DI approach and then display Key1 Value both ways: using ConfigHelper and using the app’s Configuration.
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private IConfigurationRoot _configRoot;
public IndexModel(ILogger<IndexModel> logger,
IConfiguration configRoot)
{
_logger = logger;
_configRoot = (IConfigurationRoot)configRoot;
}
public string? Key1ValueFromConfigHelper { get; set; }
public string? Key1ValueFromAppConfig { get; set; }
public void OnGet()
{
var config = ConfigHelper.Load();
Key1ValueFromConfigHelper = config.GetSection("Key1").Value;
Key1ValueFromAppConfig= _configRoot.GetSection("Key1").Value;
}
}
Also, let’s change HTML a bit to display both values
<p>Key1 Value from ConfigHelper: @Model.Key1ValueFromConfigHelper</p>
<p>Key1 Value from AppConfig: @Model.Key1ValueFromAppConfig</p>
Now, F5 to start the debugger:
As before, Key1 Value from ConfigHelper still works, but the value from AppConfig is empty for some reason.
It happens because the ASP.NET Core app looks for configs only at the root of web application, while our linked configs get copied to /bin/…/Config folder during build:
The trick to make Config work for web apps is to add the following code to Web App Startup:
var builder = WebApplication.CreateBuilder(args);
...
builder.Configuration.SetBasePath(DirHelper.GetBinRunDir());
....
var app = builder.Build();
Where DirHelper looks like this:
public static class DirHelper
{
public static string GetBinRunDir()
{
return Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
}
}
This method returns the bin directory where all web app’s dlls reside. So now when loading configuration WebApp Builder will look in /bin directory instead of web app root.
But this is not enough.. We also need to indicate to WebApp Builder that we need to load files from /Config sub-dir. To do that, add a call to ConfigHelper.AddJsonFiles:
var builder = WebApplication.CreateBuilder(args);
...
builder.Configuration.SetBasePath(DirHelper.GetBinRunDir());
ConfigHelper.AddJsonFiles(builder.Configuration);
....
var app = builder.Build();
AddJsonFiles method does what it says but with /Config folder:
public static void AddJsonFiles
(IConfigurationBuilder configurationBuilder, Env? env = null)
{
if (!env.HasValue)
env = EnvHelper.GetEnvironment();
configurationBuilder
.AddJsonFile($"Config/appsettings.json")
.AddJsonFile($"Config/appsettings.{env}.json");
}
Let’s debug again:
Much better. And if we switch to Staging debug profile:
Conclusion
In this article, we went over a “linked file” approach to sharing config files between various project types available in?.NET6+.
There is a more compact way to share the config files without all the linking. We will go over it in the next blog post. So subscribe and stay tuned…
You can find all the code mentioned in this article here: