Workaround – Injecting options directly – Options, Settings, and Configuration

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<Options> instead of the Options 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 Options 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 Options class directly over mocking an interface like IOptionsSnapshot is easier.It just so happens that we can circumvent this easily with the following two parts trick:

  1. Set up the options class normally, as explored in this chapter.
  2. Create a dependency binding that instructs the container to inject the options class directly using the Options pattern.

The xUnit test of the ByPassingInterfaces class from the OptionsValidation project demonstrates this. Here’s the skeleton of that test class:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
namespace OptionsValidation;
public class ByPassingInterfaces
{
    [Fact]
    public void Should_support_any_scope() { /*…*/ }
    private class Options
    {
        public string?
Name { get; set; }
    }
}

The preceding Options class has only a Name property. We are using it next to explore the workaround in the test case:

[Fact]
public void Should_support_any_scope()
{
    // Arrange
    var services = new ServiceCollection();
    services.AddOptions<Options>()
        .Configure(o => o.Name = “John Doe”);
    services.AddScoped(serviceProvider => {
        var snapshot = serviceProvider
            .GetRequiredService<IOptionsSnapshot<Options>>();
        return snapshot.Value;
    });
    var serviceProvider = services.BuildServiceProvider();
    // Act & Assert
    using var scope1 = serviceProvider.CreateScope();
    var options1 = scope1.ServiceProvider.GetService<Options>();
    var options2 = scope1.ServiceProvider.GetService<Options>();
    Assert.Same(options1, options2);
    using var scope2 = serviceProvider.CreateScope();
    var options3 = scope2.ServiceProvider.GetService<Options>();
    Assert.NotSame(options2, options3);
}

In the preceding code block, we registered the Options class using a factory method. That way, we can inject the Options class directly (with a scoped lifetime). Moreover, the delegate now controls the Options class’s creation and lifetime (highlighted code).And voilà, this workaround allows us to inject Options directly into our system without tying our classes with any .NET-specific options interface.

Consuming options through the IOptionsSnapshot<TOptions> interface results in a scoped lifetime.

The Act & Assert section of the test validates the correctness of the setup by creating two scopes and ensuring that each scope returns a different instance while returning the same instance within the scope. For example, both options1 and options2 come from scope1, so they should be the same. On the other hand, options3 comes from scope2, so it should be different than options1 and options2.This workaround also applies to existing systems that could benefit from the Options pattern without updating its code—assuming the system is dependency injection-ready. We can also use this trick to compile an assembly that does not depend on Microsoft.Extensions.Options. By using this trick, we can set the lifetime of the options from the composition root, which is a more classic dependency injection-enabled flow. To change the lifetime, use a different interface, like IOptionsMonitor or IOptionsFactory.Next, we explore a way to organize all this code.

Leave a Reply

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



         


          Terms of Use | Accessibility Privacy