Dependency Injection in Remoting SDK for .NET (Part II)

Dependency Injection in Remoting SDK for .NET (Part II)

This second part of our series on DI will be a bit more technical. It will discuss new APIs provided by Remoting SDK, their performance and other technical details.

The first question that might arise is "What if I do not need all this stuff? My application is very simple so I just want to use the approaches I am used to."

Well, the answer is simple - just do not do anything. We have designed DI support to be as unobtrusive and transparent as possible so all existing code will work without any changes. Nothing will break. Also there are no performance hits or broken code in the case you won't touch DI container in your code.

Under the hood, Remoting SDK will do all the work for you. In fact, all these years SDK already did use a code that did act like a DI container, just a very simple one.

So, to begin with, let's take a closer look at the initialization code from the end of the part I:

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

Here, an instance of Remoting SDK built-in container is created.

Then a singleton registration call is performed. The container will register an instance of the DebugLogger class as implementing the ILogger interface. After this registration, that instance will be used as a parameter value while instantiating any types registered in the DI container.

The RegisterSDK method is an extension method that registers SessionManager and EventSinkManager instances in the container. Note that passing a INetworkServer instance as a parameter of this method ensures that Application Server initializes its NetworkServer instance and its SessionManager and EventSinkManager instances before they are registered in the container.

Then a RegisterServices call is issued. This extension method finds all non-abstract SDK services in the server application and registers them in the container.

The SimpleContainer can register SDK services on the fly, so this call can be omitted. Still it is highly recommended to call the RegisterServices method anyway. In this case SimpleContainer will validate all services registrations, so any container misconfiguration will be detected immediately at server application startup.

For example let's take a look at the code snippet above. Commenting out both .RegisterSingleton and .RegisterServices will allow the server application to start. Unfortunately any FooService call will fail because its constructor requires an ILogger instance while there is none provided. The main issue here is that an exception will be raised only when the service is called.

However, uncommenting the .RegisterServices call will result in the server application failing immediately at startup, allowing you to easily detect and fix any misconfiguration.

Containers Without ApplicationServer

Older server applications (ones that do not use the Application Server boilerplate infrastructure) can also benefit from container support.

The server.DependencyResolver property used in the code earlier is just an alias for the RemObjects.SDK.Server.Engine.ObjectActivator static property. So this code will allow to configure a DI container as well:

var container = new SimpleContainer();
container.RegisterSingleton(typeof(ILogger), new DebugLogger());
container.RegisterSDK(server.NetworkServer);
container.RegisterServices();
RemObjects.SDK.Server.Engine.ObjectActivator = container;

A reminder: There is no need to add any code to your existing apps. All existing apps will continue to work without any code changes required.

Registration API

Remoting SDK and Data Abstract provide a number of methods that simplify the task of registering SDK services and infrastructure in the DI container. These methods are declared as extension methods, so it is possible to easily switch from the Remoting SDK container to any 3rd party DI container like Unity with as less code changes as possible.

Methods:

  • RemObjects.SDK.Server namespace
    • RegisterSDK(INetworkServer) - registers SessionManager and EventSinkManager instances used by the provided INetworkServer instance
    • RegisterServices - register all non-abstract SDK services defined in the server application
  • RemObjects.DataAbstract.Server namespace
    • RegisterDataAbstract - registers ConnectionManager instance. Also registers an ILocalServiceActivator instance (see below).

The ILocalServiceActivator interface provides a set of helper methods that should simplify acquiring local Data Services. At this moment the default implementation of this interface provides 2 methods that are aliases of the corresponding RemObjects.DataAbstract.Server.Engine methods: AcquireService and ReleaseService

Simple Container, Described

The built-in DI container exposes the following methods:

void Register(Type serviceType);
void Register(Type serviceType, Type implementationType);
void Register(Type serviceType, Func<Object> factory);

void RegisterSingleton(Type serviceType);
void RegisterSingleton(Type serviceType, Type implementationType);
void RegisterSingleton(Type serviceType, Object instance);

void Register<TService>();
void Register<TService, TImplementation>();
void Register<TService>(Func<Object> factory);

void RegisterSingleton<TService>();
void RegisterSingleton<TService, TImplementation>();
void RegisterSingleton<TService>(TService instance);

The Register methods allow you to register types that should be instantiated anew on every request (like SDK services):

  • Register(Type serviceType) - registers serviceType in the container
  • Register(Type serviceType, Type implementationType) - registers implementationType in the container as a type implementing the implementationType, Func factory) - registers theserviceType` in the container and sets a custom method that should be used to instantiate it. Can be used if the class being registered requires special actions to be instantiated (f.e. a type should be instantiated via a factory method call)

The RegisterSingleton methods allow you to register types that should be instantiated only once:

  • RegisterSingleton(Type serviceType) - registers singleton of type serviceType
  • RegisterSingleton(Type serviceType, Type implementationType) - registers singleton of serviceType as the type implementing the implementationType. Can be used to register the concrete implementation of abstract base class.
  • RegisterSingleton(Type serviceType, Object instance) - registers the concrete instance as the serviceType implementation.

Generic methods are used as aliases for their non-generic versions.

Integration of 3rd-party Containers

It is possible to use almost any 3rd part Dependency Injection container instead of the built-in Remoting SDK container.

The recommended approach is to create a wrapper class around the container implementing one of the IDependencyContainer or IObjectActivator interfaces. Then this wrapper instance should be assigned to either the server.DependencyResolver property (for IDependencyContainer implementation) or directly to the RemObjects.SDK.Server.Engine.ObjectActivator property.

The IObjectActivator exposes a single GetInstance method. This method accepts a service type and returns an instance of this type.

The IDependencyContainer interface provides 2 additional methods:

  • Register(Type serviceType) - registers the serviceType in the container.
  • RegisterSingleton(Type serviceType, Object instance) - registers the concrete instance as the serviceType implementation.

In most cases it is recommended to implement the IDependencyContainer interface as it will allow you to use the Registration API methods, saving you from manually writing the code required to register SDK services and SDK/DA infrastructure.

Sample Wrappers: LightInject

This is a sample wrapper for the LightInject (https://www.lightinject.net/) DI container:

public sealed class LightInjectContainer : IDependencyContainer
{
	private readonly ServiceContainer _container;

	public LightInjectContainer()
	{
		this._container = new ServiceContainer(ContainerOptions.Default);
	}

	public object GetInstance(Type objectType)
	{
		return this._container.GetInstance(objectType);
	}

	public void Register(Type serviceType)
	{
		this._container.Register(serviceType);
	}

	public void RegisterSingleton(Type serviceType, object instance)
	{
		this._container.RegisterInstance(serviceType, instance);
	}
}

Once defined this container wrapper can be used as:

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

Note how easy it to replace the DI container used.

Sample Wrappers: Autofac

As another example let's look at Autofac (https://autofac.org/):

A wrapper class for this DI container would look like this:

public sealed class AutofacContainer : IDependencyContainer
{
	private IContainer _container;
	private ContainerBuilder _builder;

	public AutofacContainer()
	{
		this._builder = new ContainerBuilder();
	}

	public void Build()
	{
		this._container = this._builder.Build();
		this._builder = null;
	}

	public object GetInstance(Type objectType)
	{
		return this._container.Resolve(objectType);
	}

	public void Register(Type serviceType)
	{
		this._builder.RegisterType(serviceType);
	}

	public void RegisterSingleton(Type serviceType, object instance)
	{
		this._builder.Register(_ => instance).As(serviceType).SingleInstance();
	}
}

This wrapper requires an additional container-specific step to be used:

var container = new AutofacContainer();
container.RegisterSingleton(typeof(ILogger), new DebugLogger());
container.RegisterSDK(server.NetworkServer);
container.RegisterServices();
container.Build(); // This call performs actual Autofac container initialization
server.DependencyResolver = container;

Still most of the code is the same for any DI container used.

Conclusion

We hope that you will find your use for this feature and that Dependency Injection containers support will help you to write even more reliable and clean code.

Any feedback will be highly appreciated.