A brief recap
Earlier last year (March 2019) I wrote the article Implementing the Open/Closed Principle in C# which was intended to introduce folks to the idea of the Open/Closed principle (the “O” in the SOLID principles) and show how we could start to use Rules in C# .NET Core.
To summarise that earlier article:
- we started out with sample code that was based on a basic
switch
statement. - We iterated on some code-structure concepts
- We finally ended up with code that used a collection of rules - these shared a common interface.
I’ve recently been introduced to some new ideas. I thought you could be interested too!
This article is intended to be fairly short blog - it’s more about sharing code than providing a deep-dive into what is possible.
Note: To be genuine and transparent in giving credit to this inspiration, I had recently watched a talk by Steve Collins at my local meetup group. Additionally, some of my smart friends (e.g. GitHub : CrypticEnigma and GitHub : P-Storm) had shared ideas with me during one of my recent Twitch streams.
Prerequisites
- We need to be familiar with C# and .NET Core.
- For continuity, we really need to have read my earlier article Implementing the Open/Closed Principle in C# .
- We’ll need to be familiar with using Dependency Injection in .NET Core.
- If that’s something we’re new to, read Microsoft Documentation : Dependency injection in ASP.NET Core
What are we doing?
- We’re going to build on previous ideas that we covered in my earlier article Implementing the Open/Closed Principle in C#
- We’re going to make a start by demonstrating a simple way that we can use use dependency injection to manually register rules.
- We’ll then show how we could improve our demo code by using reflection to scan assemblies. These can then automatically register all available rules.
- This means that we can cut out the manually-added “boiler-plate” code.
- Finally, we’ll refine this idea one-step further, by using a third-party library called Scrutor
Where did we leave our example code in the last article?
The purpose of the demonstration code that we wrote in the earlier article, was to identify fictional Star Trek spaceships, using a collection of rule-sets, rather than using
if/else
orswitch
statements.
- We can find the GitHub repo with that code here : GitHub : SiliconOrchid - OpenClosedArticle
A key part of the sample code looked like this:
public class StarshipLogic { private List<IStarshipRule> _listStarshipRules; public StarshipLogic() { _listStarshipRules = new List<IStarshipRule> { new EnterpriseRule(), new ExcelsiorRule(), new ReliantRule() }; } ...
Let’s quickly review some key points in the above code.
- We have a class in which we had a
private List<>
that represented our collection of rules.- In the constructor of that class, we then manually added each “rule” (where a “rule” is one of several classes that implement a common interface called
IStartshipRule
).
How can we change our demo code so that it instead uses the .NET Core Dependency Injection framework?
Firstly, we need to add in the appropriate packages that support dependency-injection to our project. Add in these packages:
- Microsoft.Extensions.DependencyInjection
- Microsoft.Extensions.DependencyInjection.Abstractions
Next, we’ll update our demonstration code so that, rather than having code that “registers starship rules” in the constructor of
StarshipLogic
, we’ll instead register our rules with the DI framework.In this project, the place that we register services with the DI framework is the
Main
method which can be found inprogram.cs
. We’ll also modify code in that class in a second, but first, let’s refactor theStarshipLogic()
.In the code below, we can see that we have removed the code that manually adds rules and instead replaced it with a collection that is passed to it by the DI framework via constructor-injection:
public class StarshipLogic
{
private IEnumerable<IStarshipRule> _listStarshipRules;
public StarshipLogic(IEnumerable<IStarshipRule> starshipRules)
{
_listStarshipRules = starshipRules.ToArray();
}
...
Note: The
GetStarshipNameAndClass()
method remains unchanged.
- Moving across to
Program.cs
, we make the following changes:
using System;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using OpenClosed;
using OpenClosed.StarshipRules;
class Program
{
static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();
services.AddSingleton < IStarshipRule, EnterpriseRule > ();
services.AddSingleton < IStarshipRule, ExcelsiorRule > ();
services.AddSingleton < IStarshipRule, ReliantRule > ();
services.AddSingleton < StarshipLogic > ();
ServiceProvider serviceProvider = services.BuildServiceProvider();
StarshipLogic starshipLogic = serviceProvider.GetService < StarshipLogic > ();
Regex regex = new Regex("[^a-zA-Z0-9]");
Console.WriteLine("Please enter starship registry code:");
string input = regex.Replace(Console.ReadLine().ToLower(),"");
string output = starshipLogic.GetStarshipNameAndClass(input);
Console.WriteLine(output);
Console.ReadKey(); }
}
To draw our attention to some key points in the code above:
- We started adding dependency injection to our codebase by introducing a
new ServiceCollection()
- Using the same interface
IStarshipRule
, we manually registered each class which implements that interface. Later, when we ask the DI framework for an implementation of that interface, it will provide us with a collection. - Because we are calling our
StarshipLogic
service directly from theMain()
method, we provide a way to start the DI ball rolling, by usingserviceProvider.GetService<StarshipLogic>();
.
- We can think of this as a way of asking the DI-framework to “new-up a class” for us.
- We started adding dependency injection to our codebase by introducing a
How can we upgrade our code so that it can automatically register rules, using Reflection?
Note: The following code is a Microsoft-only way of addressing this, which does not rely on any other third-party packages.
Our example code works just fine … but having to register each and every rule has some potential issues:
- As we add new implementations of rules, we also need to remember to register them with the DI container.
- This is just asking for trouble, because of the strong possibility that someone on our team will inevitably forget that this needs to happen.
- Manually adding rules adds to code bloat in the places where DI services are registered.
Fortunately, there is a much nicer way to approach this problem, which involves taking advantage of Assembly Reflection.
You can read the official documentation here at Microsoft Documentation : Reflection (C#)
Modify our
Program.cs
, so that it now looks like the following:
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using OpenClosed;
using OpenClosed.StarshipRules;
class Program
{
static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();
var starshipRules = Assembly.GetAssembly(typeof(IStarshipRule))
.GetTypes()
.Where(x => x.Namespace == "OpenClosed.StarshipRules")
.Where(x => x.IsClass)
.Where(x => !x.IsAbstract);
foreach (var starshipRule in starshipRules)
{
services.AddSingleton(serviceProvider => (IStarshipRule)ActivatorUtilities.CreateInstance(serviceProvider, starshipRule));
}
services.AddSingleton < StarshipLogic > ();
ServiceProvider serviceProvider = services.BuildServiceProvider();
StarshipLogic starshipLogic = serviceProvider.GetService < StarshipLogic > ();
Regex regex = new Regex("[^a-zA-Z0-9]");
Console.WriteLine("Please enter starship registry code:");
string input = regex.Replace(Console.ReadLine().ToLower(),"");
string output = starshipLogic.GetStarshipNameAndClass(input);
Console.WriteLine(output);
Console.ReadKey();
}
}
To draw our attention to some key points in the code above:
- We needed to add in two
using
lines to the code:- We’ve added
System.Reflection
so we can analyse the code in our build. - We’ve added
System.Linq
because we’re using a Linq query to search for the specific pieces of code that we need.
- We’ve added
- We replaced the manual-registration of rules, with a Linq query that:
- Uses
System.Reflection.Assembly.GetAssembly()
to search our assembly for the specific bit of code we’re interested in. In this particular case, we’re looking for classes that implementIStarshipRule
. - In the query, we’re specific about which namespace to look at. In this example, we’re looking for
OpenClosed.StarshipRules
. - In the query, we’re saying that we’re only interested in objects that are a class (nothing else, for example, an
enum
)
- Uses
- We iterate over the results, registering them into the DI container.
- We continue to register
services.AddSingleton<StarshipLogic>();
just as before without change.
- We needed to add in two
Note: There is also the query item
x => !x.IsAbstract
.For this demo, this query item wasn’t needed. I’ve included it here to save you potential pain in your own future examples. It’s needed because, if we have a rule-class that inherits from a base-class, the base class itself would get included in the query. This would break things.
Can we do the same thing using the third-part package Scrutor?
Scrutor is an open-source package that can be found at GitHub : Scrutor and installed as a Nuget Package from Nuget : Scrutor.
It is a popular package with around 8.7 Million downloads at the time of writing.
According to the author, the purpose of Scrutor is “Assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection”
To use Scrutor, add a package reference to “Scrutor”. At the time of writing this was version 3.2.1, but choose a version that works for you.
Modify our
Program.cs
, so that it now looks like the following:
using System;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using OpenClosed;
using OpenClosed.StarshipRules;
class Program
{
static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();
services.Scan(scan => scan.FromAssemblyOf()
.AddClasses(classes => classes.AssignableTo())
.AsImplementedInterfaces()
.WithSingletonLifetime()
);
services.AddSingleton < StarshipLogic > ();
ServiceProvider serviceProvider = services.BuildServiceProvider();
StarshipLogic starshipLogic = serviceProvider.GetService < StarshipLogic > ();
Regex regex = new Regex("[^a-zA-Z0-9]");
Console.WriteLine("Please enter starship registry code:");
string input = regex.Replace(Console.ReadLine().ToLower(),"");
string output = starshipLogic.GetStarshipNameAndClass(input);
Console.WriteLine(output);
Console.ReadKey();
}
}
- To draw our attention to some key points in the code above:
- We no longer need
using System.Linq
orusing System.Reflection
, so we can take these out.
- We no longer need
- We replace both our assembly-scanning Linq query and our iterator that registers classes, with just a handful of lines of code.
Wrapping up
If you’ve not seen code written like this before, I’m really hoping that you get the same “lightbulb moment” that I did when I was first introduced to it. When you see how relatively easy to implement, it’s really satisfying.
As ever, I would reiterate that using SOLID principles in your code is intended to be a way to help us - they are not hard and fast rules. I will bore many by repeating the advise “be pragmatic, not dogmatic”
- if we have a small project, the overhead of over-engineering your code may outweigh the benefit of rapidly producing paid-for results.
- For larger projects, where careful and considered architecture of code is important for its maintainability benefits, being able to structure code like this is just lovely.
Further Reading
Disclosure
I always disclose my position, association or bias at the time of writing; No third party compensate me or otherwise endorse me for my promotion of their services. I have no bias to recommend any of the services mentioned in this article.