Simplify your configuration with this pattern
The pattern described here is widely recognized as one of the most commonly employed and elegant approaches for configuring settings. It has been extensively utilized in both internal .NET libraries and third-party libraries. This pattern can serve as a valuable guide when registering configurations in program or startup classes for your internal libraries. For instance, if you are developing a library and wish to allow client developers to configure options during startup, this pattern can be of great assistance.
To illustrate this concept, let's consider the hypothetical scenario of designing a ApiRetryOptions
class with two retry options — MaxRetries
& DelayInSeconds
. Within this class, default values are assigned to the MaxRetries
and DelayInSeconds
properties.
public class ApiRetryOptions
{
public int MaxRetries { get; set; } = 3;
public int DelayInSeconds { get; set; } = 1000;
}
Next, we create an extension method for this class, enabling us to register it during startup:
public static class ApiRetryOptionsExtensions
{
public static void AddApiRetryOptions(this IServiceCollection services, Action<ApiRetryOptions>? options = null)
{
if (options != null) services.Configure(options);
}
}
When registering the class with the Service Collection, it is important to note the use of the Action
delegate. In C#, an Action
represents a method with no parameters and no return value. In this case, we require an Action delegate to be registered with the built-in service configuration. The .NET framework provides a convenient method called services.Configure(Action<>)
that allows us to accomplish this, as demonstrated in the example above.
Now, let's proceed to register this configuration in the program class. The beauty of this pattern lies in its ability to facilitate easy configuration during startup for any library registration. In this example, we override the default values:
builder.Services.AddApiRetryOptions(options =>
{
options.MaxRetries = 1;
options.MaxRetries = 4000;
});
Finally, we can integrate this configuration into our client code using the IOptions
class:
app.MapGet("/retry", (IOptions<ApiRetryOptions> retry) => retry.Value.MaxRetries);
By adopting this pattern, you can align your library configuration with the established conventions used in .NET libraries, thereby ensuring consistency and coherence.