Project – Centralizing the configuration – Options, Settings, and Configuration

Creating classes and classes is very object-oriented and follows the single-responsibility principle, among others. However, dividing responsibilities into programming concerns is not always what leads to the easiest code to understand because it creates a lot of classes and files, often spread across multiple layers and more.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.In this example, we explore 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 have access to any other methods, like Configure or Validate.To achieve this, we can implement the interfaces explicitly, hiding them from the ProxyOptions but showing them to the consumers of the interfaces. For example, binding the ProxyOptions class to the IValidateOptions<ProxyOptions> interface gives the consumer access to the Validate method through the IValidateOptions<ProxyOptions> interface. Explaining this should be simpler in code; here’s the class:

using Microsoft.Extensions.Options;
namespace CentralizingConfiguration;
public class ProxyOptions : IConfigureOptions<ProxyOptions>, IValidateOptions<ProxyOptions>
{
    public static readonly int DefaultCacheTimeInSeconds = 60;
    public string?
Name { get; set; }
    public int CacheTimeInSeconds { get; set; }
    void IConfigureOptions<ProxyOptions>.Configure(
        ProxyOptions options)
    {
        options.CacheTimeInSeconds = DefaultCacheTimeInSeconds;
    }
    ValidateOptionsResult IValidateOptions<ProxyOptions>.Validate(
        string?
name, ProxyOptions options)
    {
        if (string.IsNullOrWhiteSpace(options.Name))
        {
            return ValidateOptionsResult.Fail(
                “The ‘Name’ property is required.”);
        }
        return ValidateOptionsResult.Success;
    }
}

The preceding code implements both IConfigureOptions<ProxyOptions> and IValidateOptions<ProxyOptions> interfaces explicitly (highlighted) by omitting the visibility modifier and prefixing the name of the method with the name of the interface, like the following:

ValidateOptionsResult IValidateOptions<ProxyOptions>.Validate(…)

Now, to leverage it, we must register it with the IoC container like this:

builder.Services
    .AddSingleton<IConfigureOptions<ProxyOptions>, ProxyOptions>()
    .AddSingleton<IValidateOptions<ProxyOptions>, ProxyOptions>()
    .AddSingleton(sp => sp
        .GetRequiredService<IOptions<ProxyOptions>>()
        .Value
    )
    .Configure<ProxyOptions>(options
        => options.Name = “High-speed proxy”)
    .AddOptions<ProxyOptions>()
    .ValidateOnStart()
;

In the preceding code, we combined many notions we explored, like:

  • Registering the options class
  • Using the workaround to access the ProxyOptions class directly
  • Configuring the options inline and through a configurator class
  • Leverage a validation class
  • Enforcing the validation by eager loading our options during the startup.

If you comment out the highlighted line, the application will throw an exception on startup.

The only endpoint defined in the application is the following:

app.MapGet(“/”, (ProxyOptions options) => options);

When we run the application, we get the following output:

{
  “name”: “High-speed proxy”,
  “cacheTimeInSeconds”: 60
}

As expected, the value of the cacheTimeInSeconds property equals the value of the DefaultCacheTimeInSeconds field, and the value of the name property to what we configured in the Program.cs file.When using the IntelliSense feature inside your favorite IDE, I’m using Visual Studio 2022 here, we can see only the properties, no method:

 Figure 9.1: VS IntelliSense not showing explicitly implemented interfaces members.Figure 9.1: VS IntelliSense not showing explicitly implemented interfaces members. 

That’s it; we are done with this organizational technique.

To keep the composition cleaner, we could encapsulate the bindings in an extension method, and, even better, make that extension method register the whole proxy feature. For example, services.AddProxyService().

I’ll let you practice this one on your own as we already explored this.

Next, we explore code generators!

Leave a Reply

Your email address will not be published. Required fields are marked *



         


          Terms of Use | Accessibility Privacy