Appsettings.json Files

The configuration system in ASP.NET Core is a flexible way to manage settings, with each of the different providers made available to apps as a unified IConfiguration object. Under normal circumstances adding settings to appsettings.json and environment specific appsettings files is an easy way to configure an app being deployed as a container or set of files on a web server, while also being flexible at development time. In other cases an app needs to be deployed as a single package with a fixed set of configuration settings included that shouldn’t be editable by end users. Often these types of apps can be configured using hardcoded settings in the app code, but this misses out on the benefits of environment specific sets of settings, as well as being a different pattern to think about when you’re used to building configuration in JSON.

By using appsettings files and then embedding them in an assembly, you can get the benefits of both the normal configuration file experience, but with fixed settings contained in a single file deployment. Using this method also allows for embedding appsettings along with associated code in class libraries, and then making those settings available in any app referencing the library.

Embedding Files

The first step in modifying a normal project to embed settings is to change how the appsettings files are handled during build. By default, appsettings files are added to a project as Content, with a Copy if newer setting to ensure that the files are available alongside the compiled output so that they can be loaded at runtime. These can be seen in the Properties window in Visual Studio as the Build Action and Copy to Output Directory settings. To instead embed the files in the compiled output, the Build Action should be changed to Embedded resource, which should also change the other setting to Do not copy, because there is no longer a need for the loose file to be placed with the compiled output. In the project file, this looks like this:

<ItemGroup>
  <Content Remove="appsettings.Development.json" />
  <Content Remove="appsettings.json" />
</ItemGroup>

<ItemGroup>
  <EmbeddedResource Include="appsettings.Development.json" />
  <EmbeddedResource Include="appsettings.json" />
</ItemGroup>

Loading Embedded Files

Now that the files are embedded they need to be accessed differently that the normal loose files when loading configuration at startup. Embedded resources can be read as streams from the containing assembly, using a combination of the assembly name and relative path of the file within the project (notice the use of . instead of / as path separators).

Note: Resource names can be found by calling GetManifestResourceNames on an assembly

The default configuration setup uses the AddJsonFile extension method on IConfigurationBuilder to load appsettings.json files, but there is also an AddJsonStream method which does the same thing with streams like that provided from embedded files. This can all be combined into a new extension method.

public static void AddEmbeddedJsonFile(this IConfigurationBuilder configuration, string resourceName)
{
    var host = Assembly.GetEntryAssembly();
    if (host == null)
        return;

    var fullFileName = $"{host.GetName().Name}.{resourceName}";
    using var input = host.GetManifestResourceStream(fullFileName);
    if (input != null)
    {
        configuration.AddJsonStream(input);
    }
}

The use of GetEntryAssembly should be appropriate for loading settings in the main startup project but for a class library a different method of assembly loading should be used, for example: loading based on a type in the class library, here as a generic parameter.

public static void AddEmbeddedJsonFile<T>(this IConfigurationBuilder configuration, string resourceName)
{
    var host = Assembly.GetAssembly(typeof(T));
    if (host == null)
        return;

    var fullFileName = $"{host.GetName().Name}.{resourceName}";
    using var input = host.GetManifestResourceStream(fullFileName);
    if (input != null)
    {
        configuration.AddJsonStream(input);
    }
}

ConfigurationManager Setup

With the new extension method the normal pattern for setting up configuration sources on WebApplicationBuilder can be used.

builder.Configuration.AddEmbeddedJsonFile("appsettings.json");

This should be sufficient to get your embedded file loaded as a configuration provider but depending on the needs of your specific app you may want to have more control over which other providers are added. Doing just the above will still keep the default loading of appsettings.json files in place, which may be confusing if you happen to accidentally include one in your deployment, or an end user of a packaged applications changes settings that you did not intend to allow. For more control you may want to remove the default providers and only allow a subset to be loaded. This can be done with another extension method.

public static WebApplicationBuilder ConfigureAsSelfContained(this WebApplicationBuilder builder, string[] args)
{
    builder.Configuration.Sources.Clear();
    builder.Configuration.AddEmbeddedJsonFile("appsettings.json");
    builder.Configuration.AddEmbeddedJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json");
    builder.Configuration.AddEnvironmentVariables();
    builder.Configuration.AddCommandLine(args);

    return builder;
}

You may want to include environment variables and command line arguments, or not, depending on specific requirements and keeping in mind that depending on order of providers these can allow users executing the code to override any IConfiguration value.

Publishing

The result of these changes is that now the dotnet publish option -p:PublishSingleFile=true can now truly output a single executable file without risk of losing configuration that lives in separate files.

Depending on the type of app you may need additional publish options like -p:PublishIISAssets or -p:IncludeNativeLibrariesForSelfExtract to prevent additional dlls from being included

Code Versions

Example code is using .NET 7.0.