Dependency Injection support (.NET)

Introduction

One of the pillars of the modern software development industry are the SOLID principles. These code design principles provide a great (and solid) solution to the major software development challenge: how to cope with increasing code complexity without being buried under tons of technical debt?

Dependency Injection and Inversion of Control are a very important part of SOLID. Dependency injection containers do all the work of resolving dependencies while you concentrate on the code itself. Properly used, they improve code readability and code testability as well.

This article covers the dependency injection pattern applied to the Remoting SDK (and Data Abstract) server applications.

The Task

Let's assume the following task:

Create a simple server application with a single-method service. This service method should be able to log its calls into either a debugger output or console.

How could this be implemented in the traditional way?

A simple Code-First implementation based on the Application Server boilerplate code would look like this:

using RemObjects.SDK.Server;

namespace SampleServer
{
    static class Program
    {
        public static int Main(string[] args)
        {
            ApplicationServer server = new ApplicationServer("SampleServer");
            server.Run(args);
            return 0;
        }
    }

    // Sample Service
    [Service]
    public class FooService : Service
    {
        public FooService()
        {
        }

        [ServiceMethod]
        public string Foo(string value)
        {
            // TODO: Somehow call logger here
            return value.ToUpper();
        }
    }

    public interface ILogger
    {
        void Log(string message);
    }

    class ConsoleLogger : ILogger
    {
        public void Log(string message)
        {
            System.Console.WriteLine(message);
        }
    }

    class DebugLogger : ILogger
    {
        public void Log(string message)
        {
            System.Diagnostics.Debug.WriteLine(message);
        }
    }
}

And here comes the main issue - ConsoleLogger or DebugLogger instance should be somehow passed to the FooService instance. Note that FooService is instantiated during the client request processing, so it is not possible to just create an instance in the Main method code. Also proper logge ILogger implementations (ConsoleLogger or DebugLogger) should be chosen somehow.

What can be done in this case?

It is not possible to just define the service method code as:

        [ServiceMethod]
        public string Foo(string value)
        {
            this._logger.Log("Foo: " + value);
            return value.ToUpper();
        }

The logger instance has to be somehow provided first to the Foo method code.

There are several ways to provide the ILogger instance:

  • Create a new ILogger instance in the service constructor each time it is called. Cons here are that change from ConsoleLogger to DebugLogger or back would require to change the service code. Also it is quite possible that the service needs to use an object that is defined and configured somewhere else, so it just cannot be instantiated each time (f.e. ConnectionManager in Data Abstract).

  • Define a static field in the service class and put there an ILogger instance at application startup. Cons here are that dependency between FooService and ILogger is not clear because it is not possible to find out these dependencies without reading the service source code. Also a single shared ILogger instance might result in significant difficulties in unit testing (f.e. one test might break the internal state of the shared instance thus resulting in a failure in another completely unrelated test).

  • Use a Service Locator pattern. Still in this case there will be the same cons as for the static field approach.

All of these difficulties might seem not that important. Yet they can and will complicate development and testing for more complex server applications. This might eventually result in unclean and messy code, unreliable application behavior and hard-to-catch bugs.

The Solution

Luckily there is a solution for the development challenges mentioned above. Dependency Injection containers (called DI containers below) can help to cope with code complexity and maintaining clean and testable code.

Take a look at this code sample:

using RemObjects.SDK.Server;

namespace SampleServer
{
    static class Program
    {
        public static int Main(string[] args)
        {
            ApplicationServer server = new ApplicationServer("SampleServer");
            
            server.DependencyResolver.RegisterSingleton(typeof(ILogger), new DebugLogger());

            server.Run(args);
            return 0;
        }
    }

    // Sample Service
    [Service]
    public class FooService : Service
    {
        private readonly ILogger _logger;

        public FooService(ILogger logger)
        {
            this._logger = logger;
        }

        [ServiceMethod]
        public string Foo(string value)
        {
            this._logger.Log("Foo: " + value);
            return value.ToUpper();
        }
    }

    public interface ILogger
    {
        void Log(string message);
    }

    class ConsoleLogger : ILogger
    {
        public void Log(string message)
        {
            System.Console.WriteLine(message);
        }
    }

    class DebugLogger : ILogger
    {
        public void Log(string message)
        {
            System.Diagnostics.Debug.WriteLine(message);
        }
    }
}

Note how an ILogger instance is passed to the service via its constructor.

The main magic happens in this line:

server.DependencyResolver.RegisterSingleton(typeof(ILogger), new DebugLogger());

Note: Remoting SDK for .NET provides built-in support for Dependency Injection containers as a simple way to integrate any 3rd-party DI container.

That said, the syntax used in the code sample above should be used only in simple cases.

This is the recommended way of using the DI container in Remoting SDK:

var container = new SimpleContainer();
container.RegisterSingleton(typeof(ILogger), new DebugLogger());
container.RegisterSDK(server.NetworkServer);
container.RegisterServices();
server.DependencyResolver = container;

The code above does the following:

  • A new DI container is created
  • A singleton of type ILogger implemented by the DebugLogger instance is registered in this container
  • SDK-specific entities like Session Manager and Event Sink Manager are registered in the container
  • Remoting SDK services defined in the application are registered in the container
  • The default Remoting SDK DI container is replaced with the one just configured

After applying these settings, Remoting SDK will be able to instantiate the service class using a parametrized constructor.

An additional bonus is that it is now possible to properly test the service method. A mock implementation of the ILogger interface can be provided while constructing the FooService instance. This will make the unit test of the Foo method as self-contained as it should be.

Note: Detailed description of the Dependencty Injection container API is described in this article.

Conclusion

Remoting SDK for .NET now provides built-in support for Dependency Injection containers.

Remoting SDK for .NET also provides a built-in simple Dependency Injection container, as well as a simple way to integrate any 3rd-party container like Unity, Autofac etc.