Provide default configuration to your .NET applications

May 23, 2022
Written by
Reviewed by

Provide default configuration to your .NET applications

In a previous post I shared how you can better configure your .NET applications for Twilio and SendGrid using the Microsoft.Extensions.Configuration APIs. In this tutorial, I'll build upon the techniques from those tutorials and you'll learn how to store default options in one section of your configuration and override them with specific options from another section. Let me clarify using an example. Applications often send different types of emails, like welcome emails, password reset emails, offer emails, etc. Depending on the type of email, you may want to send them from a different email address. For most emails, you may be using something like 'no-reply@yourdomain.tld', but for offer emails, the replies to the email could be of value. So instead of using the same no-reply address, you could use an email address that will be routed to a sales person and maybe automatically integrates with a Customer Relationship Management (CRM) system.

In this example, you'd want to store 'no-reply@yourdomain.tld' as the default reply address, but for offer-emails, you want to use another email address, for example, 'offers@yourdomain.tld'.

Prerequisites

To follow along, I recommend having

You can find the source code for this tutorial on GitHub. Use it as a reference if you run into any issues, or submit an issue if you need assistance.

Getting started

You can apply these concepts to any scenario, but let's continue using the email scenario throughout this tutorial.

Run the following commands to clone the tutorial project to your machine using git and navigate into the project:

git clone https://github.com/Swimburger/DotNetDefaultConfiguration.git  --branch start
cd DotNetDefaultConfiguration

The git repository you cloned contains a .NET 6 console application that already contains some JSON configuration and code to load the configuration.

Here's what the appsettings.json file looks like:

{
  "Emails": {
    "Welcome": {
      "FromEmail": "no-reply@yourdomain.tld",
      "FromName": "Your App Name",
      "ToEmail": "jon@doe.com",
      "ToName": "Jon Doe",
      "Subject": "Welcome to our service!",
      "Body": "Thank you for signing up!"
    },
    "PasswordReset": {
      "FromEmail": "no-reply@yourdomain.tld",
      "FromName": "Your App Name",
      "ToEmail": "jon@doe.com",
      "ToName": "Jon Doe",
      "Subject": "Reset your password",
      "Body": "Click the link below to reset your password."
    },
    "Offer": {
      "FromEmail": "offers@yourdomain.tld",
      "FromName": "Jane",
      "ToEmail": "jon@doe.com",
      "ToName": "Jon Doe",
      "Subject": "Don't miss this offer!",
      "Body": "We have an amazing offer for you!"
    }
  }
}

Here's what the Program.cs file looks like:

using Microsoft.Extensions.Configuration;

IConfiguration config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();
    
EmailOptions GetEmailOptions(string emailSectionName)
{
    var emailOptions = new EmailOptions();
    
    var emailsSection = config.GetSection("Emails");
    var emailSection = emailsSection.GetSection(emailSectionName);

    emailOptions.FromEmail = emailSection["FromEmail"];
    emailOptions.FromName = emailSection["FromName"];
    emailOptions.ToEmail = emailSection["ToEmail"];
    emailOptions.ToName = emailSection["ToName"];
    emailOptions.Subject = emailSection["Subject"];
    emailOptions.Body = emailSection["Body"];

    return emailOptions;
}

void LogEmailOptions(EmailOptions emailOptions)
{
    Console.WriteLine(@"Logging Email Option:
    FromEmail: {0}
    FromName: {1}
    ToEmail: {2}
    ToName: {3}
    Subject: {4}
    Body: {5}", 
        emailOptions.FromEmail, 
        emailOptions.FromName, 
        emailOptions.ToEmail, 
        emailOptions.ToName, 
        emailOptions.Subject, 
        emailOptions.Body
    );
}

var welcomeEmail = GetEmailOptions("Welcome");
LogEmailOptions(welcomeEmail);

var passwordResetEmail = GetEmailOptions("PasswordReset");
LogEmailOptions(passwordResetEmail);

var offerEmail = GetEmailOptions("Offer");
LogEmailOptions(offerEmail);

public class EmailOptions
{
    public string FromEmail { get; set; }
    public string FromName { get; set; }
    public string ToEmail { get; set; }
    public string ToName { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
}

The code builds configuration using only JSON as a source and then the GetEmailOptions local function is used to fetch the configuration and create an instance of  EmailOptions for the welcome-email, password-reset-email, and offer-email. You would probably dynamically generate the body of the emails and dynamically fetch the recipient email address in a real application, but this will work fine for the purpose of this tutorial.

After each EmailOptions object is created for each email section, the configuration is logged via the LogEmailOptions local function, so you can see the result of this code.

You may notice that there's a lot of redundancy in the JSON, which is why you'll introduce a defaults section in the next part of this tutorial.

Run the project using the .NET CLI:

dotnet run
# Output:
#  Logging Email Option:
#      FromEmail: no-reply@yourdomain.tld
#      FromName: Your App Name
#      ToEmail: jon@doe.com
#      ToName: Jon Doe
#      Subject: Welcome to our service!
#      Body: Thank you for signing up!
#  Logging Email Option:
#      FromEmail: no-reply@yourdomain.tld
#      FromName: Your App Name
#      ToEmail: jon@doe.com
#      ToName: Jon Doe
#      Subject: Reset your password
#      Body: Click the link below to reset your password.
#  Logging Email Option:
#      FromEmail: offers@yourdomain.tld
#      FromName: Jane
#      ToEmail: jon@doe.com
#      ToName: Jon Doe
#      Subject: Don't miss this offer!
#      Body: We have an amazing offer for you!

You can see that the configuration is pulled from the JSON file and then written to the console.

Add a defaults section for redundant configuration elements

You can add a Defaults section to remove the redundant configuration elements. Open appsettings.json and in front of the Welcome section, add a Defaults section:

{
  "Emails": {
    "Defaults": {
      "FromEmail": "no-reply@yourdomain.tld",
      "FromName": "Your App Name",
      "ToEmail": "jon@doe.com",
      "ToName": "Jon Doe"
    },
    "Welcome": {
      "Subject": "Welcome to our service!",
      "Body": "Thank you for signing up!"
    },
    "PasswordReset": {
      "Subject": "Reset your password",
      "Body": "Click the link below to reset your password."
    },
    "Offer": {
      "FromEmail": "offers@yourdomain.tld",
      "FromName": "Jane",
      "Subject": "Don't miss this offer!",
      "Body": "We have an amazing offer for you!"
    }
  }
}

As shown above, also remove the redundant FromEmail, FromName, ToEmail, and ToName properties from the Welcome and PasswordReset section. However, only remove the redundant ToEmail and ToName properties from the  Offer section and leave the FormEmail and FromName properties be.

Now, update the code in the Program.cs file so that the configuration is retrieved from the specific email section, but if null (??), then retrieve it from the Defaults section.

EmailOptions GetEmailOptions(string emailSectionName)
{
    var emailOptions = new EmailOptions();
    
    var emailsSection = config.GetSection("Emails");
    var defaultsSections = emailsSection.GetSection("Defaults");
    var emailSection = emailsSection.GetSection(emailSectionName);

    emailOptions.FromEmail = emailSection["FromEmail"] ?? defaultsSections["FromEmail"];
    emailOptions.FromName = emailSection["FromName"] ?? defaultsSections["FromName"];
    emailOptions.ToEmail = emailSection["ToEmail"] ?? defaultsSections["ToEmail"];
    emailOptions.ToName = emailSection["ToName"] ?? defaultsSections["ToName"];
    emailOptions.Subject = emailSection["Subject"] ?? defaultsSections["Subject"];
    emailOptions.Body = emailSection["Body"] ?? defaultsSections["Body"];

    return emailOptions;
}

?? is the null-coalescing operator which returns the value on the right side if the value on the left side is null. This allows you to succinctly check for null and if null, provide a fallback value.

Run the project again using the .NET CLI:

dotnet run

As you can see, the output has not changed, but now you can set configuration on specific sections and fallback on configuration from the Defaults section.

This code gets the job done, but the same result could be achieved more eloquently by binding configuration sections to strongly-typed objects.

Bind defaults and override defaults using the configuration binder

Instead of retrieving each configuration element individually, you can use the configuration binder to bind configuration sections to strongly-typed objects. There are two methods you can use to bind configuration sections to an instance of EmailOptions: Get<T>() and  Bind(object).

To use the Get method, pass in the desired type as a generic type parameter and the Get method create an instance of that type and bind the configuration to the object.

var emailsSection = config.GetSection("Emails");
var emailOptions = emailsSection.GetSection(emailSectionName)
    .Get<EmailOptions>();

To use the Bind method, create an instance of EmailOptions and then pass it as a parameter to the Bind method.

var emailOptions = new EmailOptions();

var emailsSection = config.GetSection("Emails");
emailsSection.GetSection(emailSectionName)
    .Bind(emailOptions);

What's even more interesting is that you can use the Bind method multiple times! So, if you'd like to bind both the defaults and the email specific configuration, you could do that as follows:

var emailOptions = new EmailOptions();

var emailsSection = config.GetSection("Emails");
emailsSection.GetSection("Defaults").Bind(emailOptions);
emailsSection.GetSection(emailSectionName).Bind(emailOptions);

Using this technique, update the GetEmailOptions local function to create an instance of  EmailOptions, then bind the Defaults section to it, and then override the defaults by binding the specific section to it:

EmailOptions GetEmailOptions(string emailSectionName)
{
    var emailOptions = new EmailOptions();
    
    var emailsSection = config.GetSection("Emails");
    emailsSection.GetSection("Defaults").Bind(emailOptions);
    emailsSection.GetSection(emailSectionName).Bind(emailOptions);

    return emailOptions;
}

Arrays are not very common in .NET configuration but they are supported as well. However, when you bind configuration sections with arrays to a single object, the arrays will not overwrite each other, but instead they will be merged into one. If you use arrays in your configuration and binding, make sure to verify this behavior works as you expect it to.

Hardcoding defaults

In addition to pulling defaults from the configuration, you may also opt to add default values directly into your code. For example, if you're developing a library for others to consume, it would be convenient for some of the configuration elements to have default values.

If you're pulling individual configuration elements, you could add default values like this:

emailOptions.FromEmail = emailSection["FromEmail"]
    ?? defaultsSections["FromEmail"]
    ?? "no-reply@yourdomain.tld";

Maybe it doesn't make sense to hardcode a default value, but the configuration element is required, so you want to throw an exception if the configuration element is null. In that case, you can also use the null-coalescing operator to throw an exception inline like this:

emailOptions.FromEmail = emailSection["FromEmail"]
    ?? throw new Exception($"Emails:{emailSectionName}:FromEmail has to be configured");

However, if you're using the configuration binder, you don't need to use the null-coalescing operator, and you can instead specify the default values in the object initializer like this:

var emailOptions = new EmailOptions
{
    FromEmail = "no-reply@yourdomain.tld",
    ...
};

var emailsSection = config.GetSection("Emails");
emailsSection.GetSection("Defaults").Bind(emailOptions);
emailsSection.GetSection(emailSectionName).Bind(emailOptions);

Alternatively, you could specify default values in the class definition itself like this:

public class EmailOptions
{
    public string FromEmail { get; set; } = "no-reply@yourdomain.tld";
    public string FromName { get; set; }
    public string ToEmail { get; set; }
    public string ToName { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
}

Next steps

You've learned how to provide default configuration values through code and through a dedicated Defaults configuration section. If you haven't already, I recommend learning about more configuration techniques in a tutorial applied to Twilio configuration, or applied to SendGrid configuration.

If you found this useful, let me know and share what you're working on. I can't wait to see what you build!

Niels Swimberghe is a Belgian American software engineer and technical content creator at Twilio. Get in touch with Niels on Twitter @RealSwimburger and follow Niels' personal blog on .NET, Azure, and web development at swimburger.net.