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.
In this blog post I won't cover why or where you should you use dependency containers. Instead we will take a look at the dependency injection pattern applied to the Remoting SDK (and Data Abstract) server applications (Spoiler: a new feature coming this November is introduced in this article).
The Task
Let's assume that we have 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 would we implement this 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: We need to 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 - we need to somehow pass a ConsoleLogger
or DebugLogger
instance to the FooService
instance. Note that FooService
is instantiated during client request processing, so we cannot just create an instance in the Main
method code. To add insult to injury, we also need to be able to use one of two possible ILogger
implementations (ConsoleLogger
or DebugLogger
). What are our options in this case?
We cannot 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.
To provide this value we can:
- Create a new
ILogger
instance in the service constructor each time it is called. Cons here are that change fromConsoleLogger
toDebugLogger
or back would require to change the service code. Also it is quite possible that the service needs to use a logger instance that is defined and configured somewhere else, so it just cannot instantiate it each time (f.e.ConnectionManager
in Data Abstract). - Define a static field and put an
ILogger
instance at application startup. Cons here are that dependency betweenFooService
andILogger
is not clear because it is not possible to find out these dependencies without reading the service source code. Also a single sharedILogger
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 the constructor.
Still the main magic happens in this line:
server.DependencyResolver.RegisterSingleton(typeof(ILogger), new DebugLogger());
Yes, Remoting SDK for .NET now provides built-in support for Dependency Injection containers! Remoting SDK for .NET provides a built-in Dependency Injection container as well as a simple way to integrate any 3rd-party DI container.
That said, the syntax used in the code line above should be used only in very simple cases. This is the recommended way of using the DI container:
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 theDebugLogger
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. You just need to provide a mock of the ILogger
service while constructing the FooService
instance. This will make the unit test of the Foo
method as self-contained as it should be.
Note: This syntax will be explained in more details in the second part of 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.