Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Create Simple Dependency Injection with SimpleDI

2.33/5 (3 votes)
8 Dec 2018CPOL7 min read 9.2K  
Simple Dependency Injection, the customizable way. Learn about the SimpleDI framework for creating simple and effective dependency injection.

Introduction

Good architectures strive to be loosely coupled with strong cohesion. Interfaces are the perfect tool for providing the strong cohesion, but loosely coupling is still a challenge. One solution is dependency injection (DI) which is used to load objects at run-time, allowing the implementations of an interface to be completely abstracted away from the consumer of the interface. This way, you are de-coupling the implementation of an application, from the implementation of the building blocks.

In this article, I will describe SimpleDI, a new dependency injection framework that aims to be simple to use and highly configurable. SimpleDI uses a pair of objects to create your consumables. These objects are implementations of IInjectable and IDefinitionLoader. The definition loader is a custom class that converts dependency definition to a grammar that is used by the injector to create the instance of the object. Creation of injectable objects is done through a single call to the provided SimpleDiFactory.

Background

SimpleDI started as the foundation of a set of tools being published by Gateway Programming School. These tools are the building blocks for creating applications that are both simple and robust. Einstein said everything should be simple, but not simpler than it is. That philosophy has driven the design and development of the GPS suite of tools.

Using the Code

This article assumes a basic understanding of interfaces and object oriented programming. The code samples given will be an actual example of implementing an injector and definition loader that allows definitions to be placed in a .NET configuration file (app.config, web.config, machine.config).

What Dependency Injection Does For Me...

Projects big and small need to be broken up into problem domains. Each project will have wildly varying breadth and depth of domains, but regardless, they can all benefit from dependency injection for these reasons:

  1. Loose-coupling allows for creating multiple polymorphic implementations of functionality. Your project may need to retrieve data, but it doesn't care where the data comes from. It simply needs an interface to retrieve the data through. Strong-cohesion is achieved through the creation of interfaces that provide such a contract. Dependency injection allows for the loose-coupling that separates the consumer from the provider of data.
  2. Unit testing is very important for the quality of your code and long-term sanity of the developer(s). Unit testing is hard if you do not have a good way to plug in various test stubs, alternate providers, and interchangable input/output systems. Dependency Injection allows you to define the puzzle pieces and plug in mock classes that implement a hard-coded use case of functionality so that the test always behaves consistently, thus reducing errors and improving overall quality.

What Does SimpleDI Look Like?

It is beyond the scope of this article to document every class and bit of functionality of SimpleDI. Instead, we will focus on creating an IInjectable and IDefinitionLoader set that can read configuration from a .NET executables app.config or web.config file, or even from machine.config.

The two interfaces that SimpleDI is built around allow you to create a definition loader that reads some definition of an injectable object and then inject an instance of that object into your running application. This precludes the need for your application to know the implementation details of your injectable interfaces.

A very basic, and not very practical, implementation can look like this:

C#
public class MyInjector : IInjectable
{
    public object MakeObject()
    {
        // Returns an anonymous object that has the TypeName and TypeNamespace defined by the 
        // Definition Loader.
        return new () { TypeName = this.TypeName, TypeNamespace = this.TypeNamespace };
    }

    public object MakeObject(List<Parameter> parameters)
    {
        throw new NotImplementedException();
    }

    public string TypeNamespace { get; set; }

    public string TypeName { get; set; }

    public List<List<Parameter>> Constructors { get; set; }

    // Future use only
    public List<Method> Methods { get; set; }
}

public class MyLoader : IDefinitionLoader<MyInjector>
{
    public MyInjector LoadDefinition()
    {
        return new MyInjector
        {
            TypeName = "System.String",
            TypeNamespace = "System"
        }
    }
}

As stated before, this is an extremely impractical example, but we will use it to learn about the roles and code of an injector and definition loader.

To call your defined loader and injector to get an instance of the desired object, simply use the SimpleDiFactory class's factory methods. You pass to it the type of the loader and any parameter values, and the signature of the method uses a generic that must be an instance of IInjectable.

So this seems simple, right? You just have to define something to load configuration, and something to build a class from that configuration.

SimpleDI uses a grammar that is hard-coded into the IInjectable interface. This grammar defines the type of object to be created, the namespace to that object (or the name of the assembly containing the object if that assembly is not named consistently with the namespace). It also allows for the definition of multiple constructors for the object to be created. If an object has five constructors and you are only interested in two of them, you simply need to define the two constructors you will be utilizing.

For a .NET Framework application, the simplest place to put this information is in the configuration section of an app.config or web.config file. So that's where we're going!

Custom Configuration in .NET Framework

Since the creation of .NET Core, we have had two separate paradigms for providing configuration for our apps. .NET Core uses JSON files to hold configuration which .NET Framework uses the classic configuration XML files. We will only be focusing on the .NET Framework configuration files for now.

Unlike the ad-hoc nature of the .NET Core configuration, the .NET Framework configuration requires the configuration file to implement a specific format based upon the configuration sections defined. There are many standard configuration sections that the ConfigurationManager already knows about, but if we want to include our own configuring of objects, then we must supply our own configuration section definition. When you request the custom configuration from the ConfigurationManager, it loads the configuration while checking that the XML in the config file directly implements the objects denoted in the custom configuration section.

To define the custom configuration section to be used, we simply add the SimpleDiConfigurationSection as such:

XML
<configuration>
  <configSections>
    <section name="simpleDiConfigurationSection"
             type="GPS.SimpleDI.Configuration.SimpleDiConfigurationSection,GPS.SimpleDI.Configuration"
             requirePermission="true" />
  </configSections>

We provide a name for the configSection's section definition. Next, we define the type. The configuration section defined by SimpleDI.Configuration is at GPS.SimpleDI.Configuration.SimpleDiConfigurationSection. Next, the assembly hosting the configuration section (GPS.SimpleDI.Configuration) must be defined as well in the type attribute. Note you don't have to include the file extension. The assembly loader will find the correct assembly by searching for a DLL in certain locations, and then for an EXE if a DLL was not found.

Once the configuration section is defined and imported, you may define an object. In this example, we will provide a definition for the System.String object using the default constructor and the constructor that accepts a System.Char[] as input to the string. This is done in the configuration file as such:

XML
  <simpleDiConfigurationSection>
    <objects>
      <add key="string" typeName="System.String" typeNamespace="mscorlib">
        <constructors>
          <add key="Default"/>
          <add key="WithString">
            <constructorParameters>
              <add name="value" typeName="System.Char[]" typeNamespace="mscorlib" />
            </constructorParameters>
          </add>
        </constructors>
      </add>
    </objects>
  </simpleDiConfigurationSection>
</configuration>

First, we declare that the following XML is a simpleDiConfigurationSection. The only valid child node is the <objects> tag which is a collection of object definitions that uses the add/remove paradigm of things such as the AppSettings section. The SimpleDiConfigurationSection uses the add/remove paradigm for all collections. An object defines three pieces of data, the object's type (typeName attribute), location (typeNamespace attribute which can alternatively define the assembly that the type resides in), and a list of constructors to create the object.

The constructors node defines a list of constructors. Each constructor has a key that can easily identify which constructor is being used. For a default constructor, you simply add a black constructor definition with a key. I suggest always naming your default constructors "Default" so that they are consistent, although the key can be any valid string. Each constructor may have a collection of constructorParameters. Each constructorParameter has a name, a type (typeName), a namespace (typeNamespace) (or assembly to find the type) and optionally a default value in case the consumer does not provide a value for the parameter. Parameters do not allow for further definitions of injectable objects, although that feature will be present in a future version of SimpleDI.

That's all we need to define an object and its constructors. The example above is fully functional.

The final piece of the puzzle is the re-usable code that is the implementation of IInjectable and IDefinitionLoader. I would suggest abstracting these objects to a separate assembly for re-use in other projects. If you download the source code of the SimpleDI.Configuration from GitHub, you will find a demo application that reads the sample configuration from above and implements a subclass of GPS.SimpleDI.Configuration.DefaultLoader.

In addition to GitHub, you can also find GPS.SimpleDI.Configuration in the Nuget gallery in Visual Studio.

History

  • Version 1.0.0.1: This is the version of SimpleDI.Configuration that is represented in this article. It includes the DefaultInjector and DefaultLoader base classes.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)