If you need markers to inject some specific dependency in a particular class, you are most likely cheating the Inversion of Control principle. Instead, you should find a way to achieve the same goal using dependency injection, such as by contextually injecting your dependencies.Let’s start with the following interface:
public interface IStrategy
{
string Execute();
}
In our program, we have two implementations and two markers, one for each implementation:
public interface IStrategyA : IStrategy { }
public interface IStrategyB : IStrategy { }
public class StrategyA : IStrategyA
{
public string Execute() => “StrategyA”;
}
public class StrategyB : IStrategyB
{
public string Execute() => “StrategyB”;
}
The code is barebones, but all the building blocks are there:
- StrategyA implements IStrategyA, which inherits from IStrategy.
- StrategyB implements IStrategyB, which inherits from IStrategy.
- Both IStrategyA and IStrategyB are empty marker interfaces.
Now, the consumer needs to use both strategies, so instead of controlling dependencies from the composition root, the consumer requests the markers:
public class Consumer
{
public IStrategyA StrategyA { get; }
public IStrategyB StrategyB { get; }
public Consumer(IStrategyA strategyA, IStrategyB strategyB)
{
StrategyA = strategyA ??
throw new ArgumentNullException(nameof(strategyA));
StrategyB = strategyB ??
throw new ArgumentNullException(nameof(strategyB));
}
}
The Consumer class exposes the strategies through properties to assert its composition later. Let’s test that out by building a dependency tree, simulating the composition root, and then asserting the value of the consumer properties:
[Fact]
public void ConsumerTest()
{
// Arrange
var serviceProvider = new ServiceCollection()
.AddSingleton<IStrategyA, StrategyA>()
.AddSingleton<IStrategyB, StrategyB>()
.AddSingleton<Consumer>()
.BuildServiceProvider();
// Act
var consumer = serviceProvider.GetRequiredService<Consumer>();
// Assert
Assert.IsType<StrategyA>(consumer.StrategyA);
Assert.IsType<StrategyB>(consumer.StrategyB);
}
Both properties are of the expected type, but that is not the problem. The Consumer class controls what dependencies to use and when to use them by injecting markers A and B instead of two IStrategy instances. Due to that, we cannot control the dependency tree from the composition root. For example, we cannot change IStrategyA to IStrategyB and IStrategyB to IStrategyA, nor inject two IStrategyB instances or two IStrategyA instances, nor even create an IStrategyC interface to replace IStrategyA or IStrategyB.How do we fix this? Let’s start by deleting our markers and injecting two IStrategy instances instead (the changes are highlighted). After doing that, we end up with the following object structure:
public class StrategyA : IStrategy
{
public string Execute() => “StrategyA”;
}
public class StrategyB : IStrategy
{
public string Execute() => “StrategyB”;
}
public class Consumer
{
public IStrategy StrategyA { get; }
public IStrategy StrategyB { get; }
public Consumer(IStrategy strategyA, IStrategy strategyB)
{
StrategyA = strategyA ??
throw new ArgumentNullException(nameof(strategyA));
StrategyB = strategyB ??
throw new ArgumentNullException(nameof(strategyB));
}
}
The Consumer class no longer controls the narrative with the new implementation, and the composition responsibility falls back to the composition root. Unfortunately, there is no way to do contextual injections using the default dependency injection container, and I don’t want to get into a third-party library for this. But all is not lost yet; we can use a factory to help ASP.NET Core build the Consumer instance, like this:
// Arrange
var serviceProvider = new ServiceCollection()
.AddSingleton<StrategyA>()
.AddSingleton<StrategyB>()
.AddSingleton(serviceProvider =>
{
var strategyA = serviceProvider.GetRequiredService<StrategyA>();
var strategyB = serviceProvider.GetRequiredService<StrategyB>();
return new Consumer(strategyA, strategyB);
})
.BuildServiceProvider();
// Act
var consumer = serviceProvider.GetRequiredService<Consumer>();
// Assert
Assert.IsType<StrategyA>(consumer.StrategyA);
Assert.IsType<StrategyB>(consumer.StrategyB);
From this point forward, we control the program’s composition, and we can swap A with B or do anything else that we want to, as long as the implementation respects the IStrategy contract.To conclude, using markers instead of doing contextual injection breaks the inversion of control principle, making the consumer control its dependencies. That’s very close to using the new keyword to instantiate objects. Inverting the dependency control back is easy, even using the default container.
If you need to inject dependencies contextually, I started an open source project in 2020 that does that. Multiple other third-party libraries add features or replace the default IoC container altogether if needed. See the Further reading section.
Next, we start the last part of this chapter. It showcases an open-source tool that can help us build CQS-oriented applications.
Leave a Reply