Simple container

    Yes, yes, you understood correctly, this article is about another bike - about my Dependency Injection (DI) container. Outside the window is already 2015, and there are a lot of different containers for every taste and color. Why might you need one more?

    Firstly, it can simply form by itself! We used this container for a long time in Elba , and some of the ideas described in the article (Factory Injection, Generics Inferring, Configurators) were initially implemented on top of it through a public API.

    Secondly, for a large project, a DI container is an essential part of the infrastructure, which largely determines the organization of the code. A simple, flexible and easily modifiable container often allows you to find an elegant solution to a specific problem, to avoid the coherence of individual components, verbose and boilerplate application code. When solving a specific problem, you can display a certain pattern, implement it at the container level, and then reuse it in other tasks.

    Thirdly, a DI container is a relatively simple thing. It lends itself very well to development in TDD mode , making it fun and pleasant to do.

    This article is not an introduction to DI. There are many other excellent publications on this subject, including on Habré. Rather, it contains a set of recipes for preparing DI so that the resulting dish is tasty, but not spicy. If you have a DI container in production or you wrote your own best container, then this is a great place for holivars about whose container is cooler!

    Motivation and Api


    The main message of the container is Convention Over Configuration . What is the point of tormenting the user, requiring him to explicitly indicate the correspondence of the implementation interface, if this interface has only one available implementation? Why not just substitute it, saving time to solve more important issues? As it turned out, a similar principle applies in many other situations. What, for example, could a container substitute into a constructor parameter of type IEnumerable or Func to be most useful? We will talk about this a bit later.

    The container code was written exclusively for specific practical tasks. This allowed us to concentrate on a small number of the most useful features and ignore all the others. So, for example, a container supports only one lifestyle - singletone. This means that instances of all classes are created on demand and stored in the container’s internal cache until it is destroyed. The container implements IDisposable by re-calling Dispose on the supporting objects from the cache. The order of calling Dispose on different services is determined by the dependencies between them: if service A depends on service B, then Dispose on A will be called earlier before Dispose on B. To create a service tree for a while and then destroy it, you can use the Clone method on the container. It returns a new container with the same configuration as the original one,

    The main container methods are Resolve and BuildUp. The first returns an instance by type using constructor injection, the second uses property injection to initialize an already created object. The BuildUp method only makes sense if the use of Resolve is difficult .

    Given that the container makes many decisions on its own, for debugging purposes, it supports the GetConstructionLog method. Using it, you can get a description of the creation process for any service at any time. This description is a tree whose leaves are either services that do not have constructor parameters, or specific primitive values ​​prompted to the container through the configuration API.

    Sequence injection


    This is the simplest and at the same time quite powerful trick: if the class in the constructor parameter accepts an array or IEnumerable, then the container will substitute all suitable implementations that it can find in this parameter. In its further work, the class can at any time select a specific implementation from the list and delegate part of its functions to it. Or, for example, notify all implementations about the occurrence of a certain event.

    Consider an example. Suppose we need to raise an http server that serves some fixed set of addresses. The processing of each address is responsible for a separate code block, which is convenient to present with this interface:

    public interface IHttpHandler
    {
    	string UrlPrefix { get; }
    	void Handle(HttpContext context);
    }
    

    Then the dispatch logic of the request-handler can be very simply expressed as follows:

    public class HttpDispatcher
    {
    	private IEnumerable handlers;
    	public HttpDispatcher(IEnumerable handlers)
    	{
    		this.handlers = handlers;
    	}
    	public void Dispatch(HttpContext context)
    	{
    		handlers.Single(h => context.Url.StartsWith(h.Prefix)).Handle(context);
    	}
    }
    

    The container finds all available implementations of IHttpHandler, creates one instance of each of them and substitutes the resulting list in the handlers parameter. Note that to add a new handler, simply create a new class that implements IHttpHandler - the container will find it and pass it to the HttpDispatcher constructor. This fairly easily enforces SRP and OCP .

    Another use case for Sequence Injection is event notification:

    public class UserService
    {
    	private readonly IDatabase database;
    	private readonly IEnumerable handlers;
    	public UserService(IDatabase database, IEnumerable handlers)
    	{
    		this.database = database;
    		this.handlers = handlers;
    	}
    	public void DeleteUser(Guid userId)
    	{
    		database.DeleteUser(userId);
    		foreach (var handler in handlers)
    			handler.OnUserDeleted(userId);
    	}
    }
    

    Removing a user can affect a number of system components. For example, some of them may have entities that refer to a remote user. To properly handle this situation, it is enough for such a component to simply implement the IUserDeletedHandler interface. At the same time, if a new such component or entity appears, there is no need to edit the UserService code - enough, in accordance with OCP , just add the IUserDeletedHandler handler.

    Factory injection


    Sometimes it may be necessary to create a new instance of a service. There may be various reasons for this. An obvious example is that a service in the constructor accepts a parameter whose value becomes known only at runtime. Or perhaps the service should be recreated for some architectural reasons. So, for example, the DataContext class from the standard Linq2Sql ORM is recommended to be recreated for each http request, because otherwise he begins to eat too much memory. In any case, you can act something like this:

    public class Calculator
    {
    	private readonly SomeService someService;
    	private readonly int factor;
    	public A(SomeService someService, int factor)
    	{
    		this.someService = someService;
    		this.factor = factor;
    	}
    	public int Calculate()
    	{
    		return someService.SomeComplexCalculation() * factor;
    	}
    }
    public class Client
    {
    	private readonly Func createCalculator;
    	public Client(Func createCalculator)
    	{
    		this.createCalculator = createCalculator;
    	}
    	public int Calculate(int value)
    	{
    		var instance = createCalculator(new { factor = value });
    		return instance.Calculate();
    	}
    }
    

    The mechanics of creation are implemented through the delegate accepted in the constructor. This delegate is generated by the container in such a way that when it is called, a new instance of Calculator will always be created. Through an object argument using an anonymous type, you can pass the parameters of the service being created. The correspondence of the parameters occurs by name - a member of the anonymous type factor falls into the factor parameter of the Calculator constructor. The constructor parameter someService does not specify a value in an anonymous type, so the container will be guided by the standard rules when it is received.

    The main disadvantage here is that checking the name / type of parameters is postponed from the compilation stage to the execution stage. Like the dynamic keyword, this requires special attention when adding / removing / renaming parameters and additional integration tests. However, in practice this does not lead to significant problems. Mainly due to the fact that using Factory Injection is not necessary very often. In our projects, there are only a few of the thousands of classes of situations in the entire code base. Secondly, even in these cases, errors with passing parameters are usually very simple and easy to detect - when a delegate is called, the container does a parameter check in the same way as the compiler does during compilation.

    Generics Inferring


    Quite often, the container itself can choose not only the implementation of the interface, but also generic arguments. For example, consider the simple message bus interface:

    public interface IBus
    {
    	void Publish(TMessage message);
    	void Subscribe(Action action);
    }
    

    Through IBus, you can publish messages and subscribe to their processing. The mechanics of message delivery are not important here, but usually this or that queue system (RabbitMQ, MSMQ, etc.). It is convenient to present a specific message handler with this interface:

    public interface IHandleMessage
    {
    	void Handle(TMessage message);
    }
    

    To process a new message type, it is enough to simply implement IHandleMessage with the corresponding generic argument:

    public class UserRegistered
    {
    }
    public class UserRegisteredHandler : IHandleMessage
    {
    	public void Handle(UserRegistered message)
    	{
    		//whatever
    	}
    }
    

    Now we need to call Subscribe for each implementation of IHandleMessage. Make it easy for a specific IHandleMessage:

    public static class MessageHandlerHelper
    {
    	public static void SubscribeHandler(IBus bus, IHandleMessage handler)
    	{
    		bus.Subscribe(handler.Handle);
    	}
    }
    

    But with what generic argument do we call the SubscribeHandler method? And where to get all such correct arguments and corresponding implementations of IHandleMessage? Ideally, I would like to reduce the situation to an example from Sequence Injection, just IEinumerable from something, thereby instructing the container to find all IHandleMessage implementations.

    To do this, we transfer the generic argument from the method level to the class level, and what happened is hidden behind a non-generic interface:

    public interface IMessageHandlerWrap
    {
    	void Subscribe();
    }
    public class MessageHandlerWrap : IMessageHandlerWrap
    {
    	private readonly IHandleMessage handler;
    	private readonly IBus bus;
    	public MessageHandlerWrap(IHandleMessage handler, IBus bus)
    	{
    		this.handler = handler;
    		this.bus = bus;
    	}
    	public void Subscribe()
    	{
    		bus.Subscribe(handler.Handle);
    	}
    }
    public class MessagingHost
    {
    	private readonly IEnumerable handlers;
    	public MessagingHost(IEnumerable handlers)
    	{
    		this.handlers = handlers;
    	}
    	public void Subscribe()
    	{
    		foreach (var handler in handlers)
    			handler.Subscribe();
    	}
    }
    

    How it works? To create a MessagingHost, the container needs to get all IMessageHandlerWrap implementations. There is only one class that implements this interface - MessageHandlerWrap, but to create it, you need to specify the specific value of the generic argument. To do this, the container considers the constructor parameter of type IHandleMessage - the existence of a suitable implementation of IHandleMessage is a prerequisite for creating MessageHandlerWrap. For IHandleMessagethere is an implementation - this is the UserRegisteredHandler class that closes IHandleMessage through UserRegistered. Thus, the container will substitute the MessageHandlerWrap instance in the handlers parameter of the MessagingHost.

    This option to close generics is based on dependency analysis. The above chain of reasoning easily extends to the case of an arbitrary number of generic arguments and arbitrary embedding of some generic services into others. The current container implementation handles these common cases correctly.

    Another option for closing generics is based on generic constraints. It can be useful in cases where the generic service does not have generic dependencies. Let the user-dependent entities implement the following interface in the example from Sequence Injection:

    public interface IUserEntity
    {
    	Guid UserId { get; }
    }
    

    Then, to remove all such entities, one generalized handler is enough:

    public class DeleteDependenciesWhenUserDeleted: IUserDeletedHandler
    	where TEntity : IUserEntity
    {
    	private readonly IDatabase database;
    	public DeleteDependenciesWhenUserDeleted(IDatabase database)
    	{
    		this.database = database;
    	}
    	public void OnDeleted(User entity)
    	{
    		foreach (var child in database.Select(x => x.UserId == entity.id))
    			database.Delete(child);
    	}
    }
    

    The container will create one instance of DeleteDependenciesWhenUserDeleted for each of the classes that implement IUserEntity.

    Configurators


    The container provides a configuration API through which you can tell him how to behave in a particular situation:

    public interface INumbersProvider
    {
    	IEnumerable ReadAll();
    }
    public class FileNumbersProvider : INumbersProvider
    {
    	private readonly string fileName;
    	public FileNumbersProvider(string fileName)
    	{
    		this.fileName = fileName;
    	}
    	public IEnumerable ReadAll()
    	{
    		return File.ReadAllLines(fileName).Select(int.Parse).ToArray();
    	}
    }
    public class FileNumbersProviderConfigurator : IServiceConfigurator
    {
    	public void Configure(ConfigurationContext context, ServiceConfigurationBuilder builder)
    	{
    		builder.Dependencies(new { fileName = "numbers.txt" });
    	}
    }
    

    Here, through the Dependencies method, we specify the specific value of the constructor parameter. As in Factory Injection, the binding takes place by the name of the parameter. When creating the container, it scans the assemblies passed to it and calls the Configure method on all found implementations of IServiceConfigurator. By convention, the configuration of class X should be in the XConfigurator class, located in the Configuration folder of the same assembly, although this is not necessary. In addition to the constructor parameters, using the ServiceConfigurationBuilder methods, you can select a specific interface implementation or, for example, specify the delegate that the container should use to create the class:

    public class LogConfigurator : IServiceConfigurator
    {
    	public void Configure(ConfigurationContext context, ServiceConfigurationBuilder builder)
    	{
    		builder.Bind(c => c.target == null ? LogManager.GetLogger("root") : LogManager.GetLogger(c.target));
    	}
    }
    

    The parameter of this delegate contains the target property - the type of the ILog client class to be created. This type will be null if there is no client, i.e. The Resolve () method on the container was called.

    Hardcode specific values ​​of constructor parameters in the code may seem a dubious decision to some. In practice, however, most of the settings (cache size, queue length, timeout values, tcp port numbers) change extremely rarely. They are tightly bound to the code that uses them. Changing them is a crucial step, requiring an understanding of the nuances of this code and therefore not much different from changing the code itself.

    Another atypical solution is to create a separate configurator class for each service. The main profit from this is a very simple structure of the configuration code. This greatly simplifies life. So, firstly, to understand exactly how the class X is created, it is enough to search for the class XConfigurator with a resolver - an action that takes seconds. Secondly, if you describe the configuration of different services in one class (modules in Ninject or Autofac, for example), then there is a high probability of a dump, because lines of code that configure different classes are often unrelated to each other. In a production project with tens of thousands of classes, hundreds of which need to be configured, such a module can become unreadable. Thirdly, the abstraction of the module itself is often not obvious - it may not always be easy to outline the framework where one module ends and another begins. Specially thinking about it just to organize the configuration code seems redundant.

    PrimaryAssembly


    Consider a fairly typical situation: FileNumbersProvider and its configurator from the example above lie in some common Class Library Lib.dll and are used in a large number of console applications. In each of them, FileNumbersProvider works with the “numbers.txt” file - and this is exactly what you need. But what if a new A.exe console appears in which the file name should be “a.txt”? You can, of course, remove FileNumbersProviderConfigurator from Lib.dll and unzip it in each of the consoles, indicating the correct file name value. Or, inside the general configurator, read the file name from another settings file (for this, the container provides the Settings method on ConfigurationContext). But you can do otherwise - just add the configurator for FileNumbersProvider with the correct file name to A.exe. This will work due to that the container will first launch the configurator from Lib.dll, and then the configurator from A.exe, and the latter will kill the first. This order of start is provided by a simple rule: all non-PrimaryAssembly configurators are launched before all of the PrimaryAssembly configurators. The specific assembly to be considered PrimaryAssembly is specified when the container is created.

    Profiles


    Quite often, the way to create a service depends on the environment. For example, in unit testing mode for INumbersProvider it is natural to use some inmemory implementation - InMemoryNumbersProvider, when running on battle servers - FileNumbersProvider with one file name value, and in manual testing mode with another. The solution to this problem is the concept of profiles. A profile is any class that implements the IProfile token exported by the container. The profile type can be passed when creating the container, and its current value will be available inside the configurator via ConfigurationContext. Typically, profiles are used like this:

    public class InMemoryProfile : IProfile
    {
    }
    public class IntegrationProfile : IProfile
    {
    }
    public class ProductionProfile : IProfile
    {
    }
    public class NumbersProviderConfigurator : IServiceConfigurator
    {
    	public void Configure(ConfigurationContext context, ServiceConfigurationBuilder builder)
    	{
    		if (context.ProfileIs())
    			builder.Bind();
    		else
    			builder.Bind();
    	}
    }
    public class FileNumbersProviderConfigurator : IServiceConfigurator
    {
    	public void Configure(ConfigurationContext context, ServiceConfigurationBuilder builder)
    	{
    		var fileName = context.ProfileIs() ? "productionNumbers.txt" : "integrationNumbers.txt";
    		builder.Dependencies(new { fileName });
    	}
    }
    

    Different applications can define their own sets of profiles, but usually these three are enough.

    Contracts


    It has already been mentioned that Factory Injection is rarely needed in practice. Most of the system can usually be described as a tree of services whose elements are cached at the container level. Such a “singleton” model is very convenient for its simplicity. To use some service, the client class simply needs to accept it in the constructor. He does not need to worry about how this service will be created and at what point it is destroyed - in all this he can rely on the container.

    However, quite often the service can conditionally be called a singleton “locally”, but not “globally”. A large service tree that implements complex business logic and has among its leaves some abstraction above the data source for it does not need to know that the container will create it in two copies, substituting one file for this source and one for the second other. Cause and effect are separated from each other by several levels of abstractions with which this tree operates. The reason is a specific constructor parameter in some service where, according to the application logic, you need to substitute a tree instance with a specific file name. The consequence is the use of this file name by the corresponding leaves of the tree.

    The described above is usually achieved either by dragging the parameter through the entire tree, or by creating factories that substitute this parameter in the correct elements of the tree. The container offers a more natural solution:

    public class StatCalculator
    {
    	private readonly FileNumbersProvider numbers;
    	public StatCalculator(FileNumbersProvider numbers)
    	{
    		this.numbers = numbers;
    	}
    	public double Average()
    	{
    		return numbers.ReadAll().Average();
    	}
    }
    public class StatController
    {
    	private readonly StatCalculator historyCalculator;
    	private readonly StatCalculator mainCalculator;
    	public StatController([HistoryNumbersContract] StatCalculator historyCalculator, [MainNumbersContract] StatCalculator mainCalculator)
    	{
    		this.historyCalculator = historyCalculator;
    		this.mainCalculator = mainCalculator;
    	}
    	public int HistoryAverage()
    	{
    		return historyCalculator.Average();
    	}
    	public int MainAverage()
    	{
    		return mainCalculator.Average();
    	}
    }
    public class FileNumbersProviderConfigurator : IServiceConfigurator
    {
    	public void Configure(ConfigurationContext context, ServiceConfigurationBuilder builder)
    	{
    		builder.Contract().Dependencies(new { fileName = “history” });
    		builder.Contract().Dependencies(new { fileName = “main” });
    	}
    }
    

    The attributes [InMemoryNumbersContract] and [FileNumbersContract] must be inherited from the container provided by [RequireContractAttribute]. Essentially, such an attribute is just a label with which you can declare some named context. This declaration can be made at once in several places of the tree, either at the level of the constructor parameter or at the class level. The definition of the contract by structure is no different from the usual configuration code - the Contract method on the builder returns a new builder, with which you can provide the contract with a certain meaning. The configuration set in this way acts on the parameter marked with the atrubut contract and on all the subtree below it. To do this, the container automatically creates a new instance of the service, if it substantially depends on the configuration of the current contract.

    A service tree may contain several contracts from root to leaves. In this case, if several of these contracts determine the configuration of the same service, then a simple stack rule applies - the configuration of the contract closest to the point of use of the service is used. If some service from the contracted subtree does not use the configuration of the contract, then it is guaranteed that the instance used for it will be exactly the same as if the contract label did not exist. In other words, if somewhere in another branch of the dependency tree this service occurs without a contract, then the same instance of the corresponding class will be used for it.

    The configuration can be hung on the sequence of contracts:

    public class FileNumbersProviderConfigurator : IServiceConfigurator
    {
    	public void Configure(ConfigurationContext context, ServiceConfigurationBuilder builder)
    	{
    		builder.Contract()
    			.Contract()
    			.Dependencies(new { "archiveHistoryNumbers.txt" });
    	}
    }
    

    In this case, “archiveHistoryNumbers.txt” will be used only if the sequence of contracts declared on the path from the root contains HistoryNumbersContractAttribute and ArchiveContractAttribute in the specified order.

    You can also define a contract as a combination of other contracts:

    public class AllNumbersConfigurator : IContainerConfigurator
    {
    	public void Configure(ConfigurationContext context, ContainerConfigurationBuilder builder)
    	{
    		builder.Contract()
    			.Union()
    			.Union()
    	}
    }
    

    The meaning of such a union is that sometimes it is necessary to process several contexts defined by contracts at once:

    public class StatController
    {
    	private readonly IEnumerable statCalculators;
    	public StatController([AllNumbersContract] IEnumerable statCalculators)
    	{
    		this.statCalculators = statCalculators;
    	}
    	public int Sum()
    	{
    		return statCalculators.Sum(c => c.Sum());
    	}
    }
    

    The container will sort through all the contracts from the union, and the resulting instance of StatCalculator will substitute a sequence of statCalculators for each of them.

    Contracts allow you to describe the state of a service only for a static, finite set of configurations, when all possible variants of the dependency tree are known at the configuration stage. If the file name for FileNumbersProvider is entered by the user, it will be much more natural to simply pass it by parameter through the chain StatController -> StatCalculator -> FileNumbersProvider.

    Optional injection


    Configurators allow you to prohibit the use of some implementation of an interface or a specific instance of a class:

    public class FileNumbersProviderConfigurator : IServiceConfigurator
    {
    	public void Configure(ConfigurationContext c, ServiceConfigurationBuilder b)
    	{
    		b.WithInstanceFilter(p => p.ReadAll().Any());
    	}
    }
    public class InMemoryNumbersProviderConfigurator : IServiceConfigurator
    {
    	public void Configure(ConfigurationContext c, ServiceConfigurationBuilder b)
    	{
    		b.DontUse();
    	}
    }
    

    The WithInstanceFilter method imposes a filter on all instances of FileNumbersProvider created by the container - clients will receive only those that can return at least one number. The DontUse method completely prohibits the use of InMemoryNumbersProvider. The class constructor can also decide that in a certain situation, the instance it creates should not be used by container clients. To report this to the container, the constructor must throw a special exception - ServiceCouldNotBeCreatedException. This will be equivalent to using the WithInstanceFilter method in the configurator.

    If creating a dependency of a certain service was forbidden by one of the described methods, then creating the service itself will also be considered forbidden. Such a process of sequential exclusion of services will rise recursively up the dependency tree until it reaches the Resolve call on the container. At this point, an exception will be generated that for this service failed to get any implementation. Another option for stopping this process is if on its way there is a constructor parameter that has a sequence type (Sequence Injection). In this case, this element of the sequence will simply be skipped. There is a third stop option - when the parameter is marked as optional:

    public class StatController
    {
    	private readonly StatCalculator statCalculator;
    	public StatController([Optional] StatCalculator statCalculator)
    	{
    		this.statCalculator = statCalculator;
    	}
    	public int InMemorySum()
    	{
    		return statCalculator == null ? 0 : statCalculator.Sum();
    	}
    }
    

    The [Optional] attribute provided by the container declares that if it is not possible to create the corresponding service, null must be passed to the parameter. You can achieve the same effect by using the default parameter value (= null) or by marking the parameter with the [CanBeNull] attribute from the JetBrains.Annotations library.

    Assume now that service A has two optional dependencies B and C. Assume also that the container successfully created B, but the creation of C was prohibited. Then the creation of A will also be prohibited and the instance of B will be unused. This is not a problem if creating B was a cheap operation, but if B requires complex initialization (going to the database, opening large files, initializing the cache), then before starting it, I would like to be sure that it is not useless. For this, the container provides the following interface:

    public interface IComponent
    {
    	void Run();
    }
    

    All the heavy logic of raising a service should be located in the implementation of the Run method of this interface. The trick here is that the container will call Run in a separate step, after it has completely created the entire dependency tree in the Resolve method. Knowing the composition of the tree, the container simply runs through it and sequentially calls Run in order from leaves to root. For each service, a call is made only once - upon first receipt. If the service is used in several subtrees, each of which was created by a separate call to Resolve, then Run on this service (if any) will also be called only for the first time.

    Total


    If you are interested in any of the above, the sources are available on github . Hands haven’t reached the documentation yet, so it’s most convenient to turn to tests for answers to API questions. If you feel that you are missing some feature or convention, then Fork and Pull Request are very welcome.

    Also popular now: