DEV Community

Cover image for Extend ConfigurationBuilder
Karen Payne
Karen Payne

Posted on

Extend ConfigurationBuilder

Introduction

Application configuration files are commonly used to access various settings, from database connection strings to values that may change so that an application does not need to be recompiled. Usually, an application has a file named appsettings.json with environment files for staging and production.

The following represents the common model for reading application settings.

Image from Microsoft docs

The focus here is to provide other options that allow settings to come from other sources, like a database, and then be accessible from ConfigurationBuilder.

Conventional approach

A connection string is a common setting to store in appsettings.json. The following code sample illustrates reading a connection string.

appsettings.json has a section named ConnectionStrings and a property named MainConnection. As shown next, a class is needed to read MainConnection, which is strongly typed.

{
  "ConnectionStrings": {
    "MainConnection": "Data Source=.\\SQLEXPRESS;Initial Catalog=AppsettingsConfigurations;Integrated Security=True;Encrypt=False"
  }
}
Enter fullscreen mode Exit fullscreen mode

Class for reading the connection string.

public class ConnectionStrings
{
    public string MainConnection { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

By using a class rather than hard coded string values, if in this case if MainConnection was to be renamed there is no need to change anything with ConfigurationBuilder code using nameof will resolve parameters to GetSection from ConfigurationBuilder.

For example, accessing the connection string to use, in this case, Dapper.

IConfigurationRoot configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .Build();

var connectionStrings = configuration.GetSection(nameof(ConnectionStrings)).Get<ConnectionStrings>();

using IDbConnection db = new SqlConnection(connectionStrings.MainConnection);
string sql = """
             SELECT Section + ':' + [Key] AS [Key], Value 
             FROM dbo.Settings;
             """;
var dictionary = db.Query<(string Key, string Value)>(sql)
    .ToDictionary(x => x.Key, x => x.Value);
Enter fullscreen mode Exit fullscreen mode

The following is for EF Core, where JsonRoot is the same as using ConfigurationBuilder, which is defined in the ConsoleConfigurationLibrary NuGet package for convenience.

appsettings.json can store more than connection strings. Below is a simple example.

{
  "ConnectionStrings": {
    "MainConnection": "Data Source=.\\SQLEXPRESS;Initial Catalog=AppsettingsConfigurations;Integrated Security=True;Encrypt=False"
  },
  "Layout": {
    "Header": "Visible",
    "Title": "Some title",
    "Footer": "Hidden"
  }
}
Enter fullscreen mode Exit fullscreen mode

For more opportunities in regards to working with conventional use for appsettings.json, see the following Storing and reading values from appsettings.json.

Note
In the next section, AnsiConsole methods are from the NuGet package Spectre.Console.

Memory configuration provider

The MemoryConfigurationProvider uses an in-memory collection as configuration key-value pairs.

Basic sample hard-coded

To best understand using the memory configuration provider, add a dictionary of values in code rather than reading from a file. There are two distinct sections: ConnectionStrings and Layout. This means that two classes are needed, the same as the first code sample presented above.

public class ConnectionStrings
{
    public string MainConnection { get; set; } = string.Empty;
}

public class Layout
{
    public string Header { get; set; } = string.Empty;
    public string Title { get; set; } = string.Empty;
    public string Footer { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

Here, we have no appsettings.json file included. We only added a dictionary using AddInMemoryCollection.

var configuration = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string>
    {
        {
            "ConnectionStrings:MainConnection",
            "Server=(localdb)\\MSSQLLocalDB;Database=NorthWind2024;Trusted_Connection=True"
        },
        { "Layout:Header", "Visible" },
        { "Layout:Title", "Some title" },
        { "Layout:Footer", "Hidden" }
    }).Build();

var connectionStrings = configuration.GetSection(nameof(ConnectionStrings)).Get<ConnectionStrings>();
var layout = configuration.GetSection(nameof(Layout)).Get<Layout>();

var mainConnection = connectionStrings.MainConnection;
var headerLayout = layout.Header;
var titleLayout = layout.Title;
var footerLayout = layout.Footer;
Enter fullscreen mode Exit fullscreen mode

The next iteration is to remove the ConnectionStrings section and read this from appsettings.json

{
  "ConnectionStrings": {
    "MainConnection": "Data Source=.\\SQLEXPRESS;Initial Catalog=AppsettingsConfigurations;Integrated Security=True;Encrypt=False"
  }
}
Enter fullscreen mode Exit fullscreen mode

The revised code reads the connection string from appsettings.json and Layout but is still hard-coded.

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddInMemoryCollection(new Dictionary<string, string>
    {
        { "Layout:Header", "Visible" },
        { "Layout:Title", "Some title" },
        { "Layout:Footer", "Hidden" }
    }).Build();

var connectionStrings = configuration.GetSection(nameof(ConnectionStrings)).Get<ConnectionStrings>();
var layout = configuration.GetSection(nameof(Layout)).Get<Layout>();

var mainConnection = connectionStrings.MainConnection;
var headerLayout = layout.Header;
var titleLayout = layout.Title;
var footerLayout = layout.Footer;
Enter fullscreen mode Exit fullscreen mode

Basic sample from two json files

The following example reads two JSON files, appsettings.json for a connection string and layout.json for the Layout class, using AddInMemoryCollection. Note that an alternate would be to simply use AddJsonFile rather than AddInMemoryCollection. The reason for AddInMemoryCollection will become clear when It gets values from a database table.

Since AddInMemoryCollection wants a dictionary, the following method is used in a separate class, which could be used in a class project for reusability.

public class SettingsReader
{

    public static Dictionary<string, string> LoadLayout()
    {

        var document = JsonDocument.Parse(File.ReadAllText("layout.json"));
        var layoutDict = new Dictionary<string, string>();

        if (!document.RootElement.TryGetProperty("Layout", out var layoutElement)) return layoutDict;
        foreach (var property in layoutElement.EnumerateObject())
        {
            layoutDict[$"Layout:{property.Name}"] = property.Value.GetString() ?? string.Empty;
        }

        return layoutDict;

    }
}
Enter fullscreen mode Exit fullscreen mode

Note that the code above is passed into AddInMemoryCollection, which can be accessed easily.

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddInMemoryCollection(SettingsReader.LoadLayout()) // Inject the layout.json content
    .Build();

var connectionStrings = configuration.GetSection(nameof(ConnectionStrings)).Get<ConnectionStrings>();
var layout = configuration.GetSection(nameof(Layout)).Get<Layout>();

AnsiConsole.MarkupLine($"[cyan]Connection String:[/] {connectionStrings.MainConnection}");
AnsiConsole.MarkupLine($"[cyan]Header:[/] {layout.Header}");
AnsiConsole.MarkupLine($"[cyan]Title:[/] {layout.Title}");
AnsiConsole.MarkupLine($"[cyan]Footer:[/] {layout.Footer}");
Enter fullscreen mode Exit fullscreen mode

Using appsettings and database

This section's settings are read from appsettings.json and an SQL-Server table using Dapper. To change things up, a HelpDesk class replaces the Layout class.

Once data read from a database is used with ConfigurationBuilder and AddInMemoryCollection, values from the database table are accessed the same way as a setting from appsetting is used.

HelpDesk class

public class HelpDesk
{
    public string Phone { get; set; }
    public string Email { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Class, which reads data from a SQL-Server database table.

public class DataOperations
{
    public static Dictionary<string, string> ReadFromDatabase()
    {

        var connectionStrings = Config.Configuration.JsonRoot()
            .GetSection(nameof(ConnectionStrings))
            .Get<ConnectionStrings>();

        using IDbConnection db = new SqlConnection(connectionStrings.MainConnection);
        string sql = """
                     SELECT Section + ':' + [Key] AS [Key], Value 
                     FROM dbo.Settings;
                     """;

        return db.Query<(string Key, string Value)>(sql)
            .ToDictionary(x => x.Key, x => x.Value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Find the following for those using EF Core, which is included in the provided source code.

public static Dictionary<string, string?> GetHelpDeskValues()
{
    using var context = new Context();
    var settings = context.Settings
        .AsNoTracking()
        .Where(x => x.Section == nameof(HelpDesk) && (x.Key == nameof(HelpDesk.Phone) || x.Key == nameof(HelpDesk.Email)))
        .ToList();

    return new Dictionary<string, string?>
    {
        {"Helpdesk:phone", settings.FirstOrDefault(x => x.Key == nameof(HelpDesk.Phone))?.Value},
        {"Helpdesk:email", settings.FirstOrDefault(x => x.Key == nameof(HelpDesk.Email))?.Value}
    };
}
Enter fullscreen mode Exit fullscreen mode

This means a developer has to use Dapper or EF Core, simply pick your preference.

Putting everything together. Note that the DataOperations.ReadFromDatabase() could avoid the variable and drop directly into AddInMemoryCollection. By introducing a variable, a developer can set a breakpoint to inspect the data.

var settings = DataOperations.ReadFromDatabase();
var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddInMemoryCollection(settings)
    .Build();

var connectionStrings = configuration.GetSection(nameof(ConnectionStrings)).Get<ConnectionStrings>();
string mainConnection = connectionStrings.MainConnection;
var helpDesk = configuration.GetSection(nameof(HelpDesk)).Get<HelpDesk>();

AnsiConsole.MarkupLine($"[cyan]Connection String:[/] {mainConnection}");
AnsiConsole.MarkupLine($"[cyan]Phone:[/] {helpDesk.Phone}");
AnsiConsole.MarkupLine($"[cyan]Email:[/] {helpDesk.Email}");
Enter fullscreen mode Exit fullscreen mode

ASP.NET Core and EF Core

Project 1

Finish index page

The following is in project CustomIConfigurationSourceRazorPages.

The change for configuration is working with dependency injection along with first registering the DbContext followed by registering IConfiguration.

  • Secrets are used and are provided below which need to be configured.
  • Two JSON files are used.
  • Serilog is used for simple logging.

In Program.cs

Register the DbContext

builder.Services.AddDbContext<Context>(options => options.UseSqlServer(
    builder.Configuration.GetConnectionString(nameof(ConnectionStrings.MainConnection))));
Enter fullscreen mode Exit fullscreen mode

Setup a method in Program.cs to configure settings, note the use of MemoryConfigurationSource an alternate to AddInMemoryCollection.

private static IConfigurationBuilder SetupCustomConfiguration()
{
    /*
     * An alternate to AddInMemoryCollection in ConfigurationBuilder
     */
    var memorySource = new MemoryConfigurationSource { InitialData = DataOperations.GetHelpDeskValues() };

    // Add Configuration Sources
    var configurationBuilder = new ConfigurationBuilder()
        .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile("other.json", optional: false, reloadOnChange: true)
        .AddUserSecrets<MailSettings>()
        .Add(memorySource);


    /*
     * Find the MemoryConfigurationSource instance in the ConfigurationBuilder
     * For demonstration purposes only
     */
    var memoryConfigurationSource = configurationBuilder.Sources
        .OfType<MemoryConfigurationSource>()
        .FirstOrDefault();


    return configurationBuilder;
}
Enter fullscreen mode Exit fullscreen mode

This class is responsible for reading data from the SQL Server database using Entity Framework Core.

public class DataOperations
{
    public static Dictionary<string, string?> GetHelpDeskValues()
    {
        using var context = new Context();
        var settings = context.Settings
            .AsNoTracking()
            .Where(x => x.Section == nameof(HelpDesk) && (x.Key == nameof(HelpDesk.Phone) || x.Key == nameof(HelpDesk.Email)))
            .ToList();

        return new Dictionary<string, string?>
        {
            {"Helpdesk:phone", settings.FirstOrDefault(x => x.Key == nameof(HelpDesk.Phone))?.Value},
            {"Helpdesk:email", settings.FirstOrDefault(x => x.Key == nameof(HelpDesk.Email))?.Value}
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Also, there is a secondary configuration file named other.json.

{
  "Layout": {
    "Title": "Working with app configurations"
  }
}
Enter fullscreen mode Exit fullscreen mode

User secrets

{
  "MailSettings:TimeOut": "30",
  "MailSettings:Port": "587",
  "MailSettings:Host": "smtp.example.com",
  "MailSettings:FromAddress": "your-email@example.com",
  "MailSettings:PickupFolder": "MailDrop"
}
Enter fullscreen mode Exit fullscreen mode

Class for secrets

public class MailSettings
{
    public string FromAddress { get; set; }
    public string Host { get; set; }
    public int Port { get; set; }
    public int TimeOut { get; set; }
    public string PickupFolder { get; set; } = "MailDrop";
    public override string ToString() => $"From: {FromAddress} Host: {Host} Port: {Port}";
}
Enter fullscreen mode Exit fullscreen mode

Help desk

The help desk data will be set up and displayed on the page footer configured in _Layout.cshtml.

Add @inject IConfiguration Configuration at the top of _Layout.cshtml to access HelpDesk settings.

Replace the standard footer with the following.

<footer class="border-top footer text-muted">

    <div class="container">

        @{
            Logger.LogInformation("Reading help desk value...");
            /*
             * Read the HelpDesk section in memory collection, see configurationBuilder in Program.cs
             */
            var helpDesk = Configuration.GetSection(nameof(HelpDesk)).Get<HelpDesk>();
            var email = helpDesk.Email;
            var phone = helpDesk.Phone;
        }

        <span class="text-success fw-bold">Help Desk:</span> <strong>Phone</strong> @phone
        <div class="vr opacity-100"></div> <strong>Email</strong> @email

    </div>
</footer>
Enter fullscreen mode Exit fullscreen mode

Index page

It will display MailSettings settings.

Frontend code

@page
@model IndexModel


<div class="container">

    <div class="container d-flex justify-content-center">
        <div class="card shadow mt-5" style="width: 28rem;">
            <div class="card-body">
                <h1 class="card-title">About</h1>
                <p class="card-text">Help desk information in footer</p>
                <p class="card-text mb-2">Code in _layout.cshtml</p>

                <p class="card-text fw-bold">User secrets</p>

                <ul>
                    <li><strong>From Address:</strong> @Model.MailSettings.FromAddress</li>
                    <li><strong>Host:</strong> @Model.MailSettings.Host</li>
                    <li><strong>Port:</strong> @Model.MailSettings.Port</li>
                    <li><strong>Timeout:</strong> @Model.MailSettings.TimeOut</li>
                    <li><strong>Pickup Folder:</strong> @Model.MailSettings.PickupFolder</li>
                </ul>

            </div>
        </div>

    </div>

</div>
Enter fullscreen mode Exit fullscreen mode

Backend code

using ConsoleConfigurationLibrary.Models;
using CustomIConfigurationSourceRazorPages.Models;
using CustomIConfigurationSourceSample.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace CustomIConfigurationSourceRazorPages.Pages;

public class IndexModel : PageModel
{

    private readonly IConfiguration _configuration;

    public MailSettings MailSettings { get; private set; }
    private readonly Context _context;


    public IndexModel(IConfiguration configuration, Context context)
    {
        _configuration = configuration;
        _context = context;

        // non-secrets
        var connectionSection = Config.Configuration.JsonRoot()
            .GetSection(nameof(ConnectionStrings)).Get<ConnectionStrings>();

        var mainConnection = connectionSection!.MainConnection;

    }

    public void OnGet()
    {
        // non-secrets
        var layoutSection = _configuration.GetSection(nameof(Layout)).Get<Layout>();
        var title = layoutSection?.Title ?? "Home page"; 

        ViewData["Title"] = title;

        // in secrets
        MailSettings = _configuration.GetSection(nameof(MailSettings)).Get<MailSettings>();

    }
}
Enter fullscreen mode Exit fullscreen mode

Project 2: HelpDeskApplication

This project renders the same as Project 1, which does not use the memory configuration provider

Project 3: MemoryConfigurationSourceRazorPages

This project is a simplified use of memory configuration provider

Article source code

NET 9 Source code

Shows source code for article in Microsoft VS2022

Best practices

The code presented here can be argued that securely storing settings opens these settings to hackers. Many developers would not find use here if the code were written to use paid-for cloud services to store settings.

It is the developer’s responsibility to ensure that settings are secure, and each developer will have different ways of achieving this. What works for enterprise developers may not be worth it for others to use the same techniques.

Keep security in mind no matter what a developer’s environment is.

Summary

With the information and code samples provided, developers have new options for storing and reading settings in any most project types in the Microsoft Visual Studio ecosystem.

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay