When I (Mark) was a kid, my mother and I would occasionally make ice cream. This didn’t happen too often because it required work, and it was hard to get right. Real ice cream is based on a crème anglaise, which is a light custard made from sugar, egg yolks, and milk or cream. If heated too much, this mixture curdles. Even if you manage to avoid this, the next phase presents more problems. Left alone in the freezer, the cream mixture crystallizes, so you have to stir it at regular intervals until it becomes so stiff that this is no longer possible. Only then will you have a good, homemade ice cream. Although this is a slow and labor-intensive process, if you want to — and you have the necessary ingredients and equipment — you can use this technique to make ice cream.
Today, some 35 years later, my mother-in-law makes ice cream with a frequency unmatched by myself and my mother at much younger ages — not because she loves making ice cream, but because she uses technology to help her. The technique is still the same, but instead of regularly taking out the ice cream from the freezer and stirring it, she uses an electric ice cream maker to do the work for her (see figure 12.1).
DI is first and foremost a technique, but you can use technology to make things easier. In part 3, we described DI as a technique. Here, in part 4, we take a look at the technology that can be used to support the DI technique. We call this technology DI Containers.
In this chapter, we’ll look at DI Containers as a concept — how they fit into the overall topic of DI — as well as some patterns and practices concerning their usage. We’ll also look at some examples along the way.
This chapter begins with a general introduction to DI Containers, including a description of a concept called Auto-Wiring, followed by a section on various configuration options. You can read about each of these configuration options in isolation, but we think it’d be beneficial to at least read about Configuration as Code before you read about Auto-Registration.
The last section is different. It focuses on the advantages and disadvantages of using DI Containers and helps you decide whether the use of a DI Container is beneficial to you and your applications. We think this an important part that everyone should read, regardless of their experience with DI and DI Containers. This section can be read in isolation, although it would be beneficial to read the sections on Configuration as Code and Auto-Registration first.
The purpose of this chapter is to give you a good understanding of what a DI Container is and how it fits in with the rest of the patterns and principles in this book. In a sense, you can view this chapter as an introduction to part 4 of the book. Here, we’ll talk about DI Containers in general, whereas in the following chapters, we’ll talk about specific containers and their APIs.
A DI Container is a software library that can automate many of the tasks involved in Object Composition, Lifetime Management, and Interception. Although it’s possible to write all the required infrastructure code with Pure DI, it doesn’t add much value to an application. On the other hand, the task of composing objects is of a general nature and can be resolved once and for all; this is what’s known as a Generic Subdomain.1 Given this, using a general-purpose library can make sense. It’s not much different than implementing logging or data access; logging application data is the kind of problem that can be addressed by a general-purpose logging library. The same is true for composing object graphs.
In this section, we’ll discuss how DI Containers compose object graphs. We’ll also show you some examples to give you a general sense of what using a container and an implementation might look like.
A DI Container is a software library like any other software library. It exposes an API that you can use to compose objects, and composing an object graph is a single method call. DI Containers also require you to configure them prior to composing objects. We’ll revisit that in section 12.2.
Here, we’ll show you some examples of how DI Containers can resolve object graphs. As examples in this section, we’ll use both Autofac and Simple Injector applied to an ASP.NET Core MVC application. Refer to section 7.3 for more detailed information about how to compose ASP.NET Core MVC applications.
You can use a DI Container to resolve controller instances. This functionality can be implemented with all three DI Containers covered in the following chapters, but we’ll show only a couple of examples here.
Autofac is a DI Container with a fairly pattern-conforming API. Assuming you already have an Autofac container instance, you can resolve a controller by supplying the requested type:
var controller = (HomeController)container.Resolve(typeof(HomeController));
You’ll pass typeof(HomeController)
to the Resolve
method and get back an instance of the requested type, fully populated with all the appropriate Dependencies. The Resolve
method is weakly typed and returns an instance of System.Object
; this means you’ll need to cast it to something more specific, as the example shows.
Many of the DI Containers have APIs that are similar to Autofac’s. The corresponding code for Simple Injector looks nearly identical to Autofac’s, even though instances are resolved using the SimpleInjector.Container
class. With Simple Injector, the previous code would look like this:
controller = (HomeController)container.GetInstance(typeof(HomeController));
The only real difference is that the Resolve
method is called GetInstance
. You can extract a general shape of a DI Container from these examples.
A DI Container is an engine that resolves and manages object graphs. Although there’s more to a DI Container than resolving objects, this is a central part of any container’s API. The previous examples show that containers have a weakly typed method for that purpose. With variations in names and signatures, that method looks like this:
object Resolve(Type serviceType);
As the previous examples demonstrate, because the returned instance is typed as System.Object
, you often need to cast the return value to the expected type before using it. Many DI Containers also offer a generic version for those cases where you know which type to request at compile time. They often look like this:
T Resolve<T>();
Instead of supplying a Type
method argument, such an overload takes a type parameter (T
) that indicates the requested type. The method returns an instance of T
. Most containers throw an exception if they can’t resolve the requested type.
If we view the Resolve
method in isolation, it almost looks like magic. From the compiler’s perspective, it’s possible to ask it to resolve instances of arbitrary types. How does the container know how to compose the requested type, including all Dependencies? It doesn’t; you’ll have to tell it first. You do so using a configuration that maps Abstractions to concrete types. We’ll return to this topic in section 12.2.
If a container has insufficient configuration to fully compose a requested type, it’ll normally throw a descriptive exception. As an example, consider the following HomeController
we first discussed in listing 3.4. As you might remember, it contains a Dependency of type IProductService
:
public class HomeController : Controller
{
private readonly IProductService productService;
public HomeController(IProductService productService)
{
this.productService = productService;
}
...
}
With an incomplete configuration, Simple Injector has exemplary exception messages like this one:
The constructor of type HomeController contains the parameter with name 'productService' and type IProductService, which isn’t registered. Please ensure IProductService is registered or change the constructor of HomeController.
In the previous example, you can see that Simple Injector can’t resolve HomeController
, because it contains a constructor argument of type IProductService
, but Simple Injector wasn’t told which implementation to return when IProductService
was requested. If the container is correctly configured, it can resolve even complex object graphs from the requested type. If something is missing from the configuration, the container can provide detailed information about what’s missing. In the next section, we’ll take a closer look at how this is done.
DI Containers thrive on the static information compiled into all classes. Using reflection, they can analyze the requested class and figure out which Dependencies are needed.
As explained in section 4.2, Constructor Injection is the preferred way of applying DI and, because of this, all DI Containers inherently understand Constructor Injection. Specifically, they compose object graphs by combining their own configuration with the information extracted from the classes’ type information. This is called Auto-Wiring.
Most DI Containers also understand Property Injection, although some require you to explicitly enable it. Considering the downsides of Property Injection (as explained in section 4.4), this is a good thing. Figure 12.2 describes the general algorithm most DI Containers follow to Auto-Wire an object graph.
As shown, a DI Container finds the concrete type for a requested Abstraction. If the constructor of the concrete type requires arguments, a recursive process starts where the DI Container repeats the process for each argument type until all constructor arguments are satisfied. When this is complete, the container constructs the concrete type while injecting the recursively resolved Dependencies.
In section 12.2, we’ll take a closer look at how containers can be configured. For now, the most important thing to understand is that at the core of the configuration is a list of mappings between Abstractions and their represented concrete classes. That sounds a bit theoretical, so we think an example will be helpful.
To demonstrate how Auto-Wiring works, and to show that there’s nothing magical about DI Containers, let’s look at a simplistic DI Container implementation that’s able to build complex object graphs using Auto-Wiring.
Listing 12.1 shows this simplistic DI Container implementation. It doesn’t support Lifetime Management, Interception, or many other important features. The only supported feature is Auto-Wiring.
Listing 12.1 A simplistic DI Container that supports Auto-Wiring
public class AutoWireContainer
{
Dictionary<Type, Func<object>> registrations = ①
new Dictionary<Type, Func<object>>();
public void Register(
Type serviceType, Type componentType) ②
{ ②
this.registrations[serviceType] = ②
() => this.CreateNew(componentType); ②
} ②
public void Register( ③
Type serviceType, Func<object> factory) ③
{ ③
this.registrations[serviceType] = factory; ③
} ③
public object Resolve(Type type) ④
{ ④
if(this.registrations.ContainsKey(type)) ④
{ ④
return this.registrations[type](); ④
} ④
④
throw new InvalidOperationException( ④
"No registration for " + type); ④
}
private object CreateNew(Type componentType) ⑤
{ ⑤
var ctor = ⑤
componentType.GetConstructors()[0]; ⑤
⑤
var dependencies = ⑤
from p in ctor.GetParameters() ⑤
select this.Resolve(p.ParameterType); ⑤
⑤
return Activator.CreateInstance( ⑤
componentType, dependencies.ToArray()); ⑤
}
}
The AutoWireContainer
contains a set of registrations. A registration is a mapping between an Abstraction (the service type) and a component type. The Abstraction is presented as the dictionary’s key, whereas its value is a Func<object>
delegate that allows constructing a new instance of a component that implements the Abstraction. The Register
method registers a new registration by telling the container which component should be created for a given service type. You only specify which component to create, not how.
The Register
method adds the mapping for the service type to the registrations
dictionary. Optionally, the Register
method can supply the container with a Func<T>
delegate directly. This bypasses its Auto-Wiring abilities. It will call the supplied delegate instead.
The Resolve
methods allows resolving a complete object graph. It gets the Func<T>
from the registrations
dictionary for the requested serviceType
, invokes it, and returns its value. In case there’s no registration for the requested type, Resolve
throws an exception. And finally, CreateNew
creates a new instance of a component by iterating over the component’s constructor parameters and calling back into the container recursively. It does so by calling Resolve
for each parameter, while supplying the parameter’s Type
. When all the type’s Dependencies are resolved in this way, it constructs the type itself by using reflection (using the System.Activator
class).
An AutoWireContainer
instance can be configured to compose arbitrary object graphs. Back in chapter 3 in listing 3.13, you created a HomeController
using Pure DI. The next listing repeats that listing from chapter 3. We’ll use that as an example to demonstrate the Auto-Wiring capabilities previously defined in AutoWireContainer
.
Listing 12.2 Composing an object graph for HomeController
using Pure DI
new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(connectionString)),
new AspNetUserContextAdapter()));
Instead of composing this object graph by hand, as done in the previous listing, you can use the AutoWireContainer
to register the five required components. To do this, you must map these five components to their appropriate Abstraction. Table 12.1 lists these mappings.
HomeController
Abstraction | Concrete type |
HomeController | HomeController |
IProductService | ProductService |
IProductRepository | SqlProductRepository |
CommerceContext | CommerceContext |
IUserContext | AspNetUserContextAdapter |
Listing 12.3 shows how you can use the AutoWireContainer
’s Register
methods to add the required mappings specified in table 12.1. Note that this listing uses Configuration as Code. We’ll discuss Configuration as Code in section 12.2.2.
Listing 12.3 Using AutoWireContainer
to register HomeController
var container = new AutoWireContainer(); ①
container.Register( ②
typeof(IUserContext), ②
typeof(AspNetUserContextAdapter)); ②
②
container.Register( ②
typeof(IProductRepository), ②
typeof(SqlProductRepository)); ②
②
container.Register( ②
typeof(IProductService), ②
typeof(ProductService)); ②
container.Register( ③
typeof(HomeController), ③
typeof(HomeController)); ③
container.Register( ④
typeof(CommerceContext), ④
() => new CommerceContext(connectionString)); ④
You might find the mapping for HomeController
in table 12.1 and listing 12.3 confusing, because it maps to itself instead of mapping to an Abstraction. This is a common practice, however, especially when dealing with types that are at the top of the object graph, such as MVC controllers.
You saw something similar in listings 4.4, 7.8, and 8.3, where you created a new HomeController
instance when a HomeController
type was requested. The main difference between those listings and listing 12.3 is that the latter uses a DI Container instead of Pure DI.
Listing 12.3 effectively registered all components required for the composition of an object graph of HomeController
. You can now use the configured AutoWireContainer
to create a new HomeController
.
Listing 12.4 Using AutoWireContainer
to resolve a HomeController
object controller = container.Resolve(typeof(HomeController));
When the AutoWireContainer
’s Resolve
method is called to request a new HomeController
type, the container will call itself recursively until it has resolved all of its required Dependencies. After this, a new HomeController
instance is created, while supplying the resolved Dependencies to its constructor. Figure 12.3 shows the recursive process, using a somewhat unconventional representation to visualize recursive calls. The container
instance is spread out over four separate vertical time lines. Because there are multiple levels of recursive calls, folding them into one single line, as is the norm with UML sequence diagrams, would be quite confusing.
When the DI Container receives a request for a HomeController
, the first thing it’ll do is look up the type in its configuration. HomeController
is a concrete class, which you mapped to itself. The container then uses reflection to inspect HomeController
’s one and only constructor with the following signature:
public HomeController(IProductService productService)
Because this constructor isn’t a parameterless constructor, it needs to repeat the process for the IProductService
constructor argument when following the general flowchart from figure 12.2. The container looks up IProductService
in its configuration and finds that it maps to the concrete ProductService
class. The single public constructor for ProductService
has this signature:
public ProductService(
IProductRepository repository,
IUserContext userContext)
That’s still not a parameterless constructor, and now there are two constructor arguments to deal with. The container takes care of each in order, so it starts with the IProductRepository
interface that, according to the configuration, maps to SqlProductRepository
. That SqlProductRepository
has a public constructor with this signature:
public SqlProductRepository(CommerceContext context)
That’s again not a parameterless constructor, so the container needs to resolve CommerceContext
to satisfy SqlProductRepository
’s constructor. CommerceContext
, however, is registered in listing 12.3 using the following delegate:
() => new CommerceContext(connectionString) ①
The container calls that delegate, which results in a new CommerceContext
instance. This time, no Auto-Wiring is used.
Now that the container has the appropriate value for CommerceContext
, it can invoke the SqlProductRepository
constructor. It has now successfully handled the Repository parameter for the ProductService
constructor, but it’ll need to hold on to that value for a while longer; it also needs to take care of ProductService
’s userContext
constructor parameter. According to the configuration, IUserContext
maps to the concrete AspNetUserContextAdapter
class, which has this public constructor:
public AspNetUserContextAdapter()
Because AspNetUserContextAdapter
contains a parameterless constructor, it can be created without having to resolve any Dependencies. It can now pass the new AspNetUserContextAdapter
instance to the ProductService
constructor. Together with the SqlProductRepository
from before, it now fulfills the ProductService
constructor and invokes it via reflection. Finally, it passes the newly created ProductService
instance to the HomeController
constructor and returns the HomeController
instance. Figure 12.4 shows how the general workflow presented in figure 12.2 maps to the AutoWireContainer
from listing 12.1.
The advantage of using a DI Container’s Auto-Wiring capabilities as shown in listing 12.3 rather than using Pure DI as shown in listing 12.2 is that with Pure DI, any change to a component’s constructor needs to be reflected in the Composition Root. Auto-Wiring, on the other hand, makes the Composition Root more resilient to such changes.
For example, let’s say you need to add a CommerceContext
Dependency to AspNetUserContextAdapter
in order for it to query the database. The following listing shows the change that needs to be made to the Composition Root when you apply Pure DI.
Listing 12.5 Composition Root for the changed AspNetUserContextAdapter
new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(connectionString)),
new AspNetUserContextAdapter(
new CommerceContext(connectionString)))); ①
With Auto-Wiring, on the other hand, no changes to the Composition Root are required in this case. AspNetUserContextAdapter
is Auto-Wired, and because its new CommerceContext
Dependency was already registered, the container will be able to satisfy the new constructor argument and will happily construct a new AspNetUserContextAdapter
.
This is how Auto-Wiring works, although DI Containers also need to take care of Lifetime Management and, perhaps, address Property Injection as well as other, more specialized, creational requirements.
The salient point is that Constructor Injection statically advertises the Dependency requirements of a class, and DI Containers use that information to Auto-Wire complex object graphs. A container must be configured before it can compose object graphs. Registration of components can be done in various ways.
Although the Resolve
method is where most of the action happens, you should expect to spend most of your time with a DI Container’s configuration API. Resolving object graphs is, after all, a single method call.
DI Containers tend to support two or three of the common configuration options shown in figure 12.5. Some don’t support configuration files, and others also lack support for Auto-Registration, whereas Configuration as Code support is ubiquitous. Most allow you to mix several approaches in the same application. Section 12.2.4 discusses why you’d want to use a mixed approach.
These three configuration options have different characteristics that make them useful in different situations. Both configuration files and Configuration as Code tend to be explicit, because they require you to register each component individually. Auto-Registration, on the other hand, is more implicit because it uses conventions to register a set of components by a single rule.
When you use Configuration as Code, you compile the container configuration into an assembly, whereas file-based configuration enables you to support late binding, where you can change the configuration without recompiling the application. In that dimension, Auto-Registration falls somewhere in the middle, because you can ask it to scan a single assembly known at compile time or, alternatively, to scan all assemblies in a predefined folder that might be unknown at compile time. Table 12.2 lists the advantages and disadvantages of each option.
Style | Description | Advantages | Disadvantages |
Configuration files | Mappings are specified in configuration files (typically in XML or JSON format) |
|
|
Configuration as Code | Code explicitly determines mappings |
|
|
Auto-Registration | Rules are used to locate suitable components using reflection and to build the mappings. |
|
|
Historically, DI Containers started out with configuration files, which also explains why the older libraries still support this. But this feature has been downplayed in favor of more conventional approaches. That’s why more recently developed DI Containers, such as Simple Injector and Microsoft.Extensions.DependencyInjection, don’t have any built-in support for file-based configuration.
Although Auto-Registration is the most modern option, it isn’t the most obvious place to start. Because of its implicitness, it may seem more abstract than the more explicit options, so instead, we’ll cover each option in historical order, starting with configuration files.
When DI Containers first appeared back in the early 2000s, they all used XML as a configuration mechanism — most things did back then. Experience with XML as a configuration mechanism later revealed that this is rarely the best option.
XML tends to be verbose and brittle. When you configure a DI Container in XML, you identify various classes and interfaces, but you have no compiler support to warn you if you misspell something. Even if the class names are correct, there’s no guarantee that the required assembly is going to be in the application’s probing path.
To add insult to injury, the expressiveness of XML is limited compared to that of plain code. This sometimes makes it hard or impossible to express certain configurations in a configuration file that are otherwise trivial to express in code. In listing 12.3, for instance, you registered the CommerceContext
using a lambda expression. Such a lambda expression can be expressed in neither XML nor JSON.
The advantage of configuration files, on the other hand, is that you can change the behavior of the application without recompilation. This is valuable if you develop software that ships to thousands of customers, because it gives them a way to customize the application. But if you write an internal application or a website where you control the deployment environment, it’s often easier to recompile and redeploy the application when you need to change the behavior.
A DI Container is often configured with files by pointing it to a particular configuration file. The following example uses Autofac as an example.
In this example, you’ll configure the same classes as in section 12.1.3. A large part of the task is to apply the configuration outlined in table 12.1, but you must also supply a similar configuration to support composition of the HomeController
class. The following listing shows the configuration necessary to get the application up and running.
Listing 12.6 Configuring Autofac with a JSON configuration file
{
"defaultAssembly": "Commerce.Web", ①
"components": [
{
"services": [{ ②
"type": ②
"Commerce.Domain.IUserContext, Commerce.Domain" ②
}], ②
"type": ②
"Commerce.Web.AspNetUserContextAdapter" ②
},
{
"services": [{
"type": "Commerce.Domain.IProductRepository, Commerce.Domain"
}],
"type": "Commerce.SqlDataAccess.SqlProductRepository, Commerce.
➥
SqlDataAccess"
},
{
"services": [{
"type": "Commerce.Domain.IProductService, Commerce.Domain"
}],
"type":
"Commerce.Domain.ProductService, Commerce.Domain"
},
{
"type": "Commerce.Web.Controllers.HomeController" ③
},
{
"type": "Commerce.SqlDataAccess.CommerceContext, ④
➥
Commerce.SqlDataAccess", ④
"parameters": { ④
"connectionString": ④
"Server=.;Database=MaryCommerce;Trusted_ ④
➥
Connection=True;" ④
}
}]
}
In this example, if you don’t specify an assembly-qualified type name in a type or interface reference, defaultAssembly
will be assumed to be the default assembly. For a simple mapping, full type names must be used, including namespace and assembly name. Because AspNetUserContextAdapter
excluded the name of the assembly, Autofac looks for it in the Commerce.Web
assembly, which you defined as the defaultAssembly
.
As you can see from even this simple code listing, JSON configuration tends to be quite verbose. Simple mappings like the one from the IUserContext
interface to the AspNetUserContextAdapter
class require quite a lot of text in the form of brackets and fully qualified type names.
As you may recall, CommerceContext
takes a connection string as input, so you need to specify how the value of this string is found. By adding parameters
to a mapping, you can specify values by their parameter name — in this case, connectionString
. Loading the configuration into the container is done with the following code.
Listing 12.7 Reading configuration files using Autofac
var builder = new Autofac.ContainerBuilder(); ①
IConfigurationRoot configuration = ②
new ConfigurationBuilder() ②
.AddJsonFile("autofac.json") ②
.Build(); ②
builder.RegisterModule( ③
new Autofac.Configuration.ConfigurationModule( ③
configuration)); ③
Autofac is the only DI Container included in this book that supports configuration files, but there are other DI Containers not covered here that continue to support configuration files. The exact schema is different for each container, but the overall structure tends to be similar, because you need to map an Abstraction to an implementation.
Tip Although configuration files can work in a small application, or when used for small portions of your application, they don’t scale. Avoid using configuration files as your default method of DI configuration. As we’ll discuss in section 12.3, use either Pure DI or Auto-Registration.
Don’t let the absence of support for handling configuration files influence your choice of a DI Container too much. As described previously, only true late-bound components should be defined in configuration files, which will unlikely be more than a handful. Even with absence of support from your container, types can be loaded from configuration files in a few simple statements, as shown in listing 1.2.
Because of the disadvantages of verbosity and brittleness, you should prefer the other alternatives for configuring containers. Configuration as Code is similar to configuration files in granularity and concept, but obviously uses code instead of configuration files.
Perhaps the easiest way to compose an application is to hard code the construction of object graphs. This may seem to go against the whole spirit of DI, because it determines the concrete implementations that should be used for all Abstractions at compile time. But if done in a Composition Root, it only violates one of the benefits listed in table 1.1, namely, late binding.
The benefit of late binding is lost if Dependencies are hard-coded, but, as we mentioned in chapter 1, this may not be relevant for all types of applications. If your application is deployed in a limited number of instances in a controlled environment, it can be easier to recompile and redeploy the application if you need to replace modules:
I often think that people are over-eager to define configuration files. Often a programming language makes a straightforward and powerful configuration mechanism.4
Martin Fowler
4 Martin Fowler, “Inversion of Control Containers and the Dependency Injection pattern,” 2004, https://martinfowler.com/articles/injection.html.
When you use Configuration as Code, you explicitly state the same discrete mappings as when you use configuration files — only you use code instead of XML or JSON.
Definition In the context of DI Containers, Configuration as Code allows the container’s configuration to be stored as source code. Each mapping between an Abstraction and a particular implementation is expressed explicitly and directly in code.
All modern DI Containers fully support Configuration as Code as the successor to configuration files; in fact, most of them present this as the default mechanism, with configuration files as an optional feature. As stated previously, some don’t even offer support for configuration files at all. The API exposed to support Configuration as Code differs from DI Container to DI Container, but the overall goal is still to define discrete mappings between Abstractions and concrete types.
Tip Prefer Configuration as Code over configuration files unless you need late binding. The compiler can be helpful, and the Visual Studio build system automatically copies all required assemblies to the output folder. And if you do need late binding, only use a configuration file for the parts of the configuration that need to be late bound, which is typically just a tiny subset of the types in the entire application.
Let’s take a look how to configure the e-commerce application using Configuration as Code with Microsoft.Extensions.DependencyInjection. For this, we’ll use an example that configures the sample e-commerce application with code.
In section 12.2.1, you saw how to configure the sample e-commerce application with configuration files using Autofac. We could also demonstrate Configuration as Code with Autofac, but, to make this chapter a bit more interesting, we’ll instead use Microsoft.Extensions.DependencyInjection in this example. Using Microsoft’s configuration API, you can express the configuration from listing 12.6 more compactly, as shown here.
Listing 12.8 Configuring Microsoft.Extensions.DependencyInjection with code
var services = new ServiceCollection(); ①
services.AddSingleton< ②
IUserContext, ②
AspNetUserContextAdapter>(); ②
services.AddTransient<
IProductRepository,
SqlProductRepository>();
services.AddTransient<
IProductService,
ProductService>();
services.AddTransient<HomeController>(); ③
services.AddScoped<CommerceContext>( ④
p => new CommerceContext(connectionString)); ④
Defines mappings between Abstractions and implementations
Adds Auto-Wired mappings between Abstractions and concrete types
Overload that takes the concrete type as a generic type argument
Overload that allows mapping an Abstraction to a Func<T> delegate
ServiceCollection
is Microsoft’s equivalent to Autofac’s ContainerBuilder
, which defines the mappings between Abstractions and implementations. The AddTransient
, AddScoped
, and AddSingleton
methods are used to add Auto-Wired mappings between Abstractions and concrete types for their specific Lifestyle. These methods are generic, which results in more condensed code with the additional benefit of getting some extra compile-time checking. In case a concrete type maps to itself, instead of having an Abstraction mapping to a concrete type, there’s a convenient overload that just takes in the concrete type as a generic type argument. And, just as with the AutoWireContainer
example of listing 12.1, the API of this DI Container contains an overload that allows mapping an Abstraction to a Func<T>
delegate.
Note If this looks familiar, that isn’t a surprise: it’s conceptually almost identical to our sample code in listing 12.3. There, we established a proof of concept for how Auto-Wiring is accomplished.
In listing 12.8, we took the liberty of demonstrating the registration of components using the three common lifestyles: Singleton, Transient, and Scoped. The following chapters show how to configure lifestyles for each container in more detail.
Compare this code with listing 12.6, and notice how much more compact it is — even though it does the exact same thing. A simple mapping like the one from IProductService
to ProductService
is expressed with a single method call.
Not only is Configuration as Code much more compact than configurations expressed in a configuration file, it also enjoys compiler support. The type arguments used in listing 12.8 represent real types that the compiler checks. Generics go even a step further, because the use of generic type constraints such as Microsoft’s API applies allows the compiler to check whether the supplied concrete type matches the Abstraction. If a conversion isn’t possible, the code won’t compile.
Although Configuration as Code is safe and easy to use, it still requires more maintenance than you might like. Every time you add a new type to an application, you must also remember to register it — and many registrations end up being similar. Auto-Registration addresses this issue.
Considering the registrations of listing 12.8, it might be completely fine to have these few lines of code in your project. When a project grows, however, so will the amount of registrations required to set up the DI Container. In time, you’re likely to see many similar registrations appear. They’ll typically follow a common pattern. The following listing shows how these registrations can start to look somewhat repetitive.
Listing 12.9 Repetition in registrations when using Configuration as Code
services.AddTransient<IProductRepository, SqlProductRepository>();
services.AddTransient<ICustomerRepository, SqlCustomerRepository>();
services.AddTransient<IOrderRepository, SqlOrderRepository>();
services.AddTransient<IShipmentRepository, SqlShipmentRepository>();
services.AddTransient<IImageRepository, SqlImageRepository>();
services.AddTransient<IProductService, ProductService>();
services.AddTransient<ICustomerService, CustomerService>();
services.AddTransient<IOrderService, OrderService>();
services.AddTransient<IShipmentService, ShipmentService>();
services.AddTransient<IImageService, ImageService>();
Repeatedly writing registration code like that violates the DRY principle. It also seems like an unproductive piece of infrastructure code that doesn’t add much value to the application. You can save time and make fewer errors if you can automate the registration of components, assuming those components follow some sort of convention. Many DI Containers provide Auto-Registration capabilities that let you introduce your own conventions and apply Convention over Configuration.
Definition Auto-Registration is the ability to automatically register components in a container by scanning one or more assemblies for implementations of desired Abstractions, based on a certain convention. Auto-Registration is sometimes referred to as Batch Registration or Assembly Scanning.
An increasingly popular architectural model is the concept of Convention over Configuration. Instead of writing and maintaining a lot of configuration code, you can agree on conventions that affect the code base. The way ASP.NET Core MVC finds controllers based on controller names is a great example of a simple convention:5
5 The description of this convention for finding MVC controllers is simplified. In reality, there’s more to it (see https://mng.bz/lED8).
HomeController
. If it finds such a class, it’s a match.The convention here is that a controller must be named [ControllerName]Controller.
Conventions can be applied to more than ASP.NET Core MVC controllers. The more conventions you add, the more you can automate the various parts of the container configuration.
Tip Convention over Configuration has more advantages than just supporting DI configuration. It makes your code more consistent, because it automatically works, as long as you follow your conventions.
In reality, you may need to combine Auto-Registration with Configuration as Code or configuration files, because you may not be able to fit every single component into a meaningful convention. But the more you can move your code base towards conventions, the more maintainable it will be.
Autofac supports Auto-Registration, but we thought it would be more interesting to use yet another DI Container to configure the sample e-commerce application using conventions. Because we like to restrain the examples to the DI Containers discussed in this book, and because Microsoft.Extensions.DependencyInjection doesn’t have any Auto-Registration facilities, we’ll use Simple Injector to illustrate this concept.
Looking back at listing 12.9, you’ll likely agree that the registrations of the various data access components are repetitive. Can we express some sort of convention around them? All five concrete Repository types of listing 12.9 share some characteristics:
It seems that an appropriate convention would express these similarities by scanning the assembly in question and registering all classes that match the convention. Even though Simple Injector does support Auto-Registration, its Auto-Registration API focuses around the registration of groups of types that share the same interface. Its API, by itself, doesn’t allow you to express this convention, because there’s no single interface that describes this group of repositories.
At first, this omission might seem rather awkward, but defining a custom LINQ query on top of .NET’s reflection API is typically easy to write, provides more flexibility, and prevents you from having to learn another API — assuming you’re familiar with LINQ and .NET’s reflection API. The following listing shows such a convention using a LINQ query.
Listing 12.10 Convention for scanning repositories using Simple Injector
var assembly = ①
typeof(SqlProductRepository).Assembly; ①
var repositoryTypes = ②
from type in assembly.GetTypes() ②
where !type.Abstract ②
where type.Name.EndsWith("Repository") ②
select type; ②
foreach (Type type in repositoryTypes) ③
{ ③
container.Register( ③
type.GetInterfaces().Single(), type); ③
} ③
Selects an assembly for the convention
Defines a LINQ query that locates all types in the assembly that fit the criterion of being concrete and ending with Repository
Iterates over the LINQ query to register each type
Each of the classes that make it through the where
filters during iteration should be registered against their interface. For example, because SqlProductRepository
’s interface is an IProductRepository
, it’ll end up as a mapping from IProductRepository
to SqlProductRepository
.
This particular convention scans the assembly that contains the data access components. You could get a reference to that assembly in many ways, but the easiest way is to pick a representative type, such as SqlProductRepository
, and get the assembly from that, as shown in listing 12.10. You could also have chosen a different class or found the assembly by name.
Note With Microsoft.Extensions.DependencyInjection, the code of the convention of listing 12.10 would be almost identical. Only the body of the foreach
loop would be different, because that’s the only place the DI Container’s API is called.
Comparing this convention against the four registrations in listing 12.9, you may think that the benefits of this convention look negligible. Indeed, because there are only four data access components in the current example, the amount of code statements has increased with the convention. But this convention scales much better. Once you write it, it handles hundreds of components without any additional effort.
You can also address the other mappings from listings 12.6 and 12.8 with conventions, but there wouldn’t be much value in doing so. As an example, you can register all services with this convention:
var assembly = typeof(ProductService).Assembly;
var serviceTypes =
from type in assembly.GetTypes()
where !type.Abstract
where type.Name.EndsWith("Service")
select type;
foreach (Type type in serviceTypes)
{
container.Register(type.GetInterfaces().Single(), type);
}
This convention scans the identified assembly for all concrete classes where the name ends with Service and registers each type against the interface it implements. This effectively registers ProductService
against the IProductService
interface, but because you currently don’t have any other matches for this convention, nothing much is gained. It’s only when more services are added, as indicated in listing 12.9, that it starts to make sense to formulate a convention.
Defining conventions by hand with the use of LINQ might make sense for types all deriving from their own interface, as you’ve seen previously with the repositories. But when you start to register types that are based on a generic interface, as we extensively discussed in section 10.3.3, this strategy starts to break down rather quickly — querying generic types through reflection is typically not a pleasant thing to do.6
6 You’ll have to find implementations of the generic interface, consider types that implement multiple interfaces, register all Decorators for all implementations, and so forth.
That’s why Simple Injector’s Auto-Registration API is built around the registration of types based on a generic Abstraction, such as the ICommandService<TCommand>
interface from listing 10.12. Simple Injector allows the registration of all ICommandService<TCommand>
implementations to be done in a single line of code.
Listing 12.11 Auto-Registering implementations based on a generic Abstraction
Assembly assembly = typeof(AdjustInventoryService).Assembly;
container.Register(typeof(ICommandService<>), assembly);
Note ICommandService<>
is the C# syntax for specifying the open-generic version, accomplished by omitting the TCommand
generic type argument.
By supplying a list of assemblies to one of its Register
overloads, Simple Injector iterates through these assemblies to find any non-generic, concrete types that implement ICommandService<TCommand>
, while registering each type by its specific ICommandService<TCommand> interface. This has the generic type argument
TCommand
filled in with an actual type.
Definition A generic type that has its generic type arguments filled in (for example, ICommandService<AdjustInventory>
), is called a closed generic. Likewise, when you have just the generic type definition itself (for example, ICommandService<TCommand>
), such a type is referred to as open generic.
In an application with four ICommandService<TCommand>
implementations, the previous API call would be equivalent to the following Configuration as Code listing.
Listing 12.12 Registering implementations using Configuration as Code
container.Register(typeof(ICommandService<AdjustInventory>),
typeof(AdjustInventoryService));
container.Register(typeof(ICommandService<UpdateProductReviewTotals>),
typeof(UpdateProductReviewTotalsService));
container.Register(typeof(ICommandService<UpdateHasDiscountsApplied>),
typeof(UpdateHasDiscountsAppliedService));
container.Register(typeof(ICommandService<UpdateHasTierPricesProperty>),
typeof(UpdateHasTierPricesPropertyService));
Iterating a list of assemblies to find appropriate types, however, isn’t the only thing you can achieve with Simple Injector’s Auto-Registration API. Another powerful feature is the registration of generic Decorators, like the ones you saw in listings 10.15, 10.16, and 10.19. Instead of manually composing the hierarchy of Decorators, as you did in listing 10.21, Simple Injector allows Decorators to be applied using its RegisterDecorator
method overloads.
Listing 12.13 Registering generic Decorators using Auto-Registration
container.RegisterDecorator( ①
typeof(ICommandService<>), ①
typeof(AuditingCommandServiceDecorator<>)); ①
①
container.RegisterDecorator( ①
typeof(ICommandService<>), ①
typeof(TransactionCommandServiceDecorator<>)); ①
①
container.RegisterDecorator( ①
typeof(ICommandService<>), ①
typeof(SecureCommandServiceDecorator<>)); ①
RegisterDecorator is supplied with the open-generic ICommandService<TCommand> service type and the open-generic implementation for the Decorator. Using this information, Simple Injector wraps every ICommandService<TCommand> that it resolves with the appropriate Decorators.
Simple Injector applies Decorators in order of registration, which means that, in respect to listing 12.13, the auditing Decorator is wrapped using the transaction Decorator, and the transaction Decorator is wrapped with the security Decorator, resulting in an object graph identical to the one shown in listing 10.21.
Registration of open-generic types can be seen as a form of Auto-Registration because a single method call to RegisterDecorator
can result in a Decorator being applied to many registrations.7 Without this form of Auto-Registration for generic Decorator classes, you’d be forced to register each closed version of each Decorator for each closed ICommandService<TCommand>
implementation individually, as the following listing shows.
7 On top of that, there’s a lot going on in the background. For instance, if the Decorator contains generic-type constraints, Simple Injector automatically finds out whether the Decorator is applicable to a given registration based on these type constraints. Doing this by hand would be cumbersome and error prone.
Listing 12.14 Registering generic Decorators using Configuration as Code
container.RegisterDecorator(
typeof(ICommandService<AdjustInventory>),
typeof(AuditingCommandServiceDecorator<AdjustInventory>));
container.RegisterDecorator(
typeof(ICommandService<AdjustInventory>),
typeof(TransactionCommandServiceDecorator<AdjustInventory>));
container.RegisterDecorator(
typeof(ICommandService<AdjustInventory>),
typeof(SecureCommandServiceDecorator<AdjustInventory>));
container.RegisterDecorator(
typeof(ICommandService<UpdateProductReviewTotals>),
typeof(AuditingCommandServiceDecorator<UpdateProductReviewTotals>));
container.RegisterDecorator(
typeof(ICommandService<UpdateProductReviewTotals>),
typeof(TransactionCommandServiceDecorator<UpdateProductReviewTotals>));
container.RegisterDecorator(
typeof(ICommandService<UpdateProductReviewTotals>),
typeof(SecureCommandServiceDecorator<UpdateProductReviewTotals>));
container.RegisterDecorator(
typeof(ICommandService<UpdateHasDiscountsApplied>),
typeof(AuditingCommandServiceDecorator<UpdateHasDiscountsApplied>));
container.RegisterDecorator(
typeof(ICommandService<UpdateHasDiscountsApplied>),
typeof(TransactionCommandServiceDecorator<UpdateHasDiscountsApplied>));
container.RegisterDecorator(
typeof(ICommandService<UpdateHasDiscountsApplied>),
typeof(SecureCommandServiceDecorator<UpdateHasDiscountsApplied>));
... ①
Other registrations are omitted for brevity.
The code in this listing is cumbersome and error prone. Additionally, it would cause an exponential growth of the Composition Root.
Tip The most prominent downside of Auto-Registration is that you lose some control. It must be possible to Auto-Wire every component that’s picked up by the Auto-Registration facility. When there’s a particular component that requires hand wiring, it should be excluded from Auto-Registration to prevent errors.
In a system that adheres to the SOLID principles, you create many small and focused classes, but existing classes are less likely to change, increasing maintainability. Auto-Registration prevents the Composition Root from constantly being updated. It’s a powerful technique that has the potential to make the DI Container invisible. Once appropriate conventions are in place, you may have to modify the container configuration only on rare occasions.
So far, you’ve seen three different approaches to configuring a DI Container:
None of these are mutually exclusive. You can choose to mix Auto-Registration with specific mappings of abstract-to-concrete types, and even mix all three approaches to have some Auto-Registration, some Configuration as Code, and some of the configuration in configuration files for late binding purposes.
As a rule of thumb, you should prefer Auto-Registration as a starting point, complemented by Configuration as Code to handle more special cases. You should reserve configuration files for cases where you need to be able to vary an implementation without recompiling the application — which is rarer than you may think.
Now that we’ve covered how to configure a DI Container and how to resolve object graphs with one, you should have a good idea about how to use them. Using a DI Container is one thing, but understanding when to use one is another.
In the previous parts of this book, we solely used Pure DI as our method of Object Composition. This wasn’t just for educational purposes. Complete applications can be built using Pure DI alone.
In section 12.2, we talked about the different configuration methods of DI Containers and how the use of Auto-Registration can increase maintainability of your Composition Root. But the use of DI Containers comes with additional costs and disadvantages over Pure DI. Most, if not all, DI Containers are open source, so they’re free in a monetary sense. But because developer hours are typically the most expensive part of software development, anything that increases the time it takes to develop and maintain software is a cost, which is what we’ll talk about here.
In this section, we’ll compare the advantages and disadvantages, so you can make an educated decision about when to use a DI Container and when to stick to Pure DI. Let’s start with an often overlooked aspect of using libraries such as DI Containers, which is that they introduce costs and risks.
When a library is free in a monetary sense, we developers often tend to ignore the other costs involved in using it. A DI Container might be considered a Stable Dependency (section 1.3.1), so from a DI perspective, using one isn’t an issue. But there are other concerns to consider. As with any third-party library, using a DI Container comes with costs and risks.
The most obvious cost of any library is its learning curve — it takes time to learn to use a new library. You have to learn its API, its behavior, its quirks, and its limitations. When you’re with a team of developers, most of them will have to understand how to work with that library in one way or another. Having just one developer that knows how to work with the tool might save costs in the short run, but such a practice is in itself a liability to the continuity of your project.8
8 This is often referred to as the bus factor. The bus factor is the minimum number of team members that have to suddenly disappear from a project (get hit by a bus) before the project stalls due to lack of knowledgeable or competent personnel.
A library’s behavior, quirks, and limitations might not exactly suit your needs. A library might be opinionated towards a different model than the one your software is built around.9 This is typically something you only find out while you’re learning to use it. As you apply it to your code base, you may find that you need to implement various workarounds. This can result in much yak shaving.
9 The library maker’s opinions might be an opportunity for you and your team to learn something, but it might simply be a different opinion, incompatible with yours.
It is, therefore, hard to estimate how much money the use of a new library will save the project because of the learning costs that are often hard to realistically estimate. The accumulated time spent on learning the API of a third-party library is time not spent building the application itself, and therefore represents a real cost.
Besides the direct cost of learning to work with a library, there are risks involved in taking a dependency on such a library. One risk is that the developers stop maintaining and supporting a library you’re using.10 When such an event occurs, it introduces extra costs to the project because it can force you to switch libraries. In that case, you’re paying the previously discussed learning costs all over again with the additional costs of migrating and testing the application again.
10 This isn’t a risk with third-party libraries only. Even Microsoft, one of the organizations most committed to long-term support of their technologies, has been known to break compatibility or abandon technologies. Examples include Workflow Foundation, Silverlight, Visual Studio LightSwitch, Windows Phone, and Windows RT. These days, Microsoft seems to be back to a more stable commitment to long-term support. The point is that one can never be certain.
Tip Because of these costs and risks, care should be taken in selecting the right libraries for your project. When starting a new project, to mitigate the risks, it’s therefore advisable to limit the amount of external libraries your team needs to become familiar with.
This all sounds like an argument against using external libraries, but that isn’t the case. You wouldn’t be productive without external libraries, because you’d have to reinvent the wheel. If not using an external library means building such a library yourself, you’ll often be worse off. (And we developers tend to underestimate the time it takes to write, test, and maintain such a piece of software.)
With DI Containers, however, you’re in a somewhat different situation. That’s because the alternative to using an external DI Container library isn’t to build your own, but to apply Pure DI.
At first sight, listing 12.1 might seem to imply that a DI Container can be written in a few lines of code. Although listing 12.1 sketches the first steps in writing a DI Container, there’s a clear reason it’s flagged as bad code.
The code in listing 12.1 is a naive implementation that, as we stated earlier, lacks many crucial capabilities. A fully functional DI Container should support Lifetime Management, Interception, Auto-Registration, and Dependency cycle detection; communicate configuration mistakes effectively; have properly designed extensibility points; rest on great documentation; and much, much more. This isn’t something you’ll be able to do in a couple of weeks.
From experience, I (Steven) can tell you that it takes years for such a library to become stable and mature. And although it might be a great learning experience for you as a developer, it doesn’t help your project or your company, because your focus should be on producing business value.11
11 Writing and maintaining Simple Injector and supporting its community gave me a lot of knowledge, which eventually led to me becoming the coauthor of this book.
This doesn’t mean you should never create a new open source library such as a DI Container. Innovation is an important aspect of our industry, and the creation of new libraries helps with this. Sometimes we need radical new ideas, and this sometimes means we need to build new libraries and frameworks based on those ideas. You should, however, be cautious about spending your employer’s money on this, because it’ll cost your employer way more than you initially envision.
As you learned in section 4.1, interaction with the DI Container should be limited to the Composition Root. This already reduces the risk when it must be replaced. But even in that case, it can be a time-consuming endeavor to replace the DI Container and become familiar with a new API and design philosophy.
The major advantage of Pure DI is that it’s easy to learn. You don’t have to learn the API of any DI Container and, although individual classes still use DI, once you find the Composition Root, it’ll be evident what’s going on and how object graphs are constructed. Although newer IDEs make this less of a problem, it can be difficult for a new developer on a team to get a sense of the constructed object graph and to find the implementation for a class’s Dependency when a DI Container is used.
With Pure DI, this is less of a problem, because object graph construction is hard coded in the Composition Root. Besides being easier to learn, Pure DI gives you a shorter feedback cycle in case there’s an error in your composition of objects. Let’s look at that next.
DI Container techniques, such as Auto-Wiring and Auto-Registration, depend on the use of reflection. This means that, at runtime, the DI Container will analyze constructor arguments using reflection or even query through complete assemblies to find types based on conventions in order to compose complete object graphs. Consequently, configuration errors are only detected at runtime when an object graph is resolved. Compared to Pure DI, the DI Container assumes the compiler’s role of code verification.
Important Pure DI has a big advantage that’s often overlooked: it’s strongly typed. This allows the compiler to provide feedback about correctness, which is the fastest feedback that you can get.
When a Composition Root is well structured so that the creation of Singletons and Scoped instances are separated (see listings 8.10 and 8.13, for instance), it allows the compiler to detect Captive Dependencies, as discussed in section 8.4.1.
As we discussed in section 3.2.2, because of strong typing, Pure DI also has the advantage of giving you a clearer picture of the structure of the application’s object graphs. This is something that you’ll lose immediately when you start using a DI Container.
But strong typing cuts both ways because, as we discussed in section 12.1.3, it also means that every time you refactor a constructor, you’ll break the Composition Root. If you’re sharing a library (domain model, utility, data access component, and so on) between applications, you may have more than one Composition Root to maintain. How much of a burden this is depends on how often you refactor constructors, but we’ve seen projects where this happens several times each day. With multiple developers working on a single project, this can easily lead to merge conflicts, which cost time to fix.
Although the compiler will give rapid feedback when using Pure DI, the amount of validations it can do is limited. It’ll be able to report missing Dependencies due to changes to constructors and to some extent Captive Dependencies, but, among other things, it will fail to detect the following:
12 These defects are sometimes referred to as Torn Lifestyles (https://simpleinjector.org/diatl) and Ambiguous Lifestyles (https://simpleinjector.org/diaal).
When using Pure DI, the size of the Composition Root grows linearly with the size of the application. When an application is small, its Composition Root will also be small. This makes its Composition Root clean and manageable, and previously listed defects will be easy to spot. But when the Composition Root grows, it becomes easier to miss such defects.
This is something that the use of a DI Container can mitigate. Most DI Containers automatically detect a disposable component on your behalf and might detect common pitfalls, such as Captive Dependencies.13
13 The three DI Containers discussed in this book all detect Captive Dependencies to some extent.
If you use a DI Container’s Configuration as Code abilities (as discussed in section 12.2.2), explicitly registering each and every component using the container’s API, you lose the rapid feedback from strong typing. On the other hand, the maintenance burden is also likely to drop because of Auto-Wiring. Still, you’ll need to register each new class when you introduce it, which is a linear growth, and you and your team have to learn the specific API of that container. But even if you’re already familiar with its API, there’s still the risk of having to replace it someday. You might lose more than you gain.
Ultimately, if you can wield a DI Container in a sufficiently sophisticated way, you can use it to define a set of conventions using Auto-Registration (as discussed in section 12.2.3). These conventions define a rule set that your code should adhere to, and as long as you stick to those rules, things just work. The container drops to the background, and you rarely need to touch it.
Important The use of Convention over Configuration using Auto-Registration can minimize the amount of maintenance on the Composition Root to almost zero.
Auto-Registration takes time to learn, and is weakly typed, but, if done right, it enables you to focus on code that adds value instead of infrastructure. An additional advantage is that it creates a positive feedback mechanism, forcing a team to produce code that’s consistent with the conventions. Figure 12.6 visualizes the trade-off between Pure DI and using a DI Container.
As we stated in section 12.2.4, none of the available approaches are mutually exclusive. Although you might find a single Composition Root to contain a mix of all configuration styles, a Composition Root should either be focused around Pure DI with, perhaps, a few late-bound types, or around Auto-Registration with, optionally, a limited amount of Configuration as Code, Pure DI, and configuration files. A Composition Root that focuses around Configuration as Code is pointless and should therefore be avoided.
The question then becomes this: when should you choose Pure DI, and when should you use Auto-Registration? We, unfortunately, can’t give any hard numbers on this. It depends on the size of the project, the amount of experience you and your team have with a DI Container, and the calculation of risk.
In general, though, you should use Pure DI for Composition Roots that are small and switch to Auto-Registration when maintaining such a Composition Root becomes a problem. Bigger applications with many classes that can be captured by several conventions can benefit from using Auto-Registration.14
14 We also promote the idea of keeping applications relatively small. This inevitably leads to the concept of Bounded Contexts. See Eric J. Evans, Domain-Driven Design, 335.
I (Mark) once worked for a client where I applied convention-based Auto-Registration in a code base. The other developers weren’t too happy with it because they found it too automagical. They fully embraced DI and used TDD, but weren’t keen on using a DI Container, because they weren’t familiar with its API.
In many cases, the conventions worked as advertised. When developers introduced new classes or interfaces, the DI Container discovered the new types and correctly configured them. Once in a while, however, developers (including myself) would implement a feature in a way not anticipated by the conventions. When that happened, it was necessary to adjust the conventions.
The other developers didn’t understand — and weren’t interested in learning — how to work with the DI Container’s API, so whenever a change was required, I had to implement it. I became a critical resource, and occasionally a bottleneck. When I left the project, I expected the remaining team to rip out the DI Container and replace it with Pure DI. When I returned a year later, I wasn’t surprised to learn that this was exactly what they had done. I can’t say that I blamed them.
The other thing we won’t tell you is which DI Container to choose. Selecting a DI Container involves more than technical evaluation. You must also evaluate whether the licensing model is acceptable, whether you trust the people or organization that develops and maintains the DI Container, how it fits into your organization’s IT strategy, and so on. Your search for the right DI Container also shouldn’t be limited to the containers listed in this book. For example, many excellent DI Containers for the .NET platform are available to choose from.
A DI Container can be a helpful tool if you use it correctly. The most important thing to understand is that the use of DI in no way depends on the use of a DI Container. An application can be made from many loosely coupled classes and modules, and none of these modules knows anything about a container. The most effective way to make sure that application code is unaware of any DI Container is by limiting its use to the Composition Root. This prevents you from inadvertently applying the Service Locator anti-pattern, because it constrains the container to a small, isolated area of the code.
Used in this way, a DI Container becomes an engine that takes care of part of the application’s infrastructure. It composes object graphs based on its configuration. This can be particularly beneficial if you employ Convention over Configuration. If suitably implemented, it can take care of composing object graphs, and you can concentrate your efforts on implementing new features. The container will automatically discover new classes that follow the established conventions and make them available to consumers. The final three chapters of this book cover Autofac (chapter 13), Simple Injector (chapter 14), and Microsoft.Extensions.DependencyInjection (chapter 15).
3.133.132.99