Reader small image

You're reading from  Architecting ASP.NET Core Applications - Third Edition

Product typeBook
Published inMar 2024
Reading LevelIntermediate
PublisherPackt
ISBN-139781805123385
Edition3rd Edition
Languages
Right arrow
Author (1)
Carl-Hugo Marcotte
Carl-Hugo Marcotte
author image
Carl-Hugo Marcotte

Carl-Hugo Marcotte is a software craftsman who has developed digital products professionally since 2005, while his coding journey started around 1989 for fun. He has a bachelor's degree in computer science. He has acquired a solid background in software architecture and expertise in ASP.NET Core through creating a wide range of web and cloud applications, from custom e-commerce websites to enterprise applications. He served many customers as an independent consultant, taught programming, and is now a Principal Architect at Export Development Canada. Passionate about C#, ASP.NET Core, AI, automation, and Cloud computing, he fosters collaboration and the open-source ethos, sharing his expertise with the tech community.
Read more about Carl-Hugo Marcotte

Right arrow

Application Configuration and the Options Pattern

This chapter delves into the Options pattern and the configuration of applications, using features provided by ASP.NET Core to simplify the management and implementation of configuration. We explore a range of tools and methodologies that allow us to divide our configuration into multiple smaller objects (Separation of Concerns), configure them during different stages of the startup flow, and validate them. Additionally, we cover a broad spectrum of scenarios, including watching for runtime changes with minimal effort and managing, injecting, and loading configurations into our ASP.NET Core applications.

The new options system repurposed the ConfigurationManager class as an internal piece. We can no longer use it as the old .NET Framework-era static methods are gone. The new patterns and mechanisms help avoid useless coupling, add flexibility to our designs, and are DI-native. The system is also simpler to extend.

...

Loading the configuration

ASP.NET Core allows us to load settings from multiple sources seamlessly by using configuration providers. We can customize these sources from the WebApplicationBuilder, or use the defaults set by the WebApplication.CreateBuilder(args) method.

The default sources, in order, are as follows:

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. User secrets; these are only loaded when the environment is Development
  4. Environment variables
  5. Command-line arguments

The order is essential, as the last to be loaded overrides previous values. For example, you can set a value in appsettings.json and override it in appsettings.Staging.json by redefining the value in that file, user secrets, or an environment variable or by passing it as a command-line argument when you run your application.

You can name your environments as you want, but by default, ASP.NET Core has built-in helper methods for Development, Staging,...

Learning the options interfaces

There are four main interfaces to use settings:

  • IOptionsMonitor<TOptions>
  • IOptionsFactory<TOptions>
  • IOptionsSnapshot<TOptions>
  • IOptions<TOptions>

We must inject one of those interfaces into a class to use the available settings. TOptions is the type that represents the settings that we want to access.

The framework returns an empty instance of your options class if you don’t configure it. We learn how to configure options properly in the next subsection; meanwhile, remember that using property initializers inside your options class can also be a great way to ensure certain defaults are used. You can also use constants to centralize those defaults somewhere in your codebase (making them easier to maintain). Proper configuration and validation are always preferred, but both combined can add a safety net.

Don’t use initializers or constants for default values that change...

Exploring common usage scenarios

This first example covers multiple basic use cases, such as injecting options, using named options, and storing options values in settings.

The name of the project in the source code is CommonScenarios.

Let’s start by manually changing the configuration values.

Manual configuration

In the composition root, we can manually configure options, which is very useful for configuring ASP.NET Core MVC, the JSON serializer, other pieces of the framework, or our own handcrafted options.

Here’s the first options class we use in the code, which contains only a Name property:

namespace CommonScenarios;
public class MyOptions
{
    public string? Name { get; set; }
}

In the composition root, we can use the Configure extension method, which extends the IServiceCollection interface to configure our object. Here’s how we can set the default options of the MyOptions class:

builder.Services.Configure<...

Learning options configuration

Now that we have covered basic usage scenarios, let’s attack some more advanced possibilities, such as creating types to configure, initialize, and validate our options.

The name of the project in the source code is OptionsConfiguration.

We start by configuring options, which happens in two phases:

  1. The configuration phase.
  2. The post-configuration phase

In a nutshell, the post-configuration phase happens later in the process. This is a good place to enforce that some values are configured a certain way or to override configuration, for example, in integration tests.

To configure an options class, we have many options, starting with the following interfaces:

Interface

Description

IConfigureOptions<TOptions>

Configure the default TOptions...

Validating our options objects

Another feature that comes out of the box is options validation, which allows us to run validation code when a TOptions object is created. The validation code is guaranteed to run the first time an option is created and does not account for subsequent option modifications. Depending on the lifetime of your options object, the validation may or may not run. For example:

...

Interface

Lifetime

Validation

IOptionsMonitor<TOptions>

Singleton

Validate the options once.

IOptionsFactory<TOptions>

Transient

Validate the options every time the code calls the Create method.

Validating options using FluentValidation

In this project, we validate options classes using FluentValidation. FluentValidation is a popular open-source library that provides a validation framework different from data annotations. One of the primary advantages of FluentValidation is that it allows encapsulating validation rules in another class than the one being validated. That makes the validation logic easier to test and more explicit than depending on metadata added by attributes. We explore FluentValidation more in Chapter 17, Getting Started with Vertical Slice Architecture, but that should not hinder you from following this example.

The name of the project in the source code is OptionsValidationFluentValidation.

Here, I want to show you how to leverage a few patterns we’ve learned so far to implement this ourselves with only a few lines of code. In this micro-project, we leverage:

  • Dependency injection
  • The Strategy design pattern
  • ...

Injecting options objects directly

The only negative point about the .NET Options pattern is that we must tie our code to the framework’s interfaces. We must inject an interface like IOptionsMonitor<MyOptions> instead of the MyOptions class itself. By letting the consumers choose the interface, we let them control the lifetime of the options, which breaks the inversion of control, dependency inversion, and open/closed principles.

We should move that responsibility out of the consumer up to the composition root.

As we explored at the beginning of this chapter, the IOptions, IOptionsFactory, IOptionsMonitor, and IOptionsSnapshot interfaces define the options object’s lifetime.

In most cases, I prefer to inject MyOptions directly, controlling its lifetime from the composition root, instead of letting the class itself control its dependencies. I’m a little anti-control-freak, I know. Moreover, writing tests using the MyOptions class...

Centralizing the configuration for easier management

Creating tons of classes is very object-oriented and follows the single responsibility principle, among others. However, dividing responsibilities into programming concerns does not always lead to the easiest code to understand because it creates a lot of classes and files, often spread across multiple layers.

An alternative is to regroup the initialization and validation with the options class itself, shifting the multiple responsibilities to a single one: an end-to-end options class management.

The name of the project in the source code is CentralizingConfiguration.

In this example, we explore the ProxyOptions class, which carries the name of the service and the time the proxy service should cache items in seconds. We want to set a default value for the CacheTimeInSeconds property and validate that the Name property is not empty.

On the other hand, we don’t want the consumer of that class to...

Using the configuration-binding source generator

.NET 8 introduces a configuration-binding source generator that provides an alternative to the default reflection-based implementation. In simple terms, the name of the options class properties and the settings keys are now hard-coded, accelerating the configuration retrieval.

Beware: the settings keys are case-sensitive and map one-on-one with the C# class property name, unlike the non-generated code.

Web applications using native AOT deployment (ahead-of-time compilation to native code) or trimming self-contained deployments to ship only the bits in use now leverage this option by default.

The native AOT deployment model compiles the code to a single runtime environment like Windows x64. It does not need the just-in-time (JIT) compiler since the code is already compiled to the native version of the targeted environment. AOT deployments are self-contained and do not need the .NET runtime to work.

...

Using the options validation source generator

.NET 8 introduces the options validation source generator, which generates the validation code based on data annotations. The idea is similar to the configuration-binding source generator but for the validation code.

To leverage the validation generator, we must add a reference on the Microsoft.Extensions.Options.DataAnnotations package.

Afterward, we must:

  1. Create an empty validator class.
  2. Ensure the class is partial.
  3. Implement the IValidateOptions<TOptions> interface (but not the methods).
  4. Decorate the validator class with the [OptionsValidator] attribute.
  5. Register the validator class with the container.

This procedure sounds complicated but is way simpler in code; let’s look at that now.

The name of the project in the source code is ConfigurationGenerators.

In this second part of the project, we continue to build on the previous pieces and add validation...

Using the ValidateOptionsResultBuilder class

ValidateOptionsResultBuilder is a new type in .NET 8. It allows us to dynamically accumulate validation errors and create a ValidateOptionsResult object representing its current state.

The name of the project in the source code is ValidateOptionsResultBuilder.

Its basic usage is straightforward, as we are about to see.

In this project, we are validating the MyOptions object. The type has multiple validation rules, and we want to ensure we are not stopping after the first rule fails validation so a consumer would know all the errors in one go. To achieve this, we decided to use the ValidateOptionsResultBuilder class.

Let’s start with the options class:

namespace ValidateOptionsResultBuilder;
public class MyOptions
{
    public string? Prop1 { get; set; }
    public string? Prop2 { get; set; }
}

Next, let’s implement a validator class that enforces that both properties can’t be empty...

Wrapping up

In this chapter, we learned how the Options pattern helps enhance application flexibility and reliability when we need configurations. We explored multiple elements of the framework to use configuration, load configurations, and validate configurations. We learned about tools that can help us validate our objects, how to inject our settings directly into their consumers, and more.

Now, on top of that, let’s explore how the Options pattern helps us adhere to the SOLID principles:

  • S: The Options pattern divides managing settings into multiple pieces where each has a single responsibility. Loading unmanaged settings into strongly typed classes is one responsibility, validating options using classes is another, and configuring options from multiple independent sources is one more.
  • O: The different IOptions*<TOptions> interfaces break this principle by forcing the consumer to decide what lifetime and capabilities the options should have. To...

Summary

This chapter explored the Options pattern, a powerful tool allowing us to configure our ASP.NET Core applications. It enables us to change the application without altering the code. The capability even allows the application to reload the options at runtime when a configuration file is updated without downtime.

We learned how to load settings from multiple sources, with the last loaded source overriding previous values. We discovered the following interfaces to access settings and learned that the choice of interface influences the lifetime of the options object:

  • IOptionsMonitor<TOptions>
  • IOptionsFactory<TOptions>
  • IOptionsSnapshot<TOptions>
  • IOptions<TOptions>

We delved into manually configuring options in the composition root and loading them from a settings file. We also learned how to inject options into a class and configure multiple instances of the same options type using named options. We explored encapsulating...

Questions

Let’s take a look at a few practice questions:

  1. Name one interface we can use to inject a settings class.
  2. Name the two phases ASP.NET Core uses when configuring options.
  3. How significant is the order in which we register configuration objects and inline delegates?
  4. Can we register multiple configuration classes?
  5. What is eager validation, and why should you use it?
  6. What interface must we implement to create a validator class?

Further reading

Here are some links to build upon what we learned in the chapter:

Answers

  1. We can use one of the following interfaces: IOptionsMonitor<TOptions>, IOptionsFactory<TOptions>, IOptionsSnapshot<TOptions>, or IOptions<TOptions>.
  2. The configuration and the post-configuration phases.
  3. Configurators are executed in the order of their registration, so their order is crucial.
  4. Yes, we can register as many configuration classes as we want.
  5. Eager validation allows catching incorrectly configured options at startup time, which can prevent runtime issues.
  6. We must implement the IValidateOptions<TOptions> interface.

Learn more on Discord

To join the Discord community for this book – where you can share feedback, ask questions to the author, and learn about new releases – follow the QR code below:

https://packt.link/ArchitectingASPNETCoreApps3e

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Architecting ASP.NET Core Applications - Third Edition
Published in: Mar 2024Publisher: PacktISBN-13: 9781805123385
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Author (1)

author image
Carl-Hugo Marcotte

Carl-Hugo Marcotte is a software craftsman who has developed digital products professionally since 2005, while his coding journey started around 1989 for fun. He has a bachelor's degree in computer science. He has acquired a solid background in software architecture and expertise in ASP.NET Core through creating a wide range of web and cloud applications, from custom e-commerce websites to enterprise applications. He served many customers as an independent consultant, taught programming, and is now a Principal Architect at Export Development Canada. Passionate about C#, ASP.NET Core, AI, automation, and Cloud computing, he fosters collaboration and the open-source ethos, sharing his expertise with the tech community.
Read more about Carl-Hugo Marcotte