This article explains Inversion of Control, its benefits and possible pitfalls. It talks about current popular IoC frameworks. I propose a minimal IoC interface allowing to build a very simple yet powerful IoC container providing all the features needed for the separation of concerns and replacing parts of the functionality easily. I also present two frameworks that implement those interfaces - very compact and simple NP.IoCy and AutofacAdapter built around Autofac.
Introduction
The Purpose of this Article
The main problem of the Inversion of Control / Dependency Injection is that often, it is not used correctly to increase the separation of concerns and to enable replacing parts of the application easily.
Another problem with IoC/DI frameworks is that they are unnecessary complex. Only a fraction of their functionality is being used productively. Some recent developments in the area or IoC/DI frameworks are not very positive (I will be talking about it later in the article).
The main purpose of this article is to emphasize the features of IoC that are really helpful and to describe how to utilize them in order to improve the software.
I also propose a minimal, but powerful, set of IoC/DI features sufficient for any well built IoC application. Two implementations of this set of features are provided - one using a new NP.IoCy
framework and another is built around Autofac.
A Brief Description of the Article
This article explains the Inversion of Control (IoC) and Dependency Injection (DI) in detail: what they are and why and when they are needed. It also discusses the various existing IoC/DI frameworks and features that make them useful.
We consider several anti-patterns that incorrect application of IoC can lead to and discuss how to avoid them.
A minimal IoC/DI interface containing a small set of features that make the IoC powerful while keeping it simple at the same time is proposed. This interface is contained within the NP.DependencyInjection package.
This minimal IoC interface is implemented by the NP.IoCy library (a new release with improved API). The NP.IoCy nuget package takes only a tiny fraction (less than 0.1) of the space of Autofac, Castle Windsor, Ninject or Microsoft.Extensions.DependencyInjection
packages.
I built the first version of NP.IoCy
several years ago, but the new version is more powerful, has separation between modifiable ContainerBuilder
and unmodifiable Container
and has method names more in line with other popular frameworks like Autofac and Castle Windsor.
To demonstrate how easy it is to implement the minimal IoC/DI interface around some other popular IoC frameworks, I also provide a different implementation in the NP.DependencyInjection.AutofacAdapter that acts in exactly the same fashion but is built on top of Autofac package.
I present and explain the samples that run for both NP.IoCy
and NP.DependencyInjection.AutofacAdaptor
packages.
What are the Inversion of Control (IoC) and Dependency Injection (DI)?
IoC is a technique that relinquishes creation of some of the objects to a singleton called an IoC Container. An IoC Container usually produces and returns an object that implements a certain interface or an abstract
class so that the consumers of the Container
created objects do not need to know their real implementation - only the interface that they implement.
In fact, the consumers of the container objects, might not even have access to the types that implement those objects - those types can be dynamically loaded from some external libraries.
In order to request an IoC object, a consumer usually passes to the container the type of the interface that the IoC object needs to implement, plus an optional parameter - key
:
IConsumer iocObj = container.Resolve(typeof(IConsumer)) as IConsumer;
or:
IConsumer iocObj = container.Resolve(typeof(IConsumer), key) as IConsumer;
IConsumer
is the IoC object's interface visible to the consumer functionality. We call it Resolving Type. The key
argument passed to the second version of container.Resolve(...)
method is called the Resolution Key. The most common Resolution Keys are either Enumerations or string
s. Here, for the sake of simplicity, we always use string
s, while in real life, it is better and more type safe to use Enumerations.
Together, the pair (ResolvingType, ResolutionKey)
(including the case when the ResolutionKey
is null
- the case equivalent to the first version container.Resolve(...)
method without the key) instructs the IoC container in a unique way of creating the object. We call such pair - the Full Resolving Key.
Important Note: The IoC container, in a sense, represents a map (or a dictionary) of Full Resolving Keys into cells containing instructions for creating and populating the corresponding objects.
The real type of the created object (usually not used at all after the object's creation) is called object's Implementation Type.
Note that the key
is necessary in order to be able to create the IoC objects in different ways for the same Resolving Type.
Also, note that C# generics allow us to simplify Resolve(...)
methods above for the frequent case when the Resolving Type is known at compile time as:
IConsumer iocObj = container.Resolve<IConsumer>();
or:
IConsumer iocObj = container.Resolve<IConsumer>(key);
The container is usually created from another object called ContainerBuilder
. The mappings of the Full Resolving Types into the instructions about creating objects are set within the ContainerBuilder
(the process of creating such mappings is usually called Registration).
Here is a very brief example of Registering Implementation Type Org
to be mapped into a Full Resolving Key of (typeof(IOrg), "MyOrg")
:
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<IOrg, Org>("MyOrg");
var container = containerBuilder.Build();
IOrg org = container.Resolve<IOrg>("MyOrg");
In compliance with our definitions above, typeof(IOrg)
is the Resolving Type, "MyOrg
" is the Resolution Key, and typeof(Org) is the Implementation Type.
We shall provide many more examples of registering objects into the ContainerBuilder
and resolving objects from the Container
below, so you do not have to completely understand it at this point.
Note that the ContainerBuilder
can be modified (its registrations can be added, or removed or overridden), but the Container
object is unmodifiable so that it can be accessed by multiple threads without locking.
What IoC and DI are Used For
The flexibility of using an IoC container for creating objects, allows us to modify the container to return different objects implementing the same interfaces when needed. For example, if you have backend proxies returned by the IoC container, you can replace them by the mock backend returning mock results instantaneously without any changes to the IoC object consumers' functionality for test purposes. Replacing the objects' implementations via the container is called the Dependency Injection.
Another, even more important purpose of an IoC container is to split the application into a number of very loosely dependent (almost independent) dynamically loaded parts (improving the separation of concerns). Such parts are usually called Plugins. This is very important for a visual application where different views and view models can be continuously coded, tested and extended on their own and then dynamically loaded by the IoC container into the common visual shell to become part of the same application.
Plugins are also important for the servers where the functionality can also be assembled as (almost) independent plugins handling different types of the requests.
Hierarchical Object Composition
Usually, when an object is returned by an IoC container, it is also recursively composed, i.e., some of the values of its properties are also returned by the container and if those values are composite objects, their properties are also set from the container.
Consider a case in point:
public interface IOrg
{
string OrgName { get; set; }
IPerson TopManager { get; set; }
}
public interface IPerson
{
string FirstName { get; set; }
string LastName { get; set; }
IAddress Address { get; set; }
}
public interface IAddress
{
int StreetNumber { get; set; }
string StreetName { get; set; }
string City { get; set; }
}
When you request IOrg
object (provided there are proper Inject
attributes within the implementation classes), the IoC container will also populate its TopManager
field by resolving IPerson
type and within the IPerson
object, it will also populate Address
field by resolving IAddress
type.
Object Creation Implementation and Modes
What this Sub-Section is About
In this sub-section, I describe in short, the main principles of designing and implementing an IoC framework which I used for the Minimal IoC/DI interfaces and then for the NP.IoCy
and NP.DependencyInjection.AutofacAdapter
implementations. A more detailed description of the IoC/DI interfaces and their usage will be provided in a future section below.
Keys to Cells Container Map
As was mentioned above, the IoC container, in a sense, represents a map (a dictionary) of Full Resolving Keys ((ResolvingType, ResolutionKey)
pairs) into container cells; each cell providing instructions for creating and composing the corresponding IoC object(s):
ContainerBuilder and Container
As we mentioned above, such cells are placed (registered) into the container by a ContainerBuilder
when the Container
is built:
IContainer container = containerBuilder.Build();
After the container is built by the ContainerBuilder
, it is not modifiable - so it is thread safe and its Resolve(...)
methods can be called in multiple threads. If needed, one can modify the containerBuilder
object by registering new cells or overriding old registrations and creating from it a different container. Being able to create multiple containers with some modifications is especially important for testing.
Container.Resolve(...)
methods find the corresponding cell within the container by the Full Resolving Key and then call some cell.GetObj(...)
method to create (if needed) and return the IoC
object. The Full Resolving Key is used for finding the cell and the cell knows exactly how to create (if needed) and return the object.
Container Objects with Different Life-Cycle Modes
In terms of the IoC object's life-cycle, there are two types of cells - those creating and returning Singleton objects and those creating and returning a new (Transient) object every time the container.Resolve(...)
method is called.
Singleton objects are created only once (either when the container is created or the first time container.Resolve(...)
is called to return such object). The Singleton cells keep their singleton objects inside themselves and return the same object every time it is requested. The Singleton object's reference is always kept within its container cell and once created, the Singleton object continues existing until the container remains existing - usually for the duration of the lifetime of the application.
Transient objects are created and composed every time container.Resolve(...)
method is called to return them. Container does not retain a reference to such objects and they exist until all objects containing references to them are removed.
Some frameworks - including Microsoft's new Microsoft.Extensions.DependencyInjection
also allow to have Scoped objects and child scopes. Scoped objects are similar to Singletons within the same scope but are different within different scopes. In my experience, Scoped life-cycle is not needed and is only created to compensate (to some degree) for some missing features. We shall talk about it below.
Various Ways of Creating IoC objects by the Cell
The cell can create an object by the object's implementation type. It chooses one of its constructors, e.g., the one marked by some Attribute and call it to create the object. If the constructor does not have any arguments, it will simply call Activator.CreateInstance(Type objType)
to create the object. If the constructor has arguments - it will first resolve those arguments and then call Activator.CreateInstance(Type objType, new object[]{arg1, arg2, ... argN})
.
Another way to create an object is by calling a static
method, e.g., by using Reflection
API: MethodInfo.Invoke(...)
. If the method has arguments, they will be resolved first by the container.
ContainerBuilder Description
As was mentioned above, the cells and the Full Resolution Key to Cell mapping are usually defined within a Container Builder. The container builder produces an immutable Container
by its ContainerBuilder.Build()
method. The resulting container cannot be modified (e.g., so that it would be working in multiple threads without any locking), but the same ContainerBuilder
object can be modified to produce another Container
with some modifications.
The process of creating a map between a FullResolutionKey
and a container cell is called Registration
. So the ContainerBulder
usually consists of a number of Register...(...)
methods specifying the FullResolutionKey
and the parameters of the cell that it needs to create. If the same FullResolutionKey
already exists, it will override it with the new cell parameters. It should also have an UnRegister(...)
method to remove a former registration.
Usage of the Attributes
One thing that I liked about MEF - Microsoft's old IoC Composition framework was the very elegant use of the Attributes for composing the objects. So I decided to use the attributes also within the minimal IoC interface.
Here is a very brief example of attribute usage for storing and injecting a Registered Type:
[RegisterType(isSingleton: true, resolutionKey:"MyConsoleLog")]
public class ConsoleLog : ILog
{
public ConsoleLog()
{
}
public void WriteLog(string info)
{
Console.WriteLine(info);
}
}
When registered as an attributed type (e.g., by calling containerBuilder.RegisterAttributedClass(typeof(ConsoleLog));
), it will create a Singleton
cell returing an object of type ConsoleLog
for the FullResolutionKey
represented by (typeof(ILog), "MyConsoleLog")
pair.
Now to inject it to become part of another object, we can use InjectAttribute
, e.g.:
public class MyObj : IMyObj
{
[Inject(resolvingType:typeof(ILog), resolutionKey:"MyConsoleLog")]
public ILog MyLog { get; set; }
}
When MyObj
object is resolved by the container, it will inject the ConsoleLog
into its MyLog
property because of the InjectAttribute
. Since ILog
is the type of MyLog
property, we can skip mentioning the resolvingType
within InjectAttribute
- the following code will still work:
public class MyObj : IMyObj
{
[Inject(resolutionKey:"MyConsoleLog")]
public ILog MyLog { get; set; }
}
Now I want to show another example with the Log
object produced by a static
method:
public class FileLog : ILog
{
const string FileName { get; };
public FileLog(string fileName)
{
if (File.Exists(FileName))
{
File.Delete(FileName);
}
}
public void WriteLog(string info)
{
using(StreamWriter writer = new StreamWriter(FileName, true))
{
writer.WriteLine(info);
}
}
}
[HasRegisterMethods]
public static class FactoryMethods
{
[RegisterMethod(resolutionKey:"LogFileName",
isSingleton:true, ResolvingType = typeof(string))]
public static string CreateFileName() => "MyLogFile.txt";
[RegisterMethod(resolutionKey:"MyFileLog", isSingleton:true,
ResolvingType = typeof(string))]
public static ILog GetFileLog([Inject(typeof(string),
resolutionKey:"LogFileName"]string fileName)
{
return new FileLog(fileName);
}
}
public class MyObj : IMyObj
{
[Inject(resolutionKey:"MyFileLog")]
public ILog MyLog { get; set; }
}
Adding the FactoryMethods
type to the ContainerBuilder
by e.g., containerBuilder.RegisterAttributedStaticFactoryMethodsFromClass(typeof(FactoryMethods))
will register two cells:
- Singleton cell returning string "MyLogFile.txt" mapped to
(typeof(string), "LogFileName
")
Full Resolving Key. - Singleton cell returning
FileLog
object writing to the "MyLogFile.txt" file (injected from the previous cell) mapped to (typeof(ILog), "MyFileLog")
Full Resolving Key.
Now if MyObj
object is built and returned by the container, it will Inject the FileLog
created by the GetFileLog(...)
method as a value for its MyLog
property, because of its [Inject(resolutionKey:"MyFileLog")]
attribute.
HasRegisterMethod
attribute above static
class FactiryMethods
is simply there to indicate that the static
class needs to be scanned for methods marked with RegisterMethod
attributes when the whole assembly is loaded into the container.
IoC Anti-Patterns
Lack of Separation of Concerns
The major problem with using the IoC/DI pattern in project architecture is using it and not taking advantage of it. I saw several projects in which Dependency Injection was severely overused and employed practically everywhere. This made everything dependent on everything else so that:
- Mocking was not possible because when you tried to replace a part, you had to also replace everything else.
- Testing, debugging and extending the individual parts (e.g., Views and View Models) was not possible because they depended on many other Views and View Models.
In such a case - it is absolutely unnecessary to use IoC which only adds another layer of complexity without providing any benefits.
In fact, every object that IoC container returns should be either easily mockable or almost independent of other peer objects (connected with other objects only via thin interfaces) so that it can be tested/debugged/extended on its own and then plugged into the common shell by the IoC container.
Is Service Locator an Anti-Pattern?
Service locator pattern - is the ability to access the IoC container globally. A simple way to implement a service locator is by providing a static reference to the IoC container.
Most people think that Service Locator is an anti-pattern. This is not quite true. Service Locator is an anti-pattern if and only if the objects composed and returned by the container, use the global reference to the container itself. If this is avoided, global reference to the container is ok.
Of course, if the project's team is large and uneven, and the global reference to the container is provided, some people might eventually use it within IoC container objects and the project is risking to decompose into a mess. There are several ways to avoid it.
- The global reference to the container should be defined in a project with limited visibility, e.g. in the main project or in some other projects not visible from where the interfaces and implementations of IoC objects are defined. In general, the project dependencies should be fully in control of the team's architects or tech leads; individual developers should not be allowed to change the dependencies between the projects, so if the team is well organized, this condition can be enforced pretty easily.
- By thorough code reviews.
Review of Current IoC/DI Frameworks
The following frameworks for IoC container are currently popular or were popular in the past:
- Autofac
Microsoft.Extensions.DependencyInjection
- Castle Windsor
- Ninject
- Microsoft MEF
- Microsoft Unity
All the frameworks above have the same drawback - they are much more complex and provide much more functionality than good IoC/DI requires. Only one of them - MEF - utilizes the attributes which make composing objects much simpler and much more elegant.
The IoC framework that is currently most popular is undoubtedly Microsoft.Extensions.DependencyInjection
. From my point of view, it became popular only because it is a new framework pushed by Microsoft, not for its own merits.
In addition to having the same problem as the rest of the IoC frameworks (being much more complex than necessary), it also has a very unfortunate shortcoming. It does not allow resolving the objects by keys - it only allows resolving objects by resolving types. So, having two different container cells returning two different implementations of, say IOrg
would be a problem. Usually, people add resolving by keys feature as custom code on top of Microsoft.Extensions.DependencyInjection
functionality. Such code is often ugly, large, full of bugs and requires additional maintenance and a person on the team who understands it.
Brief Description of the Minimal IoC/DI Interfaces
The source code for minimal IoC interfaces is located within NP.DependencyInjection git project and there is a same named nuget package available at nuget.com.
The minimal IoC/DI interfaces provide small but still very powerful IoC/DI functionality allowing to achieve all the benefits provided by the IoC.
There can be different implementations of the minimal functionality, in particular, I present two implementations:
- NP.IoCy - an implementation taking tiny disk space - less than 0.1 of major IoC frameworks including
Microsoft.Extensions.DependencyInjection
, Autofac, MEF, Ninject, Unity, Castle Windsor. - NP.DependencyInjection.AutofacAdapter - an Autofac based implementation of all the minimal interface functionality. This is for people who want to continue using the popular Autofac rather than switching to a newer and less known
NP.IoCy
.
If there is a demand, I also plan to create other implementation including those for Winsdor Castle and MEF.
The NP.DependencyInjection functionality consists of two public
interfaces and Attributes.
The interfaces are:
public interface IContainerBuilder<TKey> { ... }
public interface IDependencyInjectionContainer<TKey> { ... }
Setting generic arg type TKey
allows restricting the Resolution Keys to be of TKey
type (e.g., string
or enum
). The non generic versions of these interfaces are obtained by using <object?>
generic argument:
public interface IContainerBuilder : IContainerBuilder<object?>{ ... }
public interface IDependencyInjectionContainer : IContainerBuilder<object?> { ... }
In the future, we shall be using both generic and non-generic versions of these interfaces and classes that implement them.
The IContainerBuilder<TKey>
has:
- Many
void IContainerBuilder.Register...(...)
methods allowing to register the cells. void IContainerBuilder.UnRegister(Type resolvingType, TKey? resolutionKey = default)
method allowing to unregister (remove) the cell corresponding to the FullResolutionKey
(resovlingType, resolutionKey)
. IDependencyInjectionContainer<TKey> IContainerBuilder.Build()
method returning an immutable IDependencyInjectionContainer<TKey>
object. The IContainerBuilder<TKey>
can still be modified with some cells added, removed or replaced in order to produce another Container
object by calling IContainerBuilder.Build()
again.
I shall give more detailed explanations of IContainerBuilder
code within the samples.
The code IDependencyInjectionContainer
interface is simple enough to be published in full:
namespace NP.DependencyInjection.Interfaces
{
public interface IDependencyInjectionContainer<TKey>
{
void ComposeObject(object obj);
object Resolve(Type resolvingType, TKey resolutionKey = default);
TResolving Resolve<TResolving>(TKey resolutionKey = default);
}
}
There are following attributes provided by the NP.DependencyInjection
package: RegisterType
, RegisterMethod
, Inject
, HasRegisterMethods
and CompositeConstructor
(PluginAssembly
attribute is not used at this point). All of the above attributes aside from CompositeConstructor
attribute have already been explained within Usage of the Attributes subsection.
CompositeConstructor
is an attribute for marking a single constructor of a class to be used for building an object of that class within the container. If the constructor has arguments, those arguments should be marked by Inject
attributes and will be resolved by the container before the constructor is called.
Samples Demonstrating Abilities of the Containers that Implement Minimal IoC/DI Interfaces
Basic Descriptions of the Samples Code
There are two sets of samples - for the new NP.IoCy
and for NP.DependencyInjection.AutofacAdapter
built around Autofac. Both container frameworks implement the minimal IoC interface and provide essentially the same methods with the same behaviors.
The container objects and their interfaces are defined within two separate projects called Implementations
and Interfaces
correspondingly. The container objects describe a simple organization (class Org : IOrg {...}
) that contains a Manager
property of type IPerson
(the real implementation of type Person
). IPerson
in turn contains Address
property of type IAddress
.
The container objects and interfaces will be described in more detail below.
Sample Code Location
The sample code is located under DependencyInjectionSamples.
Implementations subfolder contains implementations for the IoC container objects, while Interfaces - contains interfaces.
Two subfolders IoCyTests and IoCyDynamicLoadingTests contain the tests for NP.IoCy
implementation.
Two subfolders AutofacAdapterTests and AutofacAdapterDynamicLoadingTests contain the tests for AutofacAdapter
implementation.
As you might have noticed from the names of the projects, both NP.IoCy
and AutofacAdapter
tests have simple tests and tests for dynamic loading capabilities of the containers.
Container Objects Interfaces and Implementations
Here, we describe the types produced by our test containers and the interfaces that they implement.
Imagine a small organization that has a Manager
and a ProjLead
(of type IPerson
). The organization objects also have some Log
property of type ILog
for writing log messages and to make it a little more complex, it also has another ILog
property called Log2
.
Here is the code for IOrg
interface:
public interface IOrg
{
string? OrgName { get; set; }
IPerson? Manager { get; set; }
IPerson? ProjLead { get; set; }
ILog? Log { get; set; }
ILog? Log2 { get; set; }
void LogOrgInfo();
}
And here is the code for Org
implementation:
public class Org : IOrg
{
[Inject]
public IPerson? Manager { get; set; } public string? OrgName { get; set; }
public IPerson? ProjLead { get; set; }
[Inject]
public ILog? Log { get; set; }
[Inject(resolutionKey:"MyLog")]
public ILog? Log2 { get; set; }
public void LogOrgInfo()
{
Log?.WriteLog($"OrgName: {OrgName}");
Log?.WriteLog($"Manager: {Manager!.PersonName}");
Log?.WriteLog($"Manager's Address: {Manager!.Address.City},
{Manager.Address.ZipCode}");
}
}
IPerson
has PersonName
property of string
type and Address
property of IAddress
type:
public interface IPerson
{
string? PersonName { get; set; }
IAddress? Address { get; set; }
}
public class Person : IPerson
{
public string? PersonName { get; set; }
[Inject]
public IAddress? Address { get; set; }
}
Now here is the code for IAddress
and Address
interface and class:
public interface IAddress
{
string? City { get; set; }
string? ZipCode { get; set; }
}
public class Address : IAddress
{
public string? City { get; set; }
public string? ZipCode { get; set; }
}
There is also ILog
interface used within the IOrg
. Here is its very simple code:
public interface ILog
{
void WriteLog(string info);
}
There are two different implementations for ILog
: ConsoleLog
and FileLog
:
[RegisterType(isSingleton: true)]
public class ConsoleLog : ILog
{
public ConsoleLog()
{
}
public void WriteLog(string info)
{
Console.WriteLine(info);
}
}
public class FileLog : ILog
{
const string FileName = "MyLogFile.txt";
public FileLog()
{
if (File.Exists(FileName))
{
File.Delete(FileName);
}
}
public void WriteLog(string info)
{
using(StreamWriter writer = new StreamWriter(FileName, true))
{
writer.WriteLine(info);
}
}
}
In order to demonstrate and test CompositeConstructor
attribute usage, we also have IOrgGettersOnly
interface and AnotherOrg
implementation where we rely on the constructor for composing the objects instead of the properties:
[RegisterType(resolutionKey:"TheOrg")]
public class AnotherOrg : IOrgGettersOnly
{
public string OrgName { get; set; }
public IPersonGettersOnly Manager { get; }
public ILog Log { get; }
[CompositeConstructor]
public AnotherOrg([Inject(resolutionKey:"AnotherPerson")]
IPersonGettersOnly manager, [Inject]ILog log)
{
Manager = manager;
Log = log;
}
public void LogOrgInfo()
{
Log?.WriteLog($"OrgName: {OrgName}");
Log?.WriteLog($"Manager: {Manager!.PersonName}");
Log?.WriteLog($"Manager's Address: {Manager!.Address.City},
{Manager.Address.ZipCode}");
}
}
To show the recursiveness of the Composition, we also have interface IPersonGettersOnly
and class that implements it - AnotherPerson
also relying on its CompositeConstructor
for composition:
[RegisterType(resolutionKey:"AnotherPerson")]
public class AnotherPerson : IPersonGettersOnly
{
public string PersonName { get; set; }
public IAddress Address { get; }
[CompositeConstructor]
public AnotherPerson([Inject(resolutionKey: "TheAddress")] IAddress address)
{
this.Address = address;
}
}
NP.IoCy Tests
IoCyTests Solution (Main Functionality)
The code for the test solution is located under IoCTests folder. All tests are within Program.cs file of the main NP.Samples.IoCyTests
project. The main projects has dependency on NP.Samples.Implementations
and NP.Samples.Interfaces
projects. It also has dependency on NP.IoCy
and FluentAssertions
nuget packages. FluentAssertions
are needed in order to conduct simple assertion tests of the functionality.
The project NP.Samples.Implementations
has a dependency on NP.Samples.Interface
project and on NP.DependencyInjection
package (for the IoC/DI attributes):
Program
class of the main project has three helper static
methods (on top of Main(...)
method):
bool IsSingleton<t>(this IDependencyInjectionContainer container, object? key = null )</t>
- checks if the container object corresponding to full resolution key of (typeof(T), key)
exists and is singleton. IOrg CreateOrg()
creates organization, populates its OrgName
, Manager
, Log
and Manager.Address
properties and assigns some values to them. IOrg CreateOrgWithArgument([Inject(resolutionKey: "TheManager")] IPerson person)
- another version of static CreateOrg(...)
method with injected person
argument. void TestOrg(this IDependencyInjectionContainer container, bool isSingleton, object key = null)
- tests that the IOrg
object under resolution key key
is a singleton or not (depending on the value of the bool
parameter isSingleton
) and also tests that the OrgName
of the obtained IOrg
object is set to be "Other Department Store".
Now take a look at the beginning of the void Main(...)
method; here is how we populate the ContainerBuilder
and create the container:
var containerBuilder = new ContainerBuilder<string?>();
containerBuilder.RegisterType<IPerson, Person>();
containerBuilder.RegisterType<IAddress, Address>();
containerBuilder.RegisterType<IOrg, Org>();
containerBuilder.RegisterSingletonType<ILog, FileLog>();
containerBuilder.RegisterType<ILog, ConsoleLog>("MyLog");
IDependencyInjectionContainer<string?> container1 = containerBuilder.Build();
Note that the Register...(...)
methods that do not have the word 'Singleton
' in them create Transient cells.
Here are a couple of examples with explanations:
containerBuilder.RegisterType<IPerson, Person>();
will register a Transient
object of type Person
created by its default constructor to be returned for Full Resolution Key of (typeof(IPerson), null)
. Note the resolution key is null
so the Full Resolution Key essentially consists only of the resolving type: typeof(IPerson)
.
containerBuilder.RegisterSingletonType<ILog, FileLog>();
registers a Singleton
object of type FileLog
to be returned for ILog
type (the map does not have a resolution key) and the FileLog
object will be created by its default constructor.
containerBuilder.RegisterType<ILog, ConsoleLog>("MyLog");
registers a Transient
object of type ConsoleLog
to be returned for (typeof(ILog), "MyLog")
pair.
IOrg org = container1.Resolve<IOrg>();
org.ProjLead.Should().BeNull();
org.ProjLead
property should be null
, since the property does not have Inject
attribute and will not be automatically injected.
Now check that Manager
property is not null
(it should have been populated because of the [Inject]
attribute):
org.Manager.Should().NotBeNull();
The code below resolves IPerson
and makes sure that it is not the same object as org.Manager
since (IPerson, null)
is not mapped to a Singleton
object.
IPerson person = container1.Resolve<IPerson>();
person.Should().NotBeSameAs(org.Manager);
Next code will obtain the resolution of ILog
type and we shall make sure that it is the reference to the same object as org.Log
since ILog
is mapped to a Singleton
object:
ILog log = container1.Resolve<ILog>();
log.Should().BeSameAs(org.Log);
Now let us take a look at Log2
property of Org
. Remember it was injected with resolution key "MyLog
"): [Inject(resolutionKey:"MyLog")]public ILog? Log2 { get; set; }
and remember that we registered ConsoleLog
to map into (typeof(ILog), "MyLog")
pair: containerBuilder.RegisterType<ilog, consolelog="">("MyLog");</ilog,>
. So Log2
should be assigned ConsoleLog
object and it should not be a singleton. This is exactly what we test next in our code:
org.Log2.Should().NotBeNull();
org.Log2.Should().BeOfType<ConsoleLog>();
ILog log2 = container1.Resolve<ILog>("MyLog");
log2.Should().NotBeNull();
log2.Should().NotBeSameAs(org.Log2);
log2.Should().BeOfType<ConsoleLog>();
Next, we provide some values for the organization and its manager and check that they are correctly printed by the FileLog
object injected into Log
property and that the file MyLogFile.txt is created and in the same folder with the executable and is populated correctly (you'll have to find and open the file to check):
org.OrgName = "Nicks Department Store";
org.Manager.PersonName = "Nick Polyak";
org.Manager.Address.City = "Miami";
org.Manager.Address.ZipCode = "12345";
org.LogOrgInfo();
Now we shall register a singleton instance object of type ConsoleLog
to be mapped to typeof(ILog)
with the same containerBuilder
:
ConsoleLog consoleLog = new ConsoleLog();
containerBuilder.RegisterSingletonInstance<ILog>(consoleLog);
Remember - it will override the old cell containing the non-singleton FileLog
with the singleton ConsoleLog
.
Then we create a new Container
from the same container builder:
var container2 = containerBuilder.Build();
Then we again get a new IOrg
object, and check that its new Log
property is set to a singleton ConsoleLog
(as opposed to FileLog
previously) object. The object instance should be the same as consoleLog
field created above:
IOrg orgWithConsoleLog = container2.Resolve<IOrg>();
orgWithConsoleLog.Log.Should().NotBeNull();
orgWithConsoleLog.Log.Should().BeSameAs(consoleLog);
We can assign value to the new orgWithConsoleLog
object and see that it prints to the console instead of the file:
orgWithConsoleLog.OrgName = "Nicks Department Store";
orgWithConsoleLog.Manager.PersonName = "Nick Polyak";
orgWithConsoleLog.Manager.Address.City = "Miami";
orgWithConsoleLog.Manager.Address.ZipCode = "12345";
orgWithConsoleLog.LogOrgInfo();
You'll have to step through the above code in the debugger to make sure that it prints onto the console when LogOrgInfo()
method is called.
Next we shall register the static
method IOrg CreateOrg()
with the ContainerBuilder
.
containerBuilder.RegisterFactoryMethod(CreateOrg);
Remember that the method simply creates an organization and populates it with some data:
public static IOrg CreateOrg()
{
IOrg org = new Org();
org.Manager = new Person();
org.OrgName = "Other Department Store";
org.Manager.PersonName = "Joe Doe";
org.Manager.Address = new Address();
org.Manager.Address.City = "Boston";
org.Manager.Address.ZipCode = "12345";
org.Log = new ConsoleLog();
return org;
}
Since no resolutionKey
argument is provided to RegisterFactoryMethod
, it will simply map the factory method to the typeof(IOrg)
type overriding the previous registration. So, once we create a new container, and call container3.Resolve<IOrg>()
on it, the factory method CreateOrg
will be used to create the Transient IOrg
object:
var container3 = containerBuilder.Build();
container3.TestOrg(false);
Calling container3.TestOrg(false)
will test precisely that the IOrg
object resolved from the new container is Transient (since the first argument to TestOrg(...)
method is false
) and has its OrgName
set to "Other Department Store" (as CreateOrg()
method sets it).
Next, let us make sure that we can register a factory method to a pair (ResolvingType, ResolutionKey)
with non-null ResolutionKey
:
containerBuilder.RegisterFactoryMethod(CreateOrg, "TheOrg");
var container4 = containerBuilder.Build();
We registered the same factory method CreateOrg()
to the pair (typeof(IOrg), "TheOrg")
.
Now test that the new cell is mapped to a Transient CreateOrg
method:
container4.TestOrg(false, "TheOrg" );
Note that we are not overriding anything since that pair has not been used as the key before. The old registration mapping type typeof(IOrg)
into factory method CreateOrg
is still there. Let us verify it also:
container4.TestOrg(false);
Now, let us re-register IOrg
(without a resolutionKey
) as a singleton
:
containerBuilder.RegisterSingletonFactoryMethod(CreateOrg);
This will override the old registration, so that when we create a new Container
, it will resolve typeof(IOrg)
using CreateOrg()
method to produce a Singleton
object (as opposed to a Transient
object before):
IDependencyInjectionContainer<string?> container5 = containerBuilder.Build();
container5.TestOrg(true );
Passing the first argument to container.TestOrg(...)
method as true
will test for a singleton.
Now let us override the keyed (named) cell with the Singleton
and test that the new container will produce a Singleton
object for that cell.
containerBuilder.RegisterSingletonFactoryMethod(CreateOrg, "TheOrg");
var container6 = containerBuilder.Build();
container6.TestOrg(true, "TheOrg" );
Note that the other registration mapping typeof(IOrg)
(without a resolutionKey
) should still be there so we can also test:
container6.TestOrg(true);
Now we are going to test that we can unregister the cells from the container builder so that the new container will return null
s when trying to resolve those cells. We shall unregister the two cells that have resolving type set to IOrg
, one with a key and one without:
containerBuilder.UnRegister(typeof(IOrg));
containerBuilder.UnRegister(typeof(IOrg), "TheOrg");
Build the new container and test that the returned IOrg
objects are null
for both registrations:
var container7 = containerBuilder.Build();
org = container7.Resolve<IOrg>();
org.Should().BeNull();
org = container7.Resolve<IOrg>("TheOrg");
org.Should().BeNull();
Next, we shall test registering a method by its reflection MethodInfo
. The power of this type of registration/resolving is that the method's arguments (marked by [Inject(...)]
attribute) are resolved recursively.
We shall use method Program.CreateOrgWithArgument(IPerson person)
for the sample:
public static IOrg CreateOrgWithArgumen
(
[Inject(resolutionKey:"TheManager")]IPerson person)
{
return new Org { OrgName = "Other Department Store", Manager = person };
}
Note that since the new CreateOrgWithArgument(...)
method injects the attribute mapped to (typeof(IPerson), "TheManager")
pair we shall have to register a cell mapped to (typeof(IPerson), "TheManager")
Full Resolving Key.
MethodInfo createOrgMethodInfo =
typeof(Program).GetMethod(nameof(CreateOrgWithArgument));
containerBuilder.RegisterSingletonType<IPerson, Person>("TheManager");
containerBuilder.RegisterSingletonFactoryMethodInfo<IOrg>(createOrgMethodInfo, "TheOrg");
IDependencyInjectionContainer container8 = containerBuilder.Build();
container8.TestOrg(true, "TheOrg");
The code sample above is testing that the containerBuilder.RegisterSingletonFactoryMethodInfo(...)
method works and produces a Singleton
cell.
Next, we shall test the containerBuilder.RegisterFactoryMethodInfo(...)
method that should produce a Transient
cell:
containerBuilder.RegisterFactoryMethodInfo<IOrg>(createOrgMethodInfo, "TheOrg");
IDependencyInjectionContainer<string?> container9 = containerBuilder.Build();
container9.TestOrg(false, "TheOrg");
Now let us test the method ContainerBuilder.RegisterAttributedClass(Type type)
. This method takes the whole class and checks if it has RegisterType
attribute. If not, it throws an exception. If it finds the attribute, it will create a container cell based on the attribute's parameter. For example, we have our AnotherOrg
class marked with RegisterType
attribute:
[RegisterType(resolutionKey:"MyOrg")]
public class AnotherOrg : IOrgGettersOnly
{
}
Based on this attribute, the container builder will create a Transient
cell mapped to (typeof(IOrgGettersOnly), "MyOrg")
Full Resolving Key.
The constructor marked with [CompositeConstructor]
attribute will be chosen to build the object. If no constructor is marked with such attribute, the default constructor will be used.
Here is our constructor for AnotherOrg
class:
[CompositeConstructor]
public AnotherOrg
(
[Inject(resolutionKey:"AnotherPerson")] IPersonGettersOnly manager,
[Inject] ILog log)
{
Manager = manager;
Log = log;
}
Its arguments marked with Inject
will be recursively composed.
Here is how our test is built:
var attributedTypesContainerBuilder = new ContainerBuilder<string?>();
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(AnotherOrg));
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(AnotherPerson));
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(ConsoleLog));
attributedTypesContainerBuilder.RegisterType<IAddress, Address>("TheAddress");
var container10 = attributedTypesContainerBuilder.Build();
We create a brand new ContainerBuilder
and then use its RegisterAttributedClass
method to build the container objects based on their attributes. Here is the RegisterType
attribute and composite constructor for the AnotherPerson
class:
[RegisterType(resolutionKey:"AnotherPerson")]
public class AnotherPerson : IPersonGettersOnly
{
public string PersonName { get; set; }
public IAddress Address { get; }
[CompositeConstructor]
public AnotherPerson([Inject(resolutionKey: "TheAddress")] IAddress address)
{
this.Address = address;
}
}
And here is the RegisterType
attribute for ConsoleLog
:
[RegisterType(isSingleton: true)]
public class ConsoleLog : ILog
{
}
ConsoleLog
is a Singleton
based on its RegisterType
attribute parameter.
Now let us get back to the main Program.cs file:
IOrgGettersOnly orgGettersOnly =
container10.Resolve<IOrgGettersOnly>("MyOrg");
orgGettersOnly.Manager.Address.Should().NotBeNull();
container10.IsSingleton<ILog>().Should().BeTrue();
We get the IOrgGettersOnly
object (resolved to AnotherOrg
), make sure that its org10.Manager.Address
property is not null
. And also make sure that ILog
in that container is a singleton.
Finally, let us test method containerBuilder.RegisterAttributedStaticFactoryMethodsFromClass(Type classContainingStaticFactoryMethods)
. This method takes a class we some static
factory methods with RegisterMethod(...)
attributes. These methods are turned into container cells based on the RegisterMethod(...)
arguments. The attributed static
factory methods are located within FactoryMethods
class under NP.Samples.Implementations
project. Here is its code:
[HasRegisterMethods]
public static class FactoryMethods
{
[RegisterMethod(resolutionKey: "TheAddress")]
public static IAddress CreateAddress()
{
return new Address { City = "Providence" };
}
[RegisterMethod(isSingleton: true, resolutionKey: "TheManager")]
public static IPerson CreateManager
(
[Inject(resolutionKey: "TheAddress")] IAddress address)
{
return new Person { PersonName = "Joe Doe", Address = address };
}
[RegisterMethod(resolutionKey: "TheOrg")]
public static IOrg CreateOrg([Inject(resolutionKey: "TheManager")] IPerson person)
{
return new Org { OrgName = "Other Department Store", Manager = person };
}
}
The factory methods create three named cells for IOrg
, IPerson
and IAddress
types. The IPerson
cell is Singleton
- there rest two are Transient
.
Here is the test code:
IContainerBuilder<string?> containerBuilder11 = new ContainerBuilder<string?>();
containerBuilder11.RegisterAttributedStaticFactoryMethodsFromClass
(typeof(FactoryMethods));
var container11 = containerBuilder11.Build();
IOrg org11 = container11.Resolve<IOrg>("TheOrg");
org11.OrgName.Should().Be("Other Department Store");
org11.Manager.PersonName.Should().Be("Joe Doe");
org11.Manager.Address.City.Should().Be("Providence");
IOrg anotherOrg11 = container11.Resolve<IOrg>("TheOrg");
org11.Should().NotBeSameAs(anotherOrg11);
org11.Manager.Should().BeSameAs(anotherOrg11.Manager);
IAddress address11 = container11.Resolve<IAddress>("TheAddress");
address11.Should().NotBeSameAs(org11.Manager.Address);
Again, we create a brand new ContainerBuilder
and then use method containerBuilder11.RegisterAttributedStaticFactoryMethodsFromClass(typeof(FactoryMethods))
to create the cells based on the RegisterMethod
attributes.
Then we test the objects' properties making sure that their values are the same as what they were set by the factory methods.
IoCyTests Solution Continued (Multi-Cell Functionality)
Recently, I added new functionality allowing to unite several Registered objects into a collection of objects - the Multi-Cell functionality. I correspondingly added tests to the MainProgram of the IoCyTests solution to demonstrate and test such functionality. Those who worked with MEF, might see some similarity to ImportMany
attribute. In some ways it is similar, but more rationally built. In particular in NP.IoCy
, the property or argument injection do not have to be different for the Multi-Cell, so that at the injection stage, the software does not need to know whether it is dealing with a Multi-Cell or a usual cell returning a collection of items. The difference is only when the multi-cells are registered: then we use methods or attributes containing MultiCell
in their names.
Note that all cells registered as parts of a Multi-Cell are singletons.
Note, that Multi-Cell functionality is only part of NP.IoCy - I did not reproduce it for Autofac (at least so far).
The Multi-Cell samples (from the same Program.cs file) start with showing how to unite multiple string into a Multi-Cell returning a collection of those string
s:
var containerBuilder12 = new ContainerBuilder<string>(true);
containerBuilder12.RegisterMultiCellObjInstance(typeof(string), "Str1", "MyStrings");
containerBuilder12.RegisterMultiCellObjInstance
(typeof(string), new[] { "Str2", "Str3" }, "MyStrings");
var container12 = containerBuilder12.Build();
IEnumerable<string> myStrings = container12.Resolve<IEnumerable<string>>("MyStrings");
myStrings.Should().BeEquivalentTo(new[] { "Str1", "Str2", "Str3" });
Note that in the code above, we using RegisterMultiCellObjInstance
first to register a single object "Str1"
and then an array of objects - new[]{"Str2", "Str3"}
. Also note that the CellType
argument to the method is set to typeof(string)
. CellType
specifies the type of a single cell within the resulting collection. Since we are using the same CellType
and the same resolutionKey
("MyStrings")
, both results of calling RegisterMultiCellObjInstance(...)
are mapped to the same Multi-Cell. The resulting collection will be of type IEnumerable<CellType>
, or in our case - IEnumerable<string>
. This is why we resolve it by passing IEnumerable<string>
type argument: IEnumerable<string> myStrings = container12.Resolve<IEnumerable<string>>("MyStrings")
;.
The last line - verifies that the resolved collection of strings indeed contains "Str1"
, "Str2"
and "Str3"
values.
The next sample shows how to create a Multi-Cell out of two different implementations of ILog
interface: ConsoleLog
and FileLog
. This is pretty similar to what ImportMany
attribute does in MEF. Here is the code
var containerBuilder13 = new ContainerBuilder<string>(true);
containerBuilder13.RegisterMultiCellType<ILog, ConsoleLog>("MyLogs");
containerBuilder13.RegisterMultiCellType<ILog, FileLog>("MyLogs");
var container13 = containerBuilder13.Build();
IEnumerable<ILog> logs = container13.Resolve<IEnumerable<ILog>>("MyLogs");
logs.Count().Should().Be(2);
logs.First().Should().BeOfType<ConsoleLog>();
logs.Skip(1).First().Should().BeOfType<FileLog>();
Both calls to RegisterMultiCellType<ILog, ...>("MyLogs")
have the same CellType
(ILog
) and the resolutionKey ("MyLogs"
) and because of that both resulting entries map into the same Multi-Cell. The first entry creates an object of the type ConsoleLog
and the second of the type FileLog
. Then as always, we build the container and call Resolve
method passing the collection type (IEnumerable<ILog>
) and the resolutionKey ("MyLogs"
): IEnumerable<ILog> logs = container13.Resolve<IEnumerable<ILog>>("MyLogs");
. Finally, we test that the resulting collection indeed consists of two elements and one of them is of type ConsoleLog
and the other of type FileLog
.
The last sample shows how to use Multi-Cell attributes - RegisterMultiCellType
and RegisterMultiCellMethod
. The attributed and injected code is located within OrgContainer.cs file:
[RegisterMultiCellType(cellType: typeof(IOrg), "TheOrgs")]
public class MyOrg : Org
{
public MyOrg()
{
OrgName = "MyOrg1";
}
}
[HasRegisterMethods]
public static class OrgFactory
{
[RegisterMultiCellMethod(typeof(IOrg), "TheOrgs")]
public static IOrg CreateSingleOrg()
{
return new Org { OrgName = "MyOrg2" };
}
[RegisterMultiCellMethod(typeof(IOrg), "TheOrgs")]
public static IEnumerable<IOrg> CreateOrgs()
{
return new IOrg[]
{
new Org { OrgName = "MyOrg3" },
new Org { OrgName = "MyOrg4" }
};
}
}
public class OrgsContainer
{
public IEnumerable<IOrg> Orgs { get; }
[CompositeConstructor]
public OrgsContainer([Inject(resolutionKey: "TheOrgs")] IEnumerable<IOrg> orgs)
{
Orgs = orgs;
}
}
The attributed classes once registered into the container builder, will create a Multi-Cell pointed to by CellType IOrg
and resolutionKey "TheOrgs"
. There will be 4 cells - each implementing IOrg
interface containing OrgName
properties set to "MyOrg1"
, "MyOrg2"
, "MyOrg3"
and "MyOrg4"
correspondingly. The CompositeConstructor
of OrgsContainer
class injects the contents of the Multi-Cell into the orgs
argument of the constructor, which is assigned to the Orgs
property of the class.
Here is the Program.cs code that reads those classes and methods with attributes, creates the Multi-Cell and injects it into the OrgsContainer
object:
var containerBuilder14 = new ContainerBuilder<string>(true);
containerBuilder14.RegisterAttributedClass(typeof(MyOrg));
containerBuilder14.RegisterAttributedStaticFactoryMethodsFromClass(typeof(OrgFactory));
containerBuilder14.RegisterType<OrgsContainer, OrgsContainer>();
var container14 = containerBuilder14.Build();
var orgsContainer = container14.Resolve<OrgsContainer>();
var orgNames = orgsContainer.Orgs.Select(org => org.OrgName).ToList();
orgNames.Should().BeEquivalentTo(new[] { "MyOrg1", "MyOrg2", "MyOrg3", "MyOrg4" });
We use containerBuilder.RegisterAttributedClass(...)
method to read the attributed class MyOrg
and containerBuilder.RegisterAttributedStaticFactoryMethodsFromClass(...)
to read the static
class with attributed factory methods. We also add a Transient cell producing OrgsContainer
classes: containerBuilder14.RegisterType<OrgsContainer, OrgsContainer>();
. Then we build the container, and resolve the OrgsContainer
object from it: var orgsContainer = container14.Resolve<OrgsContainer>();
. Finally, we check that the object's Orgs
property is indeed populated with 4 objects - each implementing IOrg
interface and having "MyOrg1"
, "MyOrg2"
, "MyOrg3"
, "MyOrg4"
OrgNames
correspondingly.
IoCyDynamicLoadingTests Solution
This IoCyDynamicLoadingTests.sln is located under the same named folder. Its purpose is to test a very important case of Container creating objects that belong to the dynamically loaded libraries (the objects' exact type is not known to the main program and one can control the objects only via the interfaces - known to the main program).
This sample demos a case when the main application dynamically loads a single plugin.
Open up the solution explorer and take a close look at the dependencies:
The main project NP.Samples.IoCyDynamicLoadingTests
no longer depends on NP.Samples.Implementations
project.
Notice that NP.Samples.Implementations
project has a Post build event:
xcopy "$(OutDir)"
"$(SolutionDir)\bin\$(Configuration)\net6.0\Plugins\$(ProjectName)\" /S /R /Y /I
This Post Build Event will copy the content of the folder containing the result of the build into Plugins\NP.Sample.Implementation folder under the folder IoCyDynamicLoadingTests\bin\Debug\net6.0 containing the executable. If the folder Plugins\NP.Sample.Implementation does not exist, it will be created:
Here is the code for the Program.Main(...)
method:
static void Main(string[] args)
{
var builder = new ContainerBuilder<string?>();
builder.RegisterPluginsFromSubFolders("Plugins");
var container = builder.Build();
IOrgGettersOnly orgWithGettersOnly = container.Resolve<IOrgGettersOnly>("MyOrg");
orgWithGettersOnly.OrgName = "Nicks Department Store";
orgWithGettersOnly.Manager.PersonName = "Nick Polyak";
orgWithGettersOnly.Manager.Address!.City = "Miami";
orgWithGettersOnly.Manager.Address.ZipCode = "12245";
orgWithGettersOnly.LogOrgInfo();
IOrg org = container.Resolve<IOrg>("TheOrg");
org.OrgName.Should().Be("Other Department Store");
org.Manager.PersonName.Should().Be("Joe Doe");
org.Manager.Address.City.Should().Be("Providence");
IOrg anotherOrg = container.Resolve<IOrg>("TheOrg");
org.Should().NotBeSameAs(anotherOrg);
org.Manager.Should().BeSameAs(anotherOrg.Manager);
IAddress address2 = container.Resolve<IAddress>("TheAddress");
address2.Should().NotBeSameAs(org.Manager.Address);
Console.WriteLine("The END");
}
We use method builder.RegisterPluginsFromSubFolders("Plugins")
to register all the public
attributed objects within every subfolder for folder "Plugins" into the ContainerBuilder
. Then we get IOrgGettersOnly
object and test that is populated by the Composite constructors.
The we get the IOrg
object and test that it is really populated by the factory methods. We also test that the IPerson
is a Singleton
while IOrg
and IAdress
objects are Transient
.
Multiple Plugins Test
A sample demonstrating loading multiple plugins is located under PluginsTest
solution. Here, we have the MainProgram
project that runs Main(...)
method two plugins each one in its own project
DoubleManipulationPlugin
- providing two methods for manipulating doubles
: Plus(...)
for summing up two numbers and Times(...)
for multiplying two numbers. StringManipulationPlugins
also providing two methods for string manipulations: Concat(...)
- for concatenating two strings and Repeat(...)
for repeating a string
several times.
These two plugins do not depend on each other and the main project does not have a dependency on them. Instead, both plugins and the main project have a dependency on the project PluginInterfaces
containing two interfaces, one for each of the plugins:
public interface IDoubleManipulationsPlugin
{
double Plus(double number1, double number2);
double Times(double number1, double number2);
}
public interface IStringManipulationsPlugin
{
string Concat(string str1, string str2);
string Repeat(string str, int numberTimesToRepeat);
}
The plugin DLLs and executable are copied by their post build events under the same named folder under plugins subfolder within the main project's output folder:
Now take a look at the main program:
static void Main(string[] args)
{
var builder = new ContainerBuilder<string?>();
builder.RegisterPluginsFromSubFolders("Plugins");
IDependencyInjectionContainer<string?> container = builder.Build();
IDoubleManipulationsPlugin doubleManipulationsPlugin =
container.Resolve<IDoubleManipulationsPlugin>();
double timesResult =
doubleManipulationsPlugin.Times(4.0, 5.0);
timesResult.Should().Be(20.0);
double plusResult = doubleManipulationsPlugin.Plus(4.0, 5.0);
plusResult.Should().Be(9.0);
IStringManipulationsPlugin stringManipulationsPlugin =
container.Resolve<IStringManipulationsPlugin>();
string concatResult = stringManipulationsPlugin.Concat("Str1", "Str2");
concatResult.Should().Be("Str1Str2");
string repeatResult = stringManipulationsPlugin.Repeat("Str1", 3);
repeatResult.Should().Be("Str1Str1Str1");
Console.WriteLine("The END");
}
The plugins are dynamically loaded from their folders and used and tested based on their interfaces.
Since the plugins do not depend on each other and the main program does not directly depend on the plugins, each of the plugins can be developed debugged and tested on its own, e.g., by some other Test or Main project and then plugins can be assembled and work together with the help of the NP.IoCy
framework.
Multiple Plugins with Multi-Cells
The Multi-Cell functionality is most useful when a Multi-Cell combines various cells across multiple plugins. This is why I added also a Multi-Cell sample to the Multiple Plugins example. Take a look at file, DoubleManipulationFactoryMethods.cs within DoubleManipulationsPlugin
project and at file StringManipulationFactoryMethods.cs within StringManipulationsPlugin
project:
DoubleManipulationFactoryMethods.cs:
[HasRegisterMethods]
public static class DoubleManipulationFactoryMethods
{
[RegisterMultiCellMethod(cellType:typeof(string), resolutionKey:"MethodNames")]
public static IEnumerable<string> GetDoubleMethodNames()
{
return new[]
{
nameof(IDoubleManipulationsPlugin.Plus),
nameof(IDoubleManipulationsPlugin.Times)
};
}
}
StringManipulationFactoryMethods.cs:
[HasRegisterMethods]
public static class StringManipulationFactoryMethods
{
[RegisterMultiCellMethod(cellType:typeof(string), resolutionKey:"MethodNames")]
public static IEnumerable<string> GetStringMethodNames()
{
return new[]
{
nameof(IStringManipulationsPlugin.Concat),
nameof(IStringManipulationsPlugin.Repeat)
};
}
}
Each of them returns an array of names of the methods available from the corresponding plugin mapped into a Multi-Cell with CellType
- string
and resolutionKey
- "MethodNames"
.
Now within Program.cs file, we find those method names by resolving type IEnumerable<string>
and resolutionKey
- "MethodNames
":
var methodNames = container.Resolve<IEnumerable<string>>("MethodNames");
methodNames.Count().Should().Be(4);
methodNames.Should().Contain(nameof(IDoubleManipulationsPlugin.Plus));
methodNames.Should().Contain(nameof(IDoubleManipulationsPlugin.Times));
methodNames.Should().Contain(nameof(IStringManipulationsPlugin.Concat));
methodNames.Should().Contain(nameof(IStringManipulationsPlugin.Repeat));
Then we check that the result has 4 entries for 4 method names and that each of the methods is present.
NP.DependencyInjection.AutofacAdapterTests
The test solution for Autofact, AutofacAdapterTests.sln and AutofacAdapterDynamicLoadingTests.sln are contained within folders of the same name under NP.Samples folder of the same NP.Samples repository.
Both tests behave exactly the same as the IoCy tests and have almost the same code. The only difference is that we are using a dependency on NP.DependencyInjection.AutofacAdapter
package instead of NP.IoCy
and to create a container, we call new AutofacContainerBuilder()
constructor instead of IoCy's new ContainerBuilder()
.
Summary
The article provides a close look into IoC/DI concepts and ways to apply them. I propose a minimal IoC/DI interface that can be easily implemented. Also, the article shows two implementations of that interface - NP.IoCy
and NP.DependencyInjection.AutfacAdapter
container.
NP.IoCy
is a very simple and small container taking less space than 0.1 of the space taken by other containers, including Autofac, Castle Windsor, Microsoft.Extensions.DependencyInjection
and MEF.
NP.DependencyInjection.AutofacAdapter
is a wrapper around Autofac. Those who use it can also use any other Autofac features and API. This library is for those who prefer to use an older and better tested library with more familiar API.
If there is a demand, I plan to create more Adapters for other popular frameworks in the future.
History
- 18th December, 2022: Initial version
- 16th January, 2023 - Added sections on Multi-Cells