I recently came across a very cool library, thanks to this video by Nick Chapsas. The library is Scrutor. In this post, I’m going to run through a version of the Open-Closed Principle that this makes possible.
An Overly Complex Hello World App
Let’s start by creating a needlessly complex app that prints Hello World. Instead of simply printing Hello World we’ll use DI to inject a service that prints it. Let’s start with the main program.cs code (in .Net 6):
using Microsoft.Extensions.DependencyInjection;
using scrutortest;
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ITestLogger, TestLogger>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var testLogger = serviceProvider.GetRequiredService<ITestLogger>();
testLogger.Log("hello world");
Impressive, eh? Here’s the interface that we now rely on:
internal interface ITestLogger
{
public void Log(string message);
}
And here is our TestLogger class:
internal class TestLogger : ITestLogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
If you implement this, and run it, you’ll see that it works fine - almost as well as the one line version. However, let’s imagine that we now have a requirement to extend this class. After every message, we need to display ---OVER--- for… some reason.
Extending Our Overly Complex App to be Even More Pointless
There’s a few ways to do this: you can obviously just change the class itself, but that breaches the Open-Closed Principle. That’s where the Decorator Pattern comes in. Scrutor allows us to create a new class that looks like this:
internal class TestLoggerExtended : ITestLogger
{
private readonly ITestLogger \_testLogger;
public TestLoggerExtended(ITestLogger testLogger)
{
\_testLogger = testLogger;
}
public void Log(string message)
{
\_testLogger.Log(message);
\_testLogger.Log("---OVER---");
}
}
There’s a few things of note here: firstly, we’re implementing the same interface as the main / first class; secondly, we’re Injecting said interface into our constructor; and finally, in the Log method, we’re calling the original class. Obviously, if you just register this in the DI container as normal, bad things will happen; so we use the Scrutor Decorate method:
using Microsoft.Extensions.DependencyInjection;
using scrutortest;
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ITestLogger, TestLogger>();
serviceCollection.Decorate<ITestLogger, TestLoggerExtended>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var testLogger = serviceProvider.GetRequiredService<ITestLogger>();
testLogger.Log("hello world");
If you now run this, you’ll see that the functionality is very similar to inheritance, but you haven’t coupled the two services directly: