Inversion of Control & Dependency Injection

Date 2022-02-13
Author Frederik Engelhardt

Introduction

Maybe it is not clear to everybody what IoC and DI are. So let me explain that quickly to get everybody on track.

IoC and DI are design principles. You will see them usually together because their functionality goes hand in hand.

  • IoC is about to change the way how you construct instances of your classes.
  • DI is about how you setup the dependencies of your classes.

The primary idea behind both of these principles is to get loose coupling.

Let me describe a simple situation:

  • You have two classes, ClassA and ClassB.
  • ClassA requires some state or logic provided by ClassB.
  • Because of that ClassA creates an instance of ClassB whenever it is created.
  • Both ClassA and ClassB require some other shared state or logic and that’s why we introduce ClassC.
  • Whenever an instance of ClassA is created it creates an instance of ClassC too and passes it through to ClassB.

Now you may think, what’s the problem with having three classes which rely on each other in a specific way?

The problem is that the implementations of these three classes are tightly coupled to each other since they create the required instances themself.

One of the first steps to solve this could be to use interfaces instead of classes. If you prefer (abstract) classes instead of interfaces, not problem, IoC and DI do work fine with that too.

Instead of creating an instance of a specific class type by constructing it with new, you could use an IoC-Factory to request an instance of that interface type. You can then configure your IoC-Registry to know which implementation to use for which type of interface. Based on your configuration you could also configure whether to get the same instance (singleton) or a new instance each time you request one (instance-per-call). This is inversion of control. It’s not the consumer class that decides about which implementation to use, it is the configuration that decides this.

But what does that mean a class “requests” an instance from the IoC-Factory? You could think that you will have to write more code, maybe more complicated code. But at this point DI comes on stage.

In the example above we created the instances using the new keyword: var instanceB = new ClassB();
To improve this, we learned that we could do something like var instanceB = iocFactory.GetInstanceOf<ITypeB>();
With DI we can add parameters to the constructor of the class using the types of your dependencies and let the IoC-Factory pass in the requested dependency based on our configuration. You could also use specific attributes on your fields and properties an let the IoC-Factory set them for you when constructing the instance of your class. This means that you can write classes in a way as if you would expect that someone “outside” will provide all dependencies to you when an instance of your class is created. You don’t have to care about that anymore.

To summarize this, loose coupling, IoC and DI doesn’t make your code more complicated, it is exactly the other way round. It simplifies it for you and you can focus on implementing the business value.

You can start writing small optimized pieces of logic and connect these small pieces together to build bigger blocks, components, processes and applications. At that point you will start thinking about orchestration. But that is another story.

Construction

You can request instances by using three methods

  1. GetInstance and GetInstanceOf<>
    Get a singleton instance. You will always get the same instance on each call.
  2. CreateInstance and CreateInstanceOf<>
    Get an instance of a per-call registration. You will always get a new instance on each call.
  3. CreateCustomInstance and CreateCustomInstanceOf<>
    Same as CreateInstance but for a non-registered types.

That you do not accidentally consume a type differently from your intention there is a check happening on consumption.

  1. To consume a type through GetInstance, you have to register it with .AsSingleton or .AsSingletonOf<>.
  2. To consume a type through CreateInstance, you have to register it with .AsInstancePerCall or .AsInstancePerCallOf<>.

Whenever you try to consume a type differently from your intention, you will get an exception.

Singleton


In this example we will have a component which provides a shared logic and we will register that component as a singleton instance.
IMyComponent.cs

namespace IOC.Singleton
{
    internal interface IMyComponent
    {
        string Concatenate(string value1, string value2);
    }
}

MyComponent.cs

namespace IOC.Singleton
{
    internal class MyComponent : IMyComponent
    {
        public string Concatenate(string value1, string value2)
        {
            return string.Concat(value1, value2);
        }
    }
}

Program.cs

using e2.Framework.Components;
using IOC.Singleton;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<IMyComponent>().AsSingletonOf<MyComponent>();
 
/*
Alternatives:
    Use an existing instance:
        registry.Register<IMyComponent>().AsSingletonOf(new MyComponent());
 
    Use a parameterless function to construct the instance:
        registry.Register<IMyComponent>().AsSingletonOf(() => new MyComponent());
 
    Use a parametrized function to construct the instance:
        registry.Register<IMyComponent>().AsSingletonOf(factory => new MyComponent());
 
    Use a Lazy-object to provide an instance.
        var lazyObject = new Lazy<MyComponent>(() => new MyComponent());
        registry.Register<IMyComponent>().AsSingletonOf(lazyObject);
*/
 
var factory = registry.Factory;
 
var myComponent = factory.GetInstanceOf<IMyComponent>();
 
Console.WriteLine(
    $"Implemented type: {myComponent.GetType()}\\\\n" +
    $"Concatenated result is \\\\"{myComponent.Concatenate("Hello ""World!")}\\\\".");
 
Console.WriteLine("\\\\nPress any key to exit the application.");
Console.ReadKey(true);

Instance per call


In this example we will have a model which provides a shared state and we will register that model as an instance per call.

IModel.cs

namespace IOC.InstancePerCall
{
    internal interface IMyModel
    {
        long ModelId {getset;}
        string? Name {getset;}
    }
}

Model.cs

namespace IOC.InstancePerCall
{
    internal class MyModel: IMyModel
    {
        public long ModelId {getset;}
 
        public string? Name {getset;}
    }
}

Program.cs

using e2.Framework.Components;
using IOC.InstancePerCall;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<IMyModel>().AsInstancePerCallOf<MyModel>();
 
/*
Alternatives:
    Use a parameterless function to construct the instance:
        registry.Register<IMyModel>().AsInstancePerCallOf(() => new MyModel());
 
    Use a parametrized function to construct the instance (factory only):
        registry.Register<IMyModel>().AsInstancePerCallOf(factory => new MyModel());
 
    Use a parametrized function to construct the instance (factory and parameter dictionary):
        registry.Register<IMyModel>().AsInstancePerCallOf((factory, parameterDictionary) => new MyModel());
*/
 
var factory = registry.Factory;
 
var model1 = factory.CreateInstanceOf<IMyModel>();
model1.ModelId = 1;
model1.Name = "Model1";
 
var model2 = factory.CreateInstanceOf<IMyModel>();
model2.ModelId = 2;
model2.Name = "Model2";
 
Console.WriteLine($"Model 1 is of type {model1.GetType()}, ModelId: {model1.ModelId}, Name: \\\\"{model1.Name}\\\\".");
Console.WriteLine($"Model 2 is of type {model2.GetType()}, ModelId: {model2.ModelId}, Name: \\\\"{model2.Name}\\\\".");
 
Console.WriteLine("\\\\nPress any key to exit the application.");
Console.ReadKey(true);

Route

Polymorphism is very powerful and there are several use cases for it.

In this example we will have a class which implements two interfaces. This class will be registered as singleton. The two interfaces will be registered as route to the class type. These routes work fine for instance-per-call registrations too. If you consume one of these interface types then your request is routed to the registration of the class.

IMyComponent1.cs

namespace IOC.Route
{
    internal interface IMyComponent1
    {
        string Concatenate(string value1, string value2);
    }
}

IMyComponent2.cs

namespace IOC.Route
{
    internal interface IMyComponent2
    {
        string Join(string value1, string value2);
    }
}

MyComponent.cs

namespace IOC.Route
{
    internal class MyComponent: IMyComponent1,
                                IMyComponent2
    {
        public string Concatenate(string value1, string value2)
        {
            return string.Concat(value1, value2);
        }
 
        public string Join(string value1, string value2)
        {
            return this.Concatenate(value1, value2);
        }
    }
}

Program.cs

using e2.Framework.Components;
using IOC.Route;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<MyComponent>().AsSingleton();
registry.Register<IMyComponent1>().AsRouteTo<MyComponent>();
registry.Register<IMyComponent2>().AsRouteTo<MyComponent>();
 
var factory = registry.Factory;
 
var myComponent1 = factory.GetInstanceOf<IMyComponent1>();
var myComponent2 = factory.GetInstanceOf<IMyComponent2>();
 
Console.WriteLine($"Component1 and Component2 are the same instance: {ReferenceEquals(myComponent1, myComponent2)}");
 
Console.WriteLine("\\nPress any key to exit the application.");
Console.ReadKey(true);

Custom instances

Maybe you would like to use the IoC/DI logic to create an instance of a class but you hesitate to register that class to the IoC-registry – for whatever reasons. For exactly these cases exists the method CreateCustomInstance (and CreateCustomInstanceOf<>).

In this example we will have a class (MyComponent) registered to the IoC-registry and another class (MyOrchestrator) which is not registered to the IoC-registry. But we would like to create an instance of that orchestrator class using the convenient IoC/DI functionality. The registered component class provides some shared logic and the orchestrator class enhances this logic and provides a more generic solution. The orchestrator class uses dependency injection of which the next chapter of this guide will be about.

IMyComponent.cs

namespace IOC.CreateCustomInstance
{
    internal interface IMyComponent
    {
        string Concatenate(string value1, string value2);
    }
}

MyComponent.cs

namespace IOC.CreateCustomInstance
{
    internal class MyComponent : IMyComponent
    {
        public string Concatenate(string value1, string value2)
        {
            return string.Concat(value1, value2);
        }
    }
}

MyOrchestrator.cs

namespace IOC.CreateCustomInstance
{
    internal class MyOrchestrator
    {
        private readonly IMyComponent _myComponent;
 
        internal MyOrchestrator(IMyComponent myComponent)
        {
            if (myComponent == nullthrow new ArgumentNullException(nameof(myComponent));
            this._myComponent = myComponent;
        }
 
        public string? Concatenate(params string[] values)
        {
            var numberOfValues = values.Length;
            if (numberOfValues == 0) return null;
 
            var result = values[0];
 
            for (var i = 1; i < numberOfValues; i++)
            {
                result = this._myComponent.Concatenate(result, values[i]);
            }
 
            return result;
        }
    }
}

Program.cs

using e2.Framework.Components;
using IOC.CreateCustomInstance;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<IMyComponent>().AsSingletonOf<MyComponent>();
 
var factory = registry.Factory;
 
var myOrchestrator = factory.CreateCustomInstanceOf<MyOrchestrator>();
 
Console.WriteLine(
    $"Implemented type: {myOrchestrator.GetType()}\\n" +
    $"Concatenated result is \\"{myOrchestrator.Concatenate("Hello"" ""World""!")}\\".");
 
Console.WriteLine("\\nPress any key to exit the application.");
Console.ReadKey(true);

Dependency injection


Now that we know how to construct instances, we need to know how to declare dependencies.

You can declare the dependencies of your class in two ways:

  1. Add a parameter to your constructor.
  2. Add a specific attribute to your field or property: CoreIOCDependency

The IoC-factory will use the type of the parameter/field/property to look for a registration in the IoC-registry.

The  CoreIOCDependency attribute can be used on fields and properties but on constructor parameters too to set IoC specifications.

When using CreateInstance and CreateCustomInstance you can inject more dependencies which might not be registered in the IoC-registry.

Singleton

In this example we have two components: MyComponent and MyOrchestrator. MyOrchestrator enhances the logic which is provided by MyComponent. Both classes are registered with interfaces to the IoC-registry. MyOrchestrator declares its dependency to IMyComponent by having a parameter of that type on its constructor. The IoC-factory will pass a value to that parameter by using the instance registered in the IoC-registry.

IMyComponent.cs

namespace IOC.Singleton.DI
{
    internal interface IMyComponent
    {
        string Concatenate(string value1, string value2);
    }
}

IMyOrchestrator.cs

namespace IOC.Singleton.DI
{
    public interface IMyOrchestrator
    {
        string? Concatenate(params string[] values);
    }
}

MyComponent.cs

namespace IOC.Singleton.DI
{
    internal class MyComponent : IMyComponent
    {
        public string Concatenate(string value1, string value2)
        {
            return string.Concat(value1, value2);
        }
    }
}

MyOrchestrator.cs

namespace IOC.Singleton.DI
{
    internal class MyOrchestrator: IMyOrchestrator
    {
        private readonly IMyComponent _myComponent;
 
        internal MyOrchestrator(IMyComponent myComponent)
        {
            if (myComponent == nullthrow new ArgumentNullException(nameof(myComponent));
            this._myComponent = myComponent;
        }
 
        public string? Concatenate(params string[] values)
        {
            var numberOfValues = values.Length;
            if (numberOfValues == 0) return null;
 
            var result = values[0];
 
            for (var i = 1; i < numberOfValues; i++)
            {
                result = this._myComponent.Concatenate(result, values[i]);
            }
 
            return result;
        }
    }
}

Program.cs

using e2.Framework.Components;
using IOC.Singleton.DI;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<IMyComponent>().AsSingletonOf<MyComponent>();
registry.Register<IMyOrchestrator>().AsSingletonOf<MyOrchestrator>();
 
var factory = registry.Factory;
 
var myOrchestrator = factory.GetInstanceOf<IMyOrchestrator>();
 
Console.WriteLine(
    $"Implemented type: {myOrchestrator.GetType()}\n" +
    $"Concatenated result is \"{myOrchestrator.Concatenate("Hello"" ""World""!")}\".");
 
Console.WriteLine("\nPress any key to exit the application.");
Console.ReadKey(true);

Instance per call

Example 1

In this example we will use DI through a constructor parameter.

IMyModel.cs

namespace IOC.InstancePerCall.DI
{
    internal interface IMyModel
    {
        long ModelId {get;}
        string? Name {get;}
    }
}

MyModel.cs

namespace IOC.InstancePerCall.DI
{
    internal class MyModel: IMyModel
    {
        public long ModelId {get;}
        public string? Name {get;}
 
        public MyModel(long modelId, string? name)
        {
            this.ModelId = modelId;
            this.Name = name;
        }
    }
}

Program.cs

using e2.Framework.Components;
using e2.Framework.Models;
using IOC.InstancePerCall.DI;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<IMyModel>().AsInstancePerCallOf<MyModel>();
 
/*
Alternatives:
    Use a parameterless function to construct the instance:
        registry.Register<IMyModel>().AsInstancePerCallOf(() => new MyModel());
 
    Use a parametrized function to construct the instance (factory only):
        registry.Register<IMyModel>().AsInstancePerCallOf(factory => new MyModel());
 
    Use a parametrized function to construct the instance (factory and parameter dictionary):
        registry.Register<IMyModel>().AsInstancePerCallOf((factory, parameterDictionary) => new MyModel());
*/
 
var factory = registry.Factory;
 
var model1 = factory.CreateInstanceOf<IMyModel>(new CoreIOCParameter("modelId", 1L), new CoreIOCParameter("name""Model1"));
var model2 = factory.CreateInstanceOf<IMyModel>(new CoreIOCParameter("modelId", 2L), new CoreIOCParameter("name""Model2"));
 
Console.WriteLine($"Model 1 is of type {model1.GetType()}, ModelId: {model1.ModelId}, Name: \"{model1.Name}\".");
Console.WriteLine($"Model 2 is of type {model2.GetType()}, ModelId: {model2.ModelId}, Name: \"{model2.Name}\".");
 
Console.WriteLine("\nPress any key to exit the application.");
Console.ReadKey(true);

Example 2

In this example we will use DI through a constructor parameter.
We will use the CoreIOCDependency attribute to use other names for the parameters.

This example is very similar to the previous example. The major difference is that we don’t want to use the parameter names of the constructor parameters, since the consumer of the type should not need to know the exact names. It’s better to use public names, i.e. the names of the interface properties.

IMyModel.cs

namespace IOC.InstancePerCall.DI
{
    internal interface IMyModel
    {
        long ModelId {get;}
        string? Name {get;}
    }
}

MyModel.cs

using e2.Framework.Attributes;
 
namespace IOC.InstancePerCall.DI
{
    internal class MyModel: IMyModel
    {
        public long ModelId {get;}
        public string? Name {get;}
 
        public MyModel([CoreIOCDependency(nameof(ModelId))] long modelId, [CoreIOCDependency(nameof(Name))] string? name)
        {
            this.ModelId = modelId;
            this.Name = name;
        }
    }
}

Program.cs

using e2.Framework.Components;
using e2.Framework.Models;
using IOC.InstancePerCall.DI;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<IMyModel>().AsInstancePerCallOf<MyModel>();
 
/*
Alternatives:
    Use a parameterless function to construct the instance:
        registry.Register<IMyModel>().AsInstancePerCallOf(() => new MyModel());
 
    Use a parametrized function to construct the instance (factory only):
        registry.Register<IMyModel>().AsInstancePerCallOf(factory => new MyModel());
 
    Use a parametrized function to construct the instance (factory and parameter dictionary):
        registry.Register<IMyModel>().AsInstancePerCallOf((factory, parameterDictionary) => new MyModel());
*/
 
var factory = registry.Factory;
 
var model1 = factory.CreateInstanceOf<IMyModel>(new CoreIOCParameter(nameof(IMyModel.ModelId), 1L), new CoreIOCParameter(nameof(IMyModel.Name), "Model1"));
var model2 = factory.CreateInstanceOf<IMyModel>(new CoreIOCParameter(nameof(IMyModel.ModelId), 2L), new CoreIOCParameter(nameof(IMyModel.Name), "Model2"));
 
Console.WriteLine($"Model 1 is of type {model1.GetType()}, ModelId: {model1.ModelId}, Name: \"{model1.Name}\".");
Console.WriteLine($"Model 2 is of type {model2.GetType()}, ModelId: {model2.ModelId}, Name: \"{model2.Name}\".");
 
Console.WriteLine("\nPress any key to exit the application.");
Console.ReadKey(true);

Example 3

In this example we will use DI through the class properties.
We will use the CoreIOCDependency attribute to declare which properties should get set by the IoC-factory. This requires that these properties have to have setters.

This example is very similar to the two previous examples. The major difference is that we don’t have a constructor on the class. Instead of that we assigned the CoreIOCDependency attribute directly to the properties.

IMyModel.cs

namespace IOC.InstancePerCall.DI
{
    internal interface IMyModel
    {
        long ModelId {get;}
        string? Name {get;}
    }
}

MyModel.cs

using e2.Framework.Attributes;
using JetBrains.Annotations;
 
namespace IOC.InstancePerCall.DI
{
    internal class MyModel: IMyModel
    {
        [CoreIOCDependency]
        [UsedImplicitly]
        public long ModelId {getprivate set;}
 
        [CoreIOCDependency]
        [UsedImplicitly]
        public string? Name {getprivate set;}
    }
}

Program.cs

using e2.Framework.Components;
using e2.Framework.Models;
using IOC.InstancePerCall.DI;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<IMyModel>().AsInstancePerCallOf<MyModel>();
 
/*
Alternatives:
    Use a parameterless function to construct the instance:
        registry.Register<IMyModel>().AsInstancePerCallOf(() => new MyModel());
 
    Use a parametrized function to construct the instance (factory only):
        registry.Register<IMyModel>().AsInstancePerCallOf(factory => new MyModel());
 
    Use a parametrized function to construct the instance (factory and parameter dictionary):
        registry.Register<IMyModel>().AsInstancePerCallOf((factory, parameterDictionary) => new MyModel());
*/
 
var factory = registry.Factory;
 
var model1 = factory.CreateInstanceOf<IMyModel>(new CoreIOCParameter(nameof(IMyModel.ModelId), 1L), new CoreIOCParameter(nameof(IMyModel.Name), "Model1"));
var model2 = factory.CreateInstanceOf<IMyModel>(new CoreIOCParameter(nameof(IMyModel.ModelId), 2L), new CoreIOCParameter(nameof(IMyModel.Name), "Model2"));
 
Console.WriteLine($"Model 1 is of type {model1.GetType()}, ModelId: {model1.ModelId}, Name: \"{model1.Name}\".");
Console.WriteLine($"Model 2 is of type {model2.GetType()}, ModelId: {model2.ModelId}, Name: \"{model2.Name}\".");
 
Console.WriteLine("\nPress any key to exit the application.");
Console.ReadKey(true);

Advanced features

To give an example for each member of the ICoreIOCRegistry- and ICoreIOCFactory-interface would exceed the range of this guide. But there are some advanced features which are not that obvious but very helpful.

Instance factory

When you request an instance from an IoC-factory the internal logic has to look for the registration. This takes time – even though is a very short time. If your implementation requires the most efficient way to create instances of a registered type, then you should have a look at the instance-factories the IoC-factory provides. By using these instance factories, you get a type-specific factory which has everything prepared and no further look-ups to the registry will be needed.

IMyModel.cs

namespace IOC.InstanceFactory
{
    internal interface IMyModel
    {
        long ModelId {getset;}
        string? Name {getset;}
    }
}

MyModel.cs

namespace IOC.InstanceFactory
{
    internal class MyModel: IMyModel
    {
        public long ModelId {getset;}
 
        public string? Name {getset;}
    }
}

Program.cs

using e2.Framework.Components;
using IOC.InstanceFactory;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<IMyModel>().AsInstancePerCallOf<MyModel>();
 
var factory = registry.Factory;
 
var instanceFactory = factory.GetInstanceFactoryOf<IMyModel>();
 
var model1 = instanceFactory.CreateInstance();
model1.ModelId = 1;
model1.Name = "Model1";
 
var model2 = instanceFactory.CreateInstance();
model2.ModelId = 2;
model2.Name = "Model2";
 
Console.WriteLine($"Model 1 is of type {model1.GetType()}, ModelId: {model1.ModelId}, Name: \"{model1.Name}\".");
Console.WriteLine($"Model 2 is of type {model2.GetType()}, ModelId: {model2.ModelId}, Name: \"{model2.Name}\".");
 
Console.WriteLine("\nPress any key to exit the application.");
Console.ReadKey(true);

Instance provider

Imagine you would like to implement a very generic component which can provide functionality based on configuration. I’m pretty sure you already implemented things like that or have work with that. The IoC-registry and -factory are things like that. So what if you could re-use the logic of registrations and type-maps for your own components?

Too abstract? Ok, I will give you a simple example.

We would like to build a calculator – for this example a very simple one. We would like to specify two numbers (double) and an operator (string) to that calculator and get back the result (double). But: we don’t want to care about which operators are supported. We could start with the basic math operations: add, subtract, multiply and divide.

The idea is now to register these math operators to the IoC-registry and let the calculator evaluate the math operator types from the registry. Whenever we want to extend our calculator, we simply add the new math operator(s) to the IoC-registry and the calculator should support it immediately.

We can do that very easily by using instance providers.

Here is the plan:

  1. Define an interface for a MathOperator and the Calculator.
  2. Implement the MathOperators for Add, Subtract, Multiply and Divide.
  3. Implement the Calculator:
    • Request the instance provider for our math operator types as constructor parameter.
    • Every time we will perform a calculation we have a look at the instance provider to check
      • if we ever evaluated the math operator types or
      • if something new got registered since our last check.
    • If we need to (re-)evaluate we query the math operator types from the instance provider and request an instance for each of these types.
    • We will store the math operator instances in a dictionary and the math operator sign (+, -, *, /) shall be the key.
    • Then we can get the math operator from the dictionary and perform the calculation.
  4. Test all math operators in a console application.

IMathOperator.cs

namespace IOC.InstanceProvider
{
    internal interface IMathOperator
    {
        string Name {get;}
        string Operator {get;}
        double Calculate(double value1, double value2);
    }
}

ICalculator.cs

namespace IOC.InstanceProvider
{
    public interface ICalculator
    {
        string GetOperatorName(string @operator);
        double Calculate(string @operator, double value1, double value2);
    }
}

AddOperator.cs

namespace IOC.InstanceProvider
{
    internal class AddOperator: IMathOperator
    {
        /// <inheritdoc />
        public string Name => "Add";
 
        /// <inheritdoc />
        public string Operator => "+";
 
        /// <inheritdoc />
        public double Calculate(double value1, double value2)
        {
            return value1 + value2;
        }
    }
}

SubtractOperator.cs

namespace IOC.InstanceProvider
{
    internal class SubtractOperator: IMathOperator
    {
        /// <inheritdoc />
        public string Name => "Subtract";
 
        /// <inheritdoc />
        public string Operator => "-";
 
        /// <inheritdoc />
        public double Calculate(double value1, double value2)
        {
            return value1 - value2;
        }
    }
}

MultiplyOperator.cs

namespace IOC.InstanceProvider
{
    internal class MultiplyOperator: IMathOperator
    {
        /// <inheritdoc />
        public string Name => "Multiply";
 
        /// <inheritdoc />
        public string Operator => "*";
 
        /// <inheritdoc />
        public double Calculate(double value1, double value2)
        {
            return value1 * value2;
        }
    }
}

DivideOperator.cs

namespace IOC.InstanceProvider
{
    internal class DivideOperator: IMathOperator
    {
        /// <inheritdoc />
        public string Name => "Divide";
 
        /// <inheritdoc />
        public string Operator => "/";
 
        /// <inheritdoc />
        public double Calculate(double value1, double value2)
        {
            return value1 / value2;
        }
    }
}

Calculator.cs

using e2.Framework.Components;
 
namespace IOC.InstanceProvider
{
    internal class Calculator: ICalculator
    {
        private readonly ICoreIOCInstanceProvider<IMathOperator> _registredMathOperators;
        private int _registryContainerVersion;
        private readonly Dictionary<string, IMathOperator> _mathOperators;
 
        internal Calculator(ICoreIOCInstanceProvider<IMathOperator> registredMathOperators)
        {
            if (registredMathOperators == nullthrow new ArgumentNullException(nameof(registredMathOperators));
 
            this._registredMathOperators = registredMathOperators;
 
            unchecked
            {
                this._registryContainerVersion = registredMathOperators.ContainerVersion - 1;
            }
 
            this._mathOperators = new Dictionary<string, IMathOperator>();
        }
 
        /// <inheritdoc />
        public string GetOperatorName(string @operator)
        {
            this.SyncMathOperators();
 
            return this._mathOperators[@operator].Name;
        }
 
        public double Calculate(string @operator, double value1, double value2)
        {
            this.SyncMathOperators();
 
            var mathOperator = this._mathOperators[@operator];
            return mathOperator.Calculate(value1, value2);
        }
 
        private void SyncMathOperators()
        {
            var currentContainerVersion = this._registredMathOperators.ContainerVersion;
            if (this._registryContainerVersion == currentContainerVersion) return;
 
            this._mathOperators.Clear();
 
            var mathOperators = this._registredMathOperators.GetMap().Select(this._registredMathOperators.GetInstance);
            foreach (var mathOperator in mathOperators)
            {
                this._mathOperators.Add(mathOperator.Operator, mathOperator);
            }
 
            this._registryContainerVersion = currentContainerVersion;
        }
    }
}

Program.cs

using e2.Framework.Components;
using IOC.InstanceProvider;
 
var registry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
registry.Register<AddOperator>().AsSingleton();
registry.Register<SubtractOperator>().AsSingleton();
registry.Register<MultiplyOperator>().AsSingleton();
registry.Register<DivideOperator>().AsSingleton();
registry.Register<ICalculator>().AsSingletonOf<Calculator>();
 
var factory = registry.Factory;
 
var calculator = factory.GetInstanceOf<ICalculator>();
 
Console.WriteLine($"{calculator.GetOperatorName("+")}: 8 + 2 = {calculator.Calculate("+", 8, 2)}");
Console.WriteLine($"{calculator.GetOperatorName("-")}: 8 - 2 = {calculator.Calculate("-", 8, 2)}");
Console.WriteLine($"{calculator.GetOperatorName("*")}: 8 * 2 = {calculator.Calculate("*", 8, 2)}");
Console.WriteLine($"{calculator.GetOperatorName("/")}: 8 / 2 = {calculator.Calculate("/", 8, 2)}");
 
Console.WriteLine("\nPress any key to exit the application.");
Console.ReadKey(true);

Overlay registry

A rather rare use case is to have multiple IoC-registries at a time.

One potential use case could be to have different implementation of a class for different customers and splitting the application in separate customer-specific scopes. But even if these scopes have specific registrations not all registrations are different and you want to avoid double resource consumption caused by double registrations.

To get around this problem you could use overlay registries. The idea is to wrap an existing registry with another registry. If that outer registry is missing a registration is uses the inner registry as fallback.

To bring up that customer-specific example from above again: you could create a root IoC-registry and create from that root registry a child IoC-registry for each customer. Every shared resource would be registered in the root registry and every customer specific resource would be registered in the child registry.

Technically you could create a registry from a registry from a registry and so on… If it fits your requirements it’s fine. The equadrat Framework supports this.

To give you a simple example: we will have one component, a root registry and two child registries. There will be a registration of that component in the root registry and in one of the child registries. This means that the other child registry will use the root registry as fallback.

IMyComponent.cs

namespace IOC.OverlayRegistry
{
    internal interface IMyComponent
    {
        string Message {get;}
    }
}

MyComponent.cs

namespace IOC.OverlayRegistry
{
    internal class MyComponent: IMyComponent
    {
        /// <inheritdoc />
        public string Message {get;}
 
        internal MyComponent(string message)
        {
            this.Message = message;
        }
    }
}

Program.cs

using e2.Framework.Components;
using IOC.OverlayRegistry;
 
var rootRegistry = new CoreIOCRegistry();
 
// Setup the IOC/DI registry.
rootRegistry.Register<IMyComponent>().AsSingletonOf(() => new MyComponent("I'm registered in the root-registry."));
 
var childRegistry1 = rootRegistry.CreateOverlayRegistry();
childRegistry1.Register<IMyComponent>().AsSingletonOf(() => new MyComponent("I'm registered in the child-registry-1."));
 
var childRegistry2 = rootRegistry.CreateOverlayRegistry();
 
var rootFactory = rootRegistry.Factory;
var childFactory1 = childRegistry1.Factory;
var childFactory2 = childRegistry2.Factory;
 
/*
    Overlay registries can be created using a factory too.
 
    var childRegistry1 = factory.CreateOverlayRegistry();
    var childRegistry2 = factory.CreateOverlayRegistry();
*/
 
var myComponentFromRoot = rootFactory.GetInstanceOf<IMyComponent>();
var myComponentFromChild1 = childFactory1.GetInstanceOf<IMyComponent>();
var myComponentFromChild2 = childFactory2.GetInstanceOf<IMyComponent>();
 
Console.WriteLine($"Component from root-registry says: {myComponentFromRoot.Message}");
Console.WriteLine($"Component from child-registry-1 says: {myComponentFromChild1.Message}");
Console.WriteLine($"Component from child-registry-2 says: {myComponentFromChild2.Message}");
 
Console.WriteLine("\nPress any key to exit the application.");
Console.ReadKey(true);

References

Source Code on GitHub https://github.com/equadrat/inversion-of-control