Ninject – Getting Started, Resources, Basic Binding

This post will cover the information needed to get started with Ninject. By the end of this post, you will be able to use Ninject in your application as a dependency injector. Here is a description from the Ninject web site.

Ninject is a lightning-fast, ultra-lightweight dependency injector for .NET applications. It helps you split your application into a collection of loosely-coupled highly-cohesive pieces, and then glue them back together in a flexible manner. By using Ninject to support your software’s architecture, your code will become easier to write, reuse, test, and modify.

You will often hear frameworks like Ninject called Inversion of Control (IOC) containers. IOC containers are responsible for the creation and life-cycle management of dependencies that the objects in your application require. If you want more information on Dependency Injection (DI) and IOC containers check out my previous post.

Getting Started With Ninject

To get started with Ninject, you will need to download the code. Here is a link to the Ninject download page:

Download

From this page select the platform that you are targeting. In our examples we will be targeting the .NET Framework 3.5.

image You will be prompted to save an archive of the Ninject DLLs you will need. Unzip the contents of this file into a folder that can be accessed later in your project. Here is a screenshot of the contents.

image

To use Ninject in your project, add a reference to the ‘Ninject.dll’ highlighted above. That is it, you are now ready to use Ninject in your project.

Description of Ninject Core Elements

Before using Ninject it is a good idea to understand fundamentally what the Ninject core elements are and the features they provide.

  • StandardKernel – This class is the container. Bindings are setup and passed into the constructor. An instance of the StandardKernel can be used to request concrete instances of our service types. The StandardKernel instance can resolve the requested type to a type configured in the bindings. It then provides an instance of the resolved type. The StandardKernel manages the life time of the resolved type instances.

You should be warned that passing the instance of StandardKernel around to request types is frowned upon and negates many of the benefits of the container.

  • NinjectModule – This is an abstract base class that allows bindings to be defined for the application. These bindings are essentially a look up table that match a requested type to a resolved type. Instances of classes that implement NinjectModule can then be passed into the StandardKernel constructor. Using multiple NinjectModule classes can help organize your bindings into logic units.

The process of introducing Ninject into an application generally has two steps:

  1. Create concrete implementations (at least one is required) of the NinjectModule abstract base classes.
  2. Instantiate a concrete instance of the StandardKernel. The StandardKernel constructor accepts NinjectModule instances. This sets up the Ninject container.

Application Example

In order to discuss Ninject an application example is necessary. The example used here has elements that may be found in common business applications. The initial requirements of the application are as follows:

  • The application must generate an audit log of all activities.
  • The application must use an already established data source of transactions. A transaction consists of a timestamp / amount pair.
  • The application must generate a summary report for all transactions within the last 6 months. The report will be a text file consisting of the following:
    • Report generation date.
    • Total of transactions
    • A list of transactions for the interval.

This fictional application may not be of sufficient complexity that dependency injection becomes too difficult to do by hand. It does have a number of realistic elements. Let’s write some code.

The logger and reporting requirements will necessitate that we serialize data to the file system. The .NET Framework’s ‘System.IO.File’, ‘System.IO.Directory’ and ‘System.IO.Path’ classes are static. In order to have automated test code that does not write to the actual file system we introduce  the following interface:

public interface IFileSystem
{
    // System.IO.Path
    string GetDirectoryName(string path);
    string PathCombine(string path1, string path2);

    // System.IO.Directory
    bool DirectoryExists(string path);
    void CreateDirectory(string path);

    // System.IO.File
    void AppendAllText(string path, string contents);
    bool FileExists(string path);
    string[] ReadAllLines(string path);
    void WriteAllLines(string path, string[] lines);
    string ReadAllText(string path);
    void WriteAllText(string path, string contents);
}

This interface is not a complete wrapper for the ‘System.IO’ classes but contains just the features needed in our application. Two concrete implementations of this interface will be developed: ‘FileSystem’ and ‘FakeFileSystem’. ‘FileSystem’ simply calls the ‘System.IO’ counterpart while the ‘FakeFileSystem’ implementation is an in-memory file system that can be used by the test bed. The concrete implementations and the ‘IFileSystem’ interface all cry out REUSE ME. These should exist in their own namespace and assembly.

The application also needs a logger. There are some really nice logging libraries that already exist. I do not recommend reinventing the wheel. However, to keep our example small and reduce complexity we will implement our own. The following is a really simple logger interface.

public interface ILogger
{
    void Error(string info);
    void Warn(string info);
    void Info(string info);
}

This interface allows three logging levels that take simple strings as arguments. Our application will implement the following concrete versions: ‘FileLogger’ and ‘MemoryLogger’. The ‘FileLogger’ will be used by the deployed application. Once again we have an in-memory counter part for testing.

Data is being stored in an already existing data source. The data source complexity is encapsulated with the following class and interface:

public class Transaction
{
    public DateTime Date { get; set; }
    public double Amount { get; set; }
}
public interface IDataRepository
{
    IQueryable<Transaction> FetchTransactions(DateTime begin, DateTime end);
}

The ‘Transaction’ class provides a container for data. Implementations of the ‘IDataRepository’ interface hide the complexity of the data source details. The example application will implement the following concrete instances: ‘DataRepository’ and ‘FakeDataRepository’. Again we have a real implementation that queries the data source (database, xml, csv) and a fake implementation that generates in-memory collections for our test bed.

To summarize we have the following objects in the application.

 

IFileSystem

ILogger

IDataRepository

Application

FileSystem

FileLogger

DataRepository

Test Bed

FakeFileSystem

MemoryLogger

FakeDataRepository

It is pretty clear that we will want to configure the test bed and application to use their respective implementations of the interfaces. There may also be additional levels in the dependency graph. For instance the implementations of the ‘IDataRepository’ interface will require an instance of ‘ILogger’ to meet our audit logging requirement.

public class DataRepository : IDataRepository
{
    private readonly ILogger _logger;

    public DataRepository(ILogger logger)
    {
        _logger = logger;
    }

    public IQueryable<Transaction> FetchTransactions(DateTime begin, DateTime end)
    {
        // Fetch data, log and return.
        //
        throw new NotImplementedException();
    }
}

Managing these dependencies by hand may be effective for small projects, but as the application grows in size and complexity, it becomes more and more cumbersome to wire up all of objects. Another option is to use an IOC container, such as Ninject, to manage the dependencies of the application and test bed.

So far the application has a couple of highly decoupled and cohesive core infrastructure objects (file system, logging) that can be re-used in other applications. The application also has an application specific data repository object. The following objects add the remaining business intelligence to complete our application requirements.

public class Calculator
{
    private readonly ILogger _logger;

    public Calculator(ILogger logger)
    {
        _logger = logger;
    }

    public double CalculateTotalTransactions(IQueryable<Transaction> data)
    {
        double totalTransactions = 0;
        foreach (Transaction transaction in data)
        {
            totalTransactions += transaction.Amount;
        }

        _logger.Info("Calculation: num entries = " + data.Count() + ", total = " + totalTransactions);
        return totalTransactions;
    }
}

public class Report
{
    private readonly IFileSystem _fileSystem;
    private readonly ILogger _logger;
    private const string OutputFolder = @"c:tempdelete_me";
    public Report(IFileSystem fileSystem, ILogger logger)
    {
        _fileSystem = fileSystem;
        _logger = logger;
    }

    public void Save(IQueryable<Transaction> data, double total)
    {
        DateTime now = DateTime.Now;
        StringBuilder content = new StringBuilder();
        content.AppendLine("Date: " + now);
        content.AppendLine("Total: " + total);
        foreach (var datum in data)
        {
            content.AppendLine(datum.Date + ", " + datum.Amount);
        }

        string outputPath = _fileSystem.PathCombine(OutputFolder, "rpt" + now.ToString("yyyy_MM_dd_hh_mm_ss") + ".txt");

        _fileSystem.WriteAllText(outputPath, content.ToString());

        _logger.Info("Saved report. filename='" + outputPath + "'");
    }
}

The implementation of these last two class was quick-n-dirty. Not too much thought was put into optimizing the code. Instances of the ‘Calculator’ class perform the business logic of summing all the supplied transactions. The ‘Report’ object generates a report.

Notice that both the ‘Calculator’ and ‘Report’ classes have dependencies on the previous objects. Specifically they both use an instance of ‘ILogger’ and the ‘Report’ object uses an instance of ‘IFileSystem’. You can easily imagine how we could automatically test the ‘Calculator’ and ‘Report’ objects by injecting in the in-memory versions.

Wire Up The Application By Hand

Managing the dependencies and wiring up our application is not too tough in this case. The following code shows what needs to be done:

// Get config data (probably from a config file)
//
const string logFile = @"c:tempdelete_melogfile.txt";
const int numberOfMonths = 6;

// Create the objects.
//
IFileSystem fileSystem = new FileSystem.FileSystem();
ILogger logger = new FileLogger(fileSystem, logFile);
IDataRepository repo = new FakeDataService(logger);
Calculator calculator = new Calculator(logger);
Report report = new Report(fileSystem, logger);

// Process
//
var data = repo.FetchTransactions(DateTime.Now.AddMonths(-numberOfMonths), DateTime.Now);
double total = calculator.CalculateTotalTransactions(data);
report.Save(data, total);

This is really not too bad. There would be an equivalent amount of object creation in our test bed. Because this application is small, the wiring up code is probably manageable by hand.

Use Ninject To Supply Our Dependencies

As discussed earlier, the first step to use Ninject is to create a concrete implementation of the ‘NinjectModule’ abstract class. The following implementation provides the bindings necessary for this application:

public class SimpleModule : NinjectModule
{
    public override void Load()
    {
        // Get this from an external file.
        //
        const string logFile = @"c:tempdelete_melogfile.txt";

        Bind<IFileSystem>().To<FileSystem.FileSystem>();
        Bind<IDataRepository>().To<FakeDataService>();
        Bind<ILogger>().To<FileLogger>().WithConstructorArgument("fileName", logFile);
    }
}

The generic Ninject ‘Bind’ method takes the type of the service (‘IFileSystem’ for instance) to bind. The Ninject ‘To’ method takes the implementation type (‘FileSystem.FileSystem’). This wires up the Ninject container to provide an instance of ‘FileSystem.FileSystem’ for every ‘IFileSystem’ request. Ninject also provides the ‘WithConstructorArgument’ method that allows the ‘fileName’ parameter to be supplied to the ‘FileLogger’ constructor.

The Ninject binding code is very fluent and offers many features. In later posts we will look at more advanced binding scenarios.

Next we need to wire up the Ninject container. The following code initializes the container, creates the objects and performs the processing.

// Get config data (probably from a config file)
//
const int numberOfMonths = 6;

// Initialize the DI container.
//
NinjectModule module = new SimpleModule();
IKernel kernel = new StandardKernel(module);

// Get object from DI container.
//
var repo = kernel.Get<IDataRepository>();
var calculator = kernel.Get<Calculator>();
var report = kernel.Get<Report>();

// Process
//
var data = repo.FetchTransactions(DateTime.Now.AddMonths(-numberOfMonths), DateTime.Now);
double total = calculator.CalculateTotalTransactions(data);
report.Save(data, total);

This code first instantiates an instance of the ‘SimpleModule’ class that contains the binding information for the application. Next an instance of the ‘StandardKernel’ is instantiated. The objects needed by the application are requested from the Ninject container using the generic ‘Get’ method. Finally, the processing is done.

The code to wire up the Ninject container consists of 15 lines of code versus 10 lines in the hand-wired case (ignoring comments and lines that only contain curly brackets).

Testing Using Ninject

How do we wire up the in-memory fakes that we created earlier for testing? It is as simple as creating another module.

public class SimpleFakeModule : NinjectModule
{
    public override void Load()
    {
        Bind<IFileSystem>().To<FakeFileSystem>();
        Bind<IDataRepository>().To<FakeDataService>();
        Bind<ILogger>().To<MemoryLogger>();
    }
}

This module wires up the in-memory dependencies. In our test bed, we can then create a ‘StandardKernel’ using this module. This ability to reconfigure an application on the fly is a very powerful feature of using Ninject.

Summary

As we have seen introducing Ninject into our application to provide basic dependency injection is simple. An IOC container does add complexity. In our case we added 5 lines and an addition of a library. The cost of this added complexity must be weighed against the benefits. Don’t make the decision just yet. There is much more that Ninject has to offer (advanced binding and life-cycle management for instance). This post sets the story for a couple of future posts on Ninject.

Comments
  1. Caine
  2. Bob Cravens
  3. Zahid
  4. Huy
    • rcravens
  5. Joe
  6. Joe

Leave a Reply

Your email address will not be published.

*