We can now use any of the two implementations without impacting the rest of the program. That demonstrates the strength of DI when it comes to dependency management. Moreover, we control the lifetime of the dependencies from the composition root.If we were to use the IApplicationState interface in another class, say SomeConsumer, its usage could look similar to the following:
namespace ApplicationState;
public class SomeConsumer
{
private readonly IApplicationState _myApplicationWideService;
public SomeConsumer(IapplicationState myApplicationWideService)
{
_myApplicationWideService = myApplicationWideService ??
throw new ArgumentNullException(nameof(myApplicationWideService));
}
public void Execute()
{
if (_myApplicationWideService.Has<string>(“some-key”))
{
var someValue = _myApplicationWideService.Get<string>(“some-key”);
// Do something with someValue
}
// Do something else like:
_myApplicationWideService.Set(“some-key”, “some-value”);
// More logic here
}
}
In that code, SomeConsumer depends only on the IApplicationState interface, not ApplicationDictionary or ApplicationMemoryCache, and even less on IMemoryCache or ConcurrentDictionary<string, object>. Using DI allows us to hide the implementation by inverting the flow of dependencies. It also breaks direct coupling between concrete implementations. This approach also promotes programming against interfaces, as recommended by the Dependency Inversion Principle (DIP), and facilitates the creation of open-closed classes, in accordance with the Open/Closed Principle (OCP).Here is a diagram illustrating our application state system, making it visually easier to notice how it breaks coupling:

Figure 8.2: DI-oriented diagram representing the application state system
From this sample, let’s remember that the singleton lifetime allows us to reuse objects between requests and share them application-wide. Moreover, hiding implementation details behind interfaces can improve the flexibility of our design.It is important to note that the singleton scope is only valid in a single process, so you can’t rely purely on in-memory mechanisms for larger applications that span multiple servers. We could use the IDistributedCache interface to circumvent this limitation and persist our application state system to a persistent caching tool, like Redis.
The flaw: If we look closely at the Has<TItem> method, it returns true only when an entry is present for the specified key AND has the right type. So we could override an entry of a different type without knowing it exists.
For example, ConsumerA sets an item of type A for the key K. Elsewhere in the code, ConsumerB looks to see if an item of type B exists for the key K. The method returns false because it’s a different type. ConsumerB overrides the value of the K with an object of type B. Here’s the code representing this:
// Arrange
var sp = new ServiceCollection()
.AddSingleton<IApplicationState, ApplicationDictionary>()
.BuildServiceProvider()
;
// Step 1: Consumer A sets a string
var consumerA = sp.GetRequiredService<IApplicationState>();
consumerA.Set(“K”, “A”);
Assert.True(consumerA.Has<string>(“K”)); // true
// Step 2: Consumer B overrides the value with an int
var consumerB = sp.GetRequiredService<IApplicationState>();
if (!consumerB.Has<int>(“K”)) // Oops, key K exists but it’s of type string, not int
{
consumerB.Set(“K”, 123);
}
Assert.True(consumerB.Has<int>(“K”)); // true
// Consumer A is broken!
Assert.False(consumerA.Has<string>(“K”)); // false
Improving the design to support such a scenario could be a good practice exercise. You could, for example, remove the TItem type from the Has method or, even better, allow storing multiple items under the same key, as long as their types are different.
Let’s now explore the next project.