Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

Generic Minimal Inversion-of-Control/Dependency Injection Interfaces implemented by the Refactored NP.IoCy and AutofacAdapter Containers

Rate me:
Please Sign up or sign in to vote.
4.97/5 (18 votes)
15 Jan 2023MIT33 min read 23.6K   27   35
I explain the Inversion of Control and propose simple but powerful interfaces for implementing IoC frameworks.
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.

Image 1

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:

C#
IConsumer iocObj = container.Resolve(typeof(IConsumer)) as IConsumer;

or:

C#
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 strings. Here, for the sake of simplicity, we always use strings, 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:

C#
IConsumer iocObj = container.Resolve<IConsumer>();  

or:

C#
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"):

C#
// create container builder
var containerBuilder = new ContainerBuilder();

// register (map) Full Resolving Key (typeof(IOrg), "MyOrg) into Org type
// the object of Org type will be created by its type's default constructor
containerBuilder.RegisterType<IOrg, Org>("MyOrg");

// create a container out of the container builder:
var container = containerBuilder.Build();

// resolve the IOrg object by the Full Resolution Key:
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.

Image 2

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:

C#
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):

Image 3

ContainerBuilder and Container

As we mentioned above, such cells are placed (registered) into the container by a ContainerBuilder when the Container is built:

C#
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:

C#
[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.:

C#
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:

C#
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:

C#
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:

  1. Singleton cell returning string "MyLogFile.txt" mapped to (typeof(string), "LogFileName") Full Resolving Key.
  2. 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.

  1. 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.
  2. 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:

  1. Autofac
  2. Microsoft.Extensions.DependencyInjection
  3. Castle Windsor
  4. Ninject
  5. Microsoft MEF
  6. 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:

  1. 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.
  2. 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:

  1. public interface IContainerBuilder<TKey> { ... }
  2. 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:

  1. public interface IContainerBuilder : IContainerBuilder<object?>{ ... }
  2. 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:

  1. Many void IContainerBuilder.Register...(...) methods allowing to register the cells.
  2. void IContainerBuilder.UnRegister(Type resolvingType, TKey? resolutionKey = default) method allowing to unregister (remove) the cell corresponding to the FullResolutionKey (resovlingType, resolutionKey).
  3. 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:

C#
namespace NP.DependencyInjection.Interfaces
{
    // unmodifyable IoC (DI) container that returns composed object
    public interface IDependencyInjectionContainer<TKey>
    {
        // composes all the properties marked with InjectAttribute 
        // for the object
        void ComposeObject(object obj);

        // returns (and if appropriate also composes) an object
        // corresponding to (resolvingType, resolutionKey) pair
        object Resolve(Type resolvingType, TKey resolutionKey = default);

        // returns (and if appropriate also composes) an object
        // corresponding to (typeof(TResolving), resolutionKey) pair
        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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
public interface ILog
{
    void WriteLog(string info);
}  

There are two different implementations for ILog: ConsoleLog and FileLog:

C#
[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:

C#
[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:

C#
[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):

Image 4

Program class of the main project has three helper static methods (on top of Main(...) method):

  1. 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.
  2. IOrg CreateOrg() creates organization, populates its OrgName, Manager, Log and Manager.Address properties and assigns some values to them.
  3. IOrg CreateOrgWithArgument([Inject(resolutionKey: "TheManager")] IPerson person) - another version of static CreateOrg(...) method with injected person argument.
  4. 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:

C#
// create container builder with keys restricted to nullable strings
var containerBuilder = new ContainerBuilder<string?>();

// register Person object to be returned by IPerson resolving type
containerBuilder.RegisterType<IPerson, Person>();
containerBuilder.RegisterType<IAddress, Address>();
containerBuilder.RegisterType<IOrg, Org>();

// Register FilesLog as a singleton to be returned by ILog type
containerBuilder.RegisterSingletonType<ILog, FileLog>();

// register ConsoleLog to be returned by (ILog type, "MyLog" key) combination
containerBuilder.RegisterType<ILog, ConsoleLog>("MyLog");

// Create container
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:

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

C#
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.

C#
containerBuilder.RegisterType<ILog, ConsoleLog>("MyLog");  

registers a Transient object of type ConsoleLog to be returned for (typeof(ILog), "MyLog") pair.

C#
// resolve and compose organization
// all its injectable properties will be populated
// this stage. 
IOrg org = container1.Resolve<IOrg>();

// make sure ProjLead is null (it does not have
// InjectAttribute)
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):

C#
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.

C#
// get another IPerson object from the container
IPerson person = container1.Resolve<IPerson>();

// make sure that the new IPerson object is not the same 
// as org.Manager (since IPerson - Person combination was not 
// registered as a singleton: containerBuilder.RegisterType<IPerson, Person>();
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:

C#
// Get another ILog (remember it was registered as a singleton:
//  containerBuilder.RegisterSingletonType<ILog, FileLog>();)
ILog log = container1.Resolve<ILog>();

// Since it is a singleton, the new log should 
// be the same as the org.Log
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:

C#
// Log2 is injected by (ILog, "MyLog") type-key combination.
// This combination has been registered as a non-singleton:
// containerBuilder.RegisterType<ILog, ConsoleLog>("MyLog");
org.Log2.Should().NotBeNull();
org.Log2.Should().BeOfType<ConsoleLog>();

ILog log2 = container1.Resolve<ILog>("MyLog");

log2.Should().NotBeNull();

// the new log should not be the same as the old one
// since it is not a singleton.
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):

C#
// assign some values to the organization's properties
org.OrgName = "Nicks Department Store";
org.Manager.PersonName = "Nick Polyak";
org.Manager.Address.City = "Miami";
org.Manager.Address.ZipCode = "12345";

// since org.Log is of FileLog type, these value
// will be printed to MyLogFile.txt file within the same
// folder as the executable
org.LogOrgInfo();

Now we shall register a singleton instance object of type ConsoleLog to be mapped to typeof(ILog) with the same containerBuilder:

C#
// replace ILog (formerly resolved to FileLog) to be resolved to 
// ConsoleLog
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:

C#
// and create another container from containerBuilder. This new container
// will reflect the change - instead of ILog resolving to FileLog
// it will be resolving to ConsoleLog within the new container - <code>container2</code>.
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:

C#
// resolve org from another Container.
IOrg orgWithConsoleLog = container2.Resolve<IOrg>();

orgWithConsoleLog.Log.Should().NotBeNull();

// check that the resolved ILog is the same instance
// as the consoleLog used for the singleton instance.
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:

C#
// assign some data to the newly resolved IOrg object
orgWithConsoleLog.OrgName = "Nicks Department Store";
orgWithConsoleLog.Manager.PersonName = "Nick Polyak";
orgWithConsoleLog.Manager.Address.City = "Miami";
orgWithConsoleLog.Manager.Address.ZipCode = "12345";

// send org data to console instead of a file.
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.

C#
containerBuilder.RegisterFactoryMethod(CreateOrg);  

Remember that the method simply creates an organization and populates it with some data:

C#
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:

C#
// create a container with registered CreateOrg method
var container3 = containerBuilder.Build();

// test that organization is not a singleton and has its
// properties correctly populated
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:

C#
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:

C#
container4.TestOrg(false, "TheOrg" /* the resolution key */); 

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:

C#
container4.TestOrg(false); // should still work because 
                           // the old registration is also there  

Now, let us re-register IOrg (without a resolutionKey) as a singleton:

C#
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):

C#
IDependencyInjectionContainer<string?> container5 = containerBuilder.Build();
// test that the resulting IOrg is a singleton
container5.TestOrg(true /* test for a singleton */);  

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.

C#
containerBuilder.RegisterSingletonFactoryMethod(CreateOrg, "TheOrg");

// create the container
var container6 = containerBuilder.Build();

// make sure the result is a singleton
container6.TestOrg(true, "TheOrg" /* resolution key*/);  

Note that the other registration mapping typeof(IOrg) (without a resolutionKey) should still be there so we can also test:

C#
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 nulls 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:

C#
// unregister FactoryMethod pointed to by  IOrg
containerBuilder.UnRegister(typeof(IOrg));

// unregister FactoryMethod pointed to by (IOrg, "TheOrg") pair
containerBuilder.UnRegister(typeof(IOrg), "TheOrg"); 

Build the new container and test that the returned IOrg objects are null for both registrations:

C#
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:

C#
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.

C#
MethodInfo createOrgMethodInfo =
    typeof(Program).GetMethod(nameof(CreateOrgWithArgument));

// register the singleton Person cell for (typeof(IPerson), "TheManager") pair
// to be injected as the argument to CreateOrgWithArgument method
containerBuilder.RegisterSingletonType<IPerson, Person>("TheManager");

// register factory methods by their MethodInfo
containerBuilder.RegisterSingletonFactoryMethodInfo<IOrg>(createOrgMethodInfo, "TheOrg");
IDependencyInjectionContainer container8 = containerBuilder.Build();
container8.TestOrg(true, "TheOrg"); // test the resulting org is a singleton  

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:

C#
containerBuilder.RegisterFactoryMethodInfo<IOrg>(createOrgMethodInfo, "TheOrg");
IDependencyInjectionContainer<string?> container9 = containerBuilder.Build();
container9.TestOrg(false, "TheOrg"); // test the resulting org is not a singleton  

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:

C#
[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:

C#
[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:

C#
// create a brand new container builder for building types with RegisterType attribute
var attributedTypesContainerBuilder = new ContainerBuilder<string?>();

// RegisterTypeAttribute will have parameters specifying the resolving type 
// and resolution Key (if applicable). It will also specify whether the
// cell should be singleton or not. 
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(AnotherOrg));
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(AnotherPerson));
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(ConsoleLog));
attributedTypesContainerBuilder.RegisterType<IAddress, Address>("TheAddress");

// create container
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:

C#
[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:

C#
[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:

C#
// get the organization also testing the composing constructors
IOrgGettersOnly orgGettersOnly =
    container10.Resolve<IOrgGettersOnly>("MyOrg");

// make sure that Manager and Address are not null
orgGettersOnly.Manager.Address.Should().NotBeNull();

// make sure ILog is a singleton.
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:

C#
[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:

C#
IContainerBuilder<string?> containerBuilder11 = new ContainerBuilder<string?>();

containerBuilder11.RegisterAttributedStaticFactoryMethodsFromClass
                   (typeof(FactoryMethods));

var container11 = containerBuilder11.Build();

IOrg org11 = container11.Resolve<IOrg>("TheOrg");

// check that the org11.OrgName was set by the factory method 
// to "Other Department Store"
org11.OrgName.Should().Be("Other Department Store");

// check that the org11.Manager.PersonName was set by the factory method to "Joe Doe"
org11.Manager.PersonName.Should().Be("Joe Doe");

// Check that the org11.Manager.City is "Providence"
org11.Manager.Address.City.Should().Be("Providence");

// get another org
IOrg anotherOrg11 = container11.Resolve<IOrg>("TheOrg");

// test that it is not the same object as previous org
// (since org is transient)
org11.Should().NotBeSameAs(anotherOrg11);

// test that the manager is the same between the two orgs
// because CreateManager(...) creates a singleton
org11.Manager.Should().BeSameAs(anotherOrg11.Manager);

// get another address
IAddress address11 = container11.Resolve<IAddress>("TheAddress");

// test that the new address object is not the same
// since CreateAddress(...) is not Singleton
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 strings:

C#
// MultiCell tests 

var containerBuilder12 = new ContainerBuilder<string>(true);

// CellType is typeof(string), resolutionKey is "MyStrings" 

// example of a single string object "Str1" added to the multi-cell
containerBuilder12.RegisterMultiCellObjInstance(typeof(string), "Str1", "MyStrings");

// example of a colleciton of string objects - {"Str2", "Str3" } 
// added to the multi-cell
containerBuilder12.RegisterMultiCellObjInstance
         (typeof(string), new[] { "Str2", "Str3" }, "MyStrings");

// build the container
var container12 = containerBuilder12.Build();

// Get the collection - of strings containing 
// all strings above - { "Str1", "Str2", "Str3" }
IEnumerable<string> myStrings = container12.Resolve<IEnumerable<string>>("MyStrings");

// test that the collection is correct
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

C#
// now test putting different log types in a multi-cell:
var containerBuilder13 = new ContainerBuilder<string>(true);

// our multicell has CellType - typeof(ILog) and resolutionKey - "MyLogs"

// we set two objects to the cell - one of ConsoleLog and the other of FileLog types
containerBuilder13.RegisterMultiCellType<ILog, ConsoleLog>("MyLogs");
containerBuilder13.RegisterMultiCellType<ILog, FileLog>("MyLogs");

// get the container 
var container13 = containerBuilder13.Build();

// get the logs
IEnumerable<ILog> logs = container13.Resolve<IEnumerable<ILog>>("MyLogs");

// check there are two ILog objects within the collection
logs.Count().Should().Be(2);

// check that the first object is of type ConsoleLog
logs.First().Should().BeOfType<ConsoleLog>();

// check that the second object is of type FileLog
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:

C#
// using this attribute, an object of type MyOrg with OrgName set to "MyOrg1",
// will be created and made part of the Multi-Cell
// defined by CellType - IOrg and resolutionKey "TheOrgs"
[RegisterMultiCellType(cellType: typeof(IOrg), "TheOrgs")]
public class MyOrg : Org
{
    public MyOrg()
    {
        OrgName = "MyOrg1";
    }
}

[HasRegisterMethods]
public static class OrgFactory
{
    // returns a single Org object with OrgName set to "MyOrg2"
    [RegisterMultiCellMethod(typeof(IOrg), "TheOrgs")]
    public static IOrg CreateSingleOrg()
    {
        return new Org { OrgName = "MyOrg2" };
    }

    // returns an array of two objects with OrgNames 
    // "MyOrg3" and "MyOrg4" correspondingly
    [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; }

    // injects the constructor with orgs argument of resolving type IEnumerable<IOrg>
    // and resolutionKey - "TheOrgs" that point us to the MultiCell created above. 
    [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:

C#
// create a containerBuilder for testing MultiCell attributes
var containerBuilder14 = new ContainerBuilder<string>(true);

// MyOrg class has 
// [RegisterMultiCellType(cellType: typeof(IOrg), "TheOrgs")] attribute
// that means that an object of that type is created with a cell 
// with type IOrg and resolutionKey "TheOrgs"
// It creates an organization with OrgName - "MyOrg1"
containerBuilder14.RegisterAttributedClass(typeof(MyOrg));

// OrgFactory is a static class containing two attributed methods: 
// CreateSingleOrg() - creating a single org with OrgName - "MyOrg2"
// and CreateOrgs() - creating an array of two organizations 
// with OrgNames "MyOrg3" and "MyOrg4"
containerBuilder14.RegisterAttributedStaticFactoryMethodsFromClass(typeof(OrgFactory));

// OrgsContainer has a property Orgs set via a constructor
// The constructor has a single argument injected via the resolving type 
// IEnumerable<IOrg> and resolutionKey "TheOrgs":
// [Inject(resolutionKey: "TheOrgs")] IEnumerable<IOrg> orgs
containerBuilder14.RegisterType<OrgsContainer, OrgsContainer>();

// build the container
var container14 = containerBuilder14.Build();

// resolve the orgs container, its Orgs property should 
// be populated with the content of the MultiCell
var orgsContainer = container14.Resolve<OrgsContainer>();

// get the org names
var orgNames = orgsContainer.Orgs.Select(org => org.OrgName).ToList();

// test that the orgNames collection contains all the organizations - 
// { "MyOrg1", "MyOrg2", "MyOrg3", "MyOrg4" }
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:

Image 5

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:

C#
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:

Image 6

Here is the code for the Program.Main(...) method:

C#
static void Main(string[] args)
{
    // create container builder
    var builder = new ContainerBuilder<string?>();

    builder.RegisterPluginsFromSubFolders("Plugins");

    // create container
    var container = builder.Build();

    // resolve and compose organization
    // all its injectable properties will be injected at
    // this stage. 
    IOrgGettersOnly orgWithGettersOnly = container.Resolve<IOrgGettersOnly>("MyOrg");

    // set values
    orgWithGettersOnly.OrgName = "Nicks Department Store";
    orgWithGettersOnly.Manager.PersonName = "Nick Polyak";
    orgWithGettersOnly.Manager.Address!.City = "Miami";
    orgWithGettersOnly.Manager.Address.ZipCode = "12245";

    // print to console.
    orgWithGettersOnly.LogOrgInfo();

    IOrg org = container.Resolve<IOrg>("TheOrg");

    // test that the properties values
    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");

    // not a singleton
    org.Should().NotBeSameAs(anotherOrg);

    // singleton
    org.Manager.Should().BeSameAs(anotherOrg.Manager);

    IAddress address2 = container.Resolve<IAddress>("TheAddress");

    // not a singleton
    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

  1. DoubleManipulationPlugin - providing two methods for manipulating doubles: Plus(...) for summing up two numbers and Times(...) for multiplying two numbers.
  2. 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:

C#
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:

Image 7

Now take a look at the main program:

C#
static void Main(string[] args)
{
    // create container builder
    var builder = new ContainerBuilder<string?>();

    // load plugins dynamically from sub-folders of Plugins folder
    // localted under the same folder that the executable
    builder.RegisterPluginsFromSubFolders("Plugins");

    // build the container
    IDependencyInjectionContainer<string?> container = builder.Build();

    // get the pluging for manipulating double numbers
    IDoubleManipulationsPlugin doubleManipulationsPlugin = 
        container.Resolve<IDoubleManipulationsPlugin>();

    // get the result of 4 * 5
    double timesResult = 
        doubleManipulationsPlugin.Times(4.0, 5.0);

    // check that 4 * 5 == 20
    timesResult.Should().Be(20.0);

    // get the result of 4 + 5
    double plusResult = doubleManipulationsPlugin.Plus(4.0, 5.0);

    // check that 4 + 5 is 9
    plusResult.Should().Be(9.0);

    // get string manipulations plugin
    IStringManipulationsPlugin stringManipulationsPlugin = 
        container.Resolve<IStringManipulationsPlugin>();

    // concatinate two strings "Str1" and "Str2
    string concatResult = stringManipulationsPlugin.Concat("Str1", "Str2");

    // verify that the concatination result is "Str1Str2"
    concatResult.Should().Be("Str1Str2");

    // repeast "Str1" 3 times
    string repeatResult = stringManipulationsPlugin.Repeat("Str1", 3);

    // verify that the result is "Str1Str1Str1"
    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:

C#
[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:

C#
[HasRegisterMethods]
public static class StringManipulationFactoryMethods
{
    [RegisterMultiCellMethod(cellType:typeof(string), resolutionKey:"MethodNames")]
    public static IEnumerable<string> GetDoubleMethodNames()
    {
        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":

C#
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

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
PraiseThank You Pin
Jeff Bowman24-Mar-23 14:49
professionalJeff Bowman24-Mar-23 14:49 
GeneralRe: Thank You Pin
Nick Polyak5-Apr-23 11:58
mvaNick Polyak5-Apr-23 11:58 
QuestionPossible typo Pin
Member 1241983520-Jan-23 16:32
Member 1241983520-Jan-23 16:32 
AnswerRe: Possible typo Pin
Nick Polyak21-Jan-23 13:42
mvaNick Polyak21-Jan-23 13:42 
QuestionScoped life-cycle is not needed ? Pin
Fabrice Avaux29-Dec-22 0:22
Fabrice Avaux29-Dec-22 0:22 
AnswerRe: Scoped life-cycle is not needed ? Pin
Nick Polyak29-Dec-22 5:34
mvaNick Polyak29-Dec-22 5:34 
GeneralRe: Scoped life-cycle is not needed ? Pin
Jeff Bowman5-Mar-23 9:51
professionalJeff Bowman5-Mar-23 9:51 
GeneralRe: Scoped life-cycle is not needed ? Pin
Nick Polyak5-Mar-23 14:57
mvaNick Polyak5-Mar-23 14:57 
GeneralRe: Scoped life-cycle is not needed ? Pin
Jeff Bowman5-Mar-23 15:37
professionalJeff Bowman5-Mar-23 15:37 
QuestionAbout anti-patterns. Pin
Paulo Zemek27-Dec-22 11:34
mvaPaulo Zemek27-Dec-22 11:34 
AnswerRe: About anti-patterns. Pin
Nick Polyak27-Dec-22 12:23
mvaNick Polyak27-Dec-22 12:23 
AnswerRe: About anti-patterns. Pin
Nick Polyak28-Dec-22 14:52
mvaNick Polyak28-Dec-22 14:52 
GeneralRe: About anti-patterns. Pin
Paulo Zemek28-Dec-22 15:03
mvaPaulo Zemek28-Dec-22 15:03 
GeneralOuch Pin
Jeff Bowman21-Dec-22 13:53
professionalJeff Bowman21-Dec-22 13:53 
GeneralRe: Ouch Pin
Nick Polyak21-Dec-22 15:04
mvaNick Polyak21-Dec-22 15:04 
GeneralRe: Ouch Pin
Jeff Bowman21-Dec-22 16:55
professionalJeff Bowman21-Dec-22 16:55 
GeneralRe: Ouch Pin
Nick Polyak21-Dec-22 17:17
mvaNick Polyak21-Dec-22 17:17 
GeneralRe: Ouch Pin
Jeff Bowman21-Dec-22 17:34
professionalJeff Bowman21-Dec-22 17:34 
GeneralRe: Ouch Pin
Nick Polyak21-Dec-22 17:56
mvaNick Polyak21-Dec-22 17:56 
GeneralRe: Ouch Pin
Nick Polyak27-Dec-22 8:47
mvaNick Polyak27-Dec-22 8:47 
GeneralRe: Ouch Pin
Jeff Bowman31-Dec-22 14:27
professionalJeff Bowman31-Dec-22 14:27 
GeneralRe: Ouch Pin
Nick Polyak31-Dec-22 17:22
mvaNick Polyak31-Dec-22 17:22 
GeneralRe: Ouch Pin
Jeff Bowman2-Mar-23 15:56
professionalJeff Bowman2-Mar-23 15:56 
GeneralRe: Ouch Pin
Nick Polyak5-Mar-23 14:54
mvaNick Polyak5-Mar-23 14:54 
GeneralRe: Ouch Pin
Jeff Bowman5-Mar-23 15:36
professionalJeff Bowman5-Mar-23 15:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.