12
DI Container introduction

In this chapter

  • Using configuration files to enable late binding
  • Explicitly registering components in a DI Container with Configuration as Code
  • Applying Convention over Configuration in a DI Container with Auto-Registration
  • Choosing between applying Pure DI or using a DI Container

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).

12-01.tif

Figure 12.1 An Italian ice cream maker. As with making ice cream, with better technology, you can accomplish programming tasks more easily and quickly.

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.

12.1 Introducing DI Containers

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.

12.1.1 Exploring containers’ Resolve API

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.

Resolving controllers with various DI Containers

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.

Resolving object graphs with DI Containers

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.

12.1.2 Auto-Wiring

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.

12-02.eps

Figure 12.2 Simplified workflow for Auto-Wiring. A DI Container uses its configuration to find the appropriate concrete class that matches the requested type. It then uses reflection to examine the class’s constructor.

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.

12.1.3 Example: Implementing a simplistic DI Container that supports Auto-Wiring

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.

bad.tif

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.

Table 12.1 Mapping types to support Auto-Wiring of HomeController
AbstractionConcrete type
HomeControllerHomeController
IProductServiceProductService
IProductRepositorySqlProductRepository
CommerceContextCommerceContext
IUserContextAspNetUserContextAdapter

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.

12-03.eps

Figure 12.3 The Composition Root requests a HomeController from the container, which recursively calls back into itself to request HomeController’s Dependencies.

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.

12-04.eps

Figure 12.4 Simplified workflow for Auto-Wiring mapped to the code from listing 12.1. The registrations dictionary is queried for the concrete type, its constructor parameters get resolved, and the concrete type is created using its resolved Dependencies.

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.

12.2 Configuring DI Containers

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.

12-05.eps

Figure 12.5 The most common ways to configure a DI Container shown against dimensions of explicitness and the degree of binding

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.

Table 12.2 Configuration options
StyleDescriptionAdvantagesDisadvantages
Configuration filesMappings are specified in configuration files (typically in XML or JSON format)
  • Supports replacement without recompilation
  • No compile-time checks
  • Verbose and brittle
Configuration as CodeCode explicitly determines mappings
  • Compile-time checks
  • High degree of control
  • No support for replacement without recompilation
Auto-RegistrationRules are used to locate suitable components using reflection and to build the mappings.
  • Supports replacement without recompilation
  • Less effort required
  • Helps enforce conventions to make a code base more consistent
  • No compile-time checks
  • Less control
  • May seem more abstract at first

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.

12.2.1 Configuring containers 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.

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.

12.2.2 Configuring containers using Configuration as Code

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

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.

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.

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));    ④  

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.

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.

12.2.3 Configuring containers by convention using Auto-Registration

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.

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:

  • They’re all defined in the same assembly.
  • Each concrete class has a name that ends with Repository.
  • Each implements a single interface.

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);  ③  
}    ③  

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.

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 

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);

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.

In an application with four ICommandService<TCommand> implementations, the previous API call would be equivalent to the following Configuration as Code listing.

smell.tif

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<>));    ①  

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.

bad.tif

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>));

...   ① 

The code in this listing is cumbersome and error prone. Additionally, it would cause an exponential growth of the Composition Root.

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.

12.2.4 Mixing and matching configuration approaches

So far, you’ve seen three different approaches to configuring a DI Container:

  • Configuration files
  • Configuration as Code
  • Auto-Registration

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.

12.3 When to use a DI Container

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.

12.3.1 Using third-party libraries involves 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 

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.

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.

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.

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.

12.3.2 Pure DI gives a shorter feedback cycle

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.

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:

  • Failing constructor invocations due to exceptions thrown from within the constructor’s body (for example, failing Guard Clauses)
  • Whether disposable components are disposed of when they go out of scope
  • When classes that are supposed to be Singleton or Scoped are again (accidentally) created in a different part of the Composition Root, possibly with a different lifestyle12 

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 

12.3.3 The verdict: When to use a DI Container

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.

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.

12-06.eps

Figure 12.6 Pure DI can be valuable because it’s simple, although a DI Container can be either valuable or pointless, depending on how it’s used. When it’s used in a sufficiently sophisticated way (using Auto-Registration), we consider a DI Container to offer the best value/cost ratio.

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 

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).

Summary

  • A DI Container is a library that provides DI functionality. It’s an engine that resolves and manages object graphs.
  • DI in no way hinges on the use of a DI Container. A DI Container is a useful, but optional, tool.
  • Auto-Wiring is the ability to automatically compose an object graph from maps between Abstractions and concrete types by making use of the type information as supplied by the compiler and the Common Language Runtime (CLR).
  • Constructor Injection statically advertises the Dependency requirements of a class, and DI Containers use that information to Auto-Wire complex object graphs.
  • Auto-Wiring makes a Composition Root more resilient to change.
  • When you start using a DI Container, you’re not required to abandon hand wiring object graphs altogether. You can use hand wiring in parts of your configuration when this is more convenient.
  • When using a DI Container, the three configuration styles are configuration files, Configuration as Code, and Auto-Registration.
  • Configuration files are as much a part of your Composition Root as Configuration as Code and Auto-Registration. Using configuration files, therefore, doesn’t make your Composition Root smaller, it just moves it.
  • As your application grows in size and complexity, so will your configuration file. Configuration files tend to become brittle and opaque to errors, so only use this approach when you need late binding.
  • Don’t let the absence of support for handling configuration files influence your choice for picking a DI Container. Types can be loaded from configuration files in a few simple statements.
  • 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. This method is preferred over configuration files unless you need late binding.
  • Convention over Configuration is the application of conventions to your code to facilitate easier registration.
  • Auto-Registration is the ability to automatically register components in a container by scanning one or more assemblies for implementations of desired Abstractions, which is a form of Convention over Configuration.
  • Auto-Registration helps avoid constantly updating the Composition Root and is, therefore, preferred over Configuration as Code.
  • Using external libraries such as DI Containers incurs costs and risks; for example, the cost of learning a new API and the risk of the library being abandoned.
  • Avoid building your own DI Container. Either use one of the existing, well-tested, and freely available DI Containers, or practice Pure DI. Creating and maintaining such a library takes a lot of effort, which is effort not spent producing business value.
  • The big advantage of Pure DI is that it’s strongly typed. This allows the compiler to provide feedback about correctness, which is the fastest feedback that you can get.
  • You should use Pure DI for Composition Roots that are small and switch to Auto-Registration whenever maintaining such Composition Roots becomes a problem. Bigger applications with many classes that can be captured by several conventions can greatly benefit from using Auto-Registration.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.132.99