Revisiting the Open/Closed Principle in C#, using Dependency Injection and Reflection



Jim Mc̮̑̑̑͒G
  ·  
28 July 2020  
  ·  
 8 min read

In this article we’ll revisit an older article where we introduced the topic of rules-based logic in C# .NET Core, that satisfies the Open/Closed Principle. This time around, we’ll be adding in the use of Dependency Injection and Assembly Reflection to further streamline things



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



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 or switch statements.

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).



image showing a hypodermic needle

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 in program.cs. We’ll also modify code in that class in a second, but first, let’s refactor the StarshipLogic().

    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 the Main() method, we provide a way to start the DI ball rolling, by using serviceProvider.GetService<StarshipLogic>();.
      • We can think of this as a way of asking the DI-framework to “new-up a class” for us.



image showing reflection of botos in a puddle

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.


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 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 implement IStarshipRule.
      • 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)
    • We iterate over the results, registering them into the DI container.
    • We continue to register services.AddSingleton<StarshipLogic>(); just as before without change.

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.



image showing a microscope

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 or using System.Reflection, so we can take these out.
  • 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

If you find my content helpful, please consider sharing the love with a GitHub Sponsorship - equivalent to a cup of coffee every other week.





Archives

2020 (23)
2019 (27)