15
The Microsoft.Extensions.DependencyInjection DI Container

In this chapter

  • Working with Microsoft.Extensions.DependencyInjection’s registration API
  • Managing component lifetime
  • Configuring difficult APIs
  • Configuring sequences, Decorators, and Composites

With the introduction of ASP.NET Core, Microsoft introduced its own DI Container, Microsoft.Extensions.DependencyInjection, as part of the Core framework. In this chapter, we shorten that name to MS.DI.

Microsoft built MS.DI to simplify Dependency management for framework and third-party component developers working with ASP.NET Core. Microsoft’s intention was to define a DI Container with a minimal, lowest common denominator feature set that all other DI Containers could conform to.

In this chapter, we’ll give MS.DI the same treatment that we gave Autofac and Simple Injector. You’ll see to which degree MS.DI can be used to apply the principles and patterns laid forth in parts 1–3. Even though MS.DI is integrated in ASP.NET Core, it can also be used separately, which is why, in this chapter, we treat it as such.

During the course of this chapter, however, you’ll find that MS.DI is so limited in functionality that we deem it unsuited for development of any reasonably sized application that practices loose coupling and follows the principles and patterns described in this book. If MS.DI isn’t suited, then why use an entire chapter covering it in this book? The most important reason is that MS.DI looks at a first glance so much like the other DI Containers that you need to spend some time with it to understand the differences between it and mature DI Containers. Because it’s part of .NET Core, it may be tempting to use this built-in container if you don’t understand its limitations. The purpose of this chapter is to reveal these limitations so you can make an informed decision.

This chapter is divided into four sections. You can read each section independently, though the first section is a prerequisite for the other sections, and the fourth section relies on some methods and classes introduced in the third section. You can read the chapter in isolation from the rest of part 4, specifically to learn about MS.DI, or you can read it together with the other chapters to compare DI Containers. The focus of this chapter is to show how MS.DI relates to and implements the patterns and principles described in parts 1–3.

15.1 Introducing Microsoft.Extensions.DependencyInjection

In this section, you’ll learn where to get MS.DI, what you get, and how you start using it. We’ll also look at common configuration options. Table 15.1 provides fundamental information that you’re likely to need to get started.

Table 15.1 Microsoft.Extensions.DependencyInjection at a glance
QuestionAnswer
Where do I get it?It’s automatically included if you create a new ASP.NET Core application, but you can also manually add it to other application types. From Visual Studio, you can get it via NuGet. The package name is Microsoft.Extensions.DependencyInjection.
Which platforms are supported?.NET Standard 2.0 (.NET Core 2.0, .NET Framework 4.6.1, Mono 5.4, Xamarin.iOS 10.14, Xamarin.Android 8.0, UWP 10.0.16299).
How much does it cost?Nothing. It’s open source.
How is it licensed?Apache License, Version 2.0
Where can I get help?Because this is an official Microsoft .NET product, there’s guaranteed commercial support at https://www.microsoft.com/net/support/policy. For noncommercial — unguaranteed — support, you’re likely to get help by asking on Stack Overflow at https://stackoverflow.com/.
On which version is this chapter based?2.1.0

At a high level, using MS.DI isn’t that different from Autofac (discussed in chapter 13). Its usage is a two-step process, as figure 15.1 illustrates. Compared to Simple Injector, however, with MS.DI this two-step process is explicit: first, you configure a ServiceCollection, and when you’re done with that, you use it to build a ServiceProvider that can be used to resolve components.

15-01.eps

Figure 15.1 The pattern for using Microsoft.Extensions.DependencyInjection is to first configure it and then resolve components.

When you’re done with this section, you should have a good feeling for the overall usage pattern of MS.DI, and you should be able to start using it in well-behaved scenarios — where all components follow proper DI patterns, such as Constructor Injection. Let’s start with the simplest scenario and see how you can resolve objects using an MS.DI container.

15.1.1 Resolving objects

The core service of any DI Container is to compose object graphs. In this section, we’ll look at the API that enables you to compose object graphs with MS.DI. MS.DI requires you to register all relevant components before you can resolve them. The following listing shows one of the simplest possible uses of MS.DI.

Listing 15.1 Simplest possible use of MS.DI

var services = new ServiceCollection();

services.AddTransient<SauceBéarnaise>();

ServiceProvider container =
    services.BuildServiceProvider(validateScopes: true);

IServiceScope scope = container.CreateScope();

SauceBéarnaise sauce =
    scope.ServiceProvider.GetRequiredService<SauceBéarnaise>();

As was already implied by figure 15.1, you need a ServiceCollection instance to configure components. MS.DI’s ServiceCollection is the equivalent of Autofac’s ContainerBuilder.

Here, you register the concrete SauceBéarnaise class with services, so that when you ask it to build a container, the resulting container is configured with the SauceBéarnaise class. This again enables you to resolve the SauceBéarnaise class from the container. If you don’t register the SauceBéarnaise component, the attempt to resolve it throws a InvalidOperationException with the following message:

No service for type 'Ploeh.Samples.MenuModel.SauceBéarnaise' has been registered.

As listing 15.1 shows, with MS.DI, you never resolve from the root container itself but from an IServiceScope. Section 15.2.1 goes into more detail about what an IServiceScope is.

As a safety measure, always build the ServiceProvider using the BuildServiceProvider overload with the validateScopes argument set to true, as shown in listing 15.1. This prevents the accidental resolution of Scoped instances from the root container. With the introduction of ASP.NET Core 2.0, validateScopes is automatically set to true by the framework when the application is running in the development environment, but it’s best to enable validation even outside the development environment as well. This means you’ll have to call BuildServiceProvider(true) manually.

Not only can MS.DI resolve concrete types with parameterless constructors, it can also Auto-Wire a type with other Dependencies. All these Dependencies need to be registered. For the most part, you want to program to interfaces, because this introduces loose coupling. To support this, MS.DI lets you map Abstractions to concrete types.

Mapping Abstractions to concrete types

Whereas our application’s root types will typically be resolved by their concrete types as listing 15.1 showed, loose coupling requires you to map Abstractions to concrete types. Creating instances based on such maps is the core service offered by any DI Container, but you must still define the map. In this example, you map the IIngredient interface to the concrete SauceBéarnaise class, which allows you to successfully resolve IIngredient:

var services = new ServiceCollection();

services.AddTransient<IIngredient, SauceBéarnaise>();  ①  

var container = services.BuildServiceProvider(true);

IServiceScope scope = container.CreateScope();

IIngredient sauce = scope.ServiceProvider
    .GetRequiredService<IIngredient>();    ②  

Here, the AddTransient method allows a concrete type to be mapped to a particular Abstraction using the Transient Lifestyle. Because of the previous AddTransient call, SauceBéarnaise can now be resolved as IIngredient.

In many cases, the generic API is all you need. Still, there are situations where you’ll need a more weakly typed way to resolve services. This is also possible.

Resolving weakly typed services

Sometimes you can’t use a generic API because you don’t know the appropriate type at design time. All you have is a Type instance, but you’d still like to get an instance of that type. You saw an example of that in section 7.3, where we discussed ASP.NET Core MVC’s IControllerActivator class. The relevant method is this one:

object Create(ControllerContext context);

As shown previously in listing 7.8, the ControllerContext captures the controller’s Type, which you can extract using the ControllerTypeInfo property of the ActionDescriptor property:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();

Because you only have a Type instance, you can’t use generics, but must resort to a weakly typed API. MS.DI offers a weakly typed overload of the GetRequiredService method that lets you implement the Create method:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return scope.ServiceProvider.GetRequiredService(controllerType);

The weakly typed overload of GetRequiredService lets you pass the controllerType variable directly to MS.DI. Typically, this means you have to cast the returned value to some Abstraction, because the weakly typed GetRequiredService method returns object. In the case of IControllerActivator, however, this isn’t required, because ASP.NET Core MVC doesn’t require controllers to implement any interface or base class.

No matter which overload of GetRequiredService you use, MS.DI guarantees that it’ll return an instance of the requested type or throw an exception if there are Dependencies that can’t be satisfied. When all required Dependencies have been properly configured, MS.DI can Auto-Wire the requested type.

To be able to resolve the requested type, all loosely coupled Dependencies must have been previously configured. Let’s investigate the ways that you can configure MS.DI.

15.1.2 Configuring the ServiceCollection

As we discussed in section 12.2, you can configure a DI Container in several conceptually different ways. Figure 12.5 reviewed the options: configuration files, Configuration as Code, and Auto-Registration. Figure 15.2 shows these options again.

15-02.eps

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

Although there’s no Auto-Registration API, to some extent, you can implement assembly scanning with the help of .NET’s LINQ and reflection APIs. Before we discuss this, we’ll start with a discussion of MS.DI’s Configuration as Code API.

Configuring the ServiceCollection using Configuration as Code

In section 15.1.1, you saw a brief glimpse of MS.DI’s strongly typed configuration API. Here, we’ll examine it in greater detail.

All configuration in MS.DI uses the API exposed by the ServiceCollection class, although most of the methods are extension methods. One of the most commonly used methods is the AddTransient method that you’ve already seen:

services.AddTransient<IIngredient, SauceBéarnaise>();

Registering SauceBéarnaise as IIngredient hides the concrete class so that you can no longer resolve SauceBéarnaise with this registration. But you can fix this by replacing the registration with the following:

services.AddTransient<SauceBéarnaise>();
services.AddTransient<IIngredient>(
    c => c.GetRequiredService<SauceBéarnaise>());  ①  

Instead of making the registration for IIngredient using the Auto-Wiring overload of AddTransient, you register a code block that, when called, forwards the call to the registration of the concrete SauceBéarnaise.

In real applications, you always have more than one Abstraction to map, so you must configure multiple mappings. This is done with multiple calls to one of the Add... methods:

services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<ICourse, Course>();

This maps IIngredient to SauceBéarnaise, and ICourse to Course. There’s no overlap of types, so it should be pretty evident what’s going on. But you can also register the same Abstraction several times:

services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<IIngredient, Steak>();

Here, you register IIngredient twice. If you resolve IIngredient, you get an instance of Steak. The last registration wins, but previous registrations aren’t forgotten. MS.DI can handle multiple configurations for the same Abstraction, but we’ll get back to this topic in section 15.4.

Although there are more-advanced options available for configuring MS.DI, you can configure an entire application with the methods shown here. But to save yourself from too much explicit maintenance of container configuration, you could instead consider a more convention-based approach using Auto-Registration.

Configuring ServiceCollection using Auto-Registration

In many cases, registrations will be similar. Such registrations are tedious to maintain, and explicitly registering each and every component might not be the most productive approach, as we discussed in section 12.3.3.

Consider a library that contains many IIngredient implementations. You can configure each class individually, but it’ll result in an ever-changing list of Type instances supplied to the Add... methods. What’s worse is that every time you add a new IIngredient implementation, you must also explicitly register it with the container if you want it to be available. It would be more productive to state that all implementations of IIngredient found in a given assembly should be registered.

As stated previously, MS.DI contains no Auto-Registration API. This means you have to do it yourself. This is possible to some degree, and in this section, we’ll show how with a simple example but delay more detailed discussions of the possibilities and limitations until section 15.4. Let’s take a look how you can register a sequence of IIngredient registrations:

Assembly ingredientsAssembly = typeof(Steak).Assembly;

var ingredientTypes =
    from type in ingredientsAssembly.GetTypes()    ①  
    where !type.IsAbstract    ①  
    where typeof(IIngredient).IsAssignableFrom(type)    ①  
    select type;    ①  

foreach (var type in ingredientTypes)
{
    services.AddTransient(typeof(IIngredient), type);  ②  
}

The previous example unconditionally configures all implementations of the IIngredient interface, but you can provide filters that enable you to select only a subset. Here’s a convention-based scan where you add only classes whose name starts with Sauce:

Assembly ingredientsAssembly = typeof(Steak).Assembly;

var ingredientTypes =
    from type in ingredientsAssembly.GetTypes()
    where !type.IsAbstract
    where typeof(IIngredient).IsAssignableFrom(type)
    where type.Name.StartsWith("Sauce")    ①  
    select type;

foreach (var type in ingredientTypes)
{
    services.AddTransient(typeof(IIngredient), type);
}

Apart from selecting the correct types from an assembly, another part of Auto-Registration is defining the correct mapping. In the previous examples, you used the AddTransient method with a specific interface to register all selected types against that interface.

But sometimes you’ll want to use different conventions. Let’s say that instead of interfaces, you use abstract base classes, and you want to register all types in an assembly where the name ends with Policy by their base type:

Assembly policiesAssembly = typeof(DiscountPolicy).Assembly;

var policyTypes =
    from type in policiesAssembly.GetTypes()    ①  
    where type.Name.EndsWith("Policy")    ②  
    select type;

foreach (var type in policyTypes)
{
    services.AddTransient(type.BaseType, type);    ③  
}

Even though MS.DI contains no convention-based API, by making use of existing .NET framework APIs, convention-based registrations are possible. This becomes a different ball game when it comes to generics, as we’ll discuss next.

Auto-Registration of generic Abstractions

During the course of chapter 10, you refactored the big, obnoxious IProductService interface to the ICommandService<TCommand> interface of listing 10.12. Here’s that Abstraction again:

public interface ICommandService<TCommand>
{
    void Execute(TCommand command);
}

As discussed in chapter 10, every command Parameter Object represents a use case, and there’ll be a single implementation per use case. The AdjustInventoryService of listing 10.8 was given as an example. It implemented the “adjust inventory” use case. The following listing shows this class again.

Listing 15.2 The AdjustInventoryService from chapter 10

public class AdjustInventoryService : ICommandService<AdjustInventory>
{
    private readonly IInventoryRepository repository;

    public AdjustInventoryService(IInventoryRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(AdjustInventory command)
    {
        var productId = command.ProductId;

        ...
    }
}

Any reasonably complex system will easily implement hundreds of use cases, and this is an ideal candidate for using Auto-Registration. But because of the lack of Auto-Registration support by MS.DI, you’ll have to write a fair amount of code to get this running. The next listing provides an example of this.

Listing 15.3 Auto-Registration of ICommandService<TCommand> implementations

Assembly assembly = typeof(AdjustInventoryService).Assembly;

var mappings =
    from type in assembly.GetTypes()
    where !type.IsAbstract    ①  
    where !type.IsGenericType    ②  
    from i in type.GetInterfaces()    ③  
    where i.IsGenericType    ③  
    where i.GetGenericTypeDefinition()    ③  
        == typeof(ICommandService<>)    ③  
    select new { service = i, type };    ③  

foreach (var mapping in mappings)
{
    services.AddTransient(    ④  
        mapping.service,    ④  
        mapping.type);    ④  
}

As in the previous listings, you make full use of .NET’s LINQ and Reflection APIs to allow selecting classes from the supplied assembly. Using the supplied open-generic interface, you iterate through the list of assembly types, and register all types that implement a closed-generic version of ICommandService<TCommand>. What this means, for instance, is that AdjustInventoryService is registered because it implements ICommandService<AdjustInventory>, which is a closed-generic version of ICommandService<TCommand>.

This section introduced the MS.DI DI Container and demonstrated these fundamental mechanics: how to configure a ServiceCollection, and, subsequently, how to use the constructed ServiceProvider to resolve services. Resolving services is done with a single call to the GetRequiredService method, so the complexity involves configuring the container. The API primarily supports Configuration as Code, although to some extend Auto-Registration can be built on top of it. As you’ll see later, however, the lack of support for Auto-Registration will lead to quite complex and hard-to-maintain code. Until now, we’ve only looked at the most basic API, but there’s another area we have yet to cover — how to manage component lifetime.

15.2 Managing lifetime

In chapter 8, we discussed Lifetime Management, including the most common conceptual lifetime styles such as Transient, Singleton, and Scoped. MS.DI supports these three Lifestyles and lets you configure the lifetime of all services. The Lifestyles shown in table 15.2 are available as part of the API.

Table 15.2 Microsoft.Extensions.DependencyInjection Lifestyles
Microsoft namePattern nameComments
TransientTransientInstances are tracked by the container and disposed of.
SingletonSingletonInstances are disposed of when the container is disposed of.
ScopedScopedInstances are reused within the same IServiceScope. Instances are tracked for the lifetime of the scope and are disposed of when the scope is disposed of.

MS.DI’s implementation of Transient and Singleton are equivalent to the general Lifestyles described in chapter 8, so we won’t spend much time on them in this chapter. Instead, in this section, you’ll see how you can define Lifestyles for components in code. By the end of this section, you should be able to use MS.DI’s Lifestyles in your own application. Let’s start by reviewing how to configure instance scopes for components.

15.2.1 Configuring Lifestyles

In this section, we’ll review how to manage Lifestyles with MS.DI. A Lifestyle is configured as part of registering components. It’s as easy as this:

services.AddSingleton<SauceBéarnaise>();

This configures the concrete SauceBéarnaise class as a Singleton so that the same instance is returned each time SauceBéarnaise is requested. If you want to map an Abstraction to a concrete class with a specific Lifestyle, you can use the AddSingleton overload with two generic arguments:

services.AddSingleton<IIngredient, SauceBéarnaise>();

Compared to other DI Containers, there aren’t many options in MS.DI when it comes to configuring Lifestyles for components. It’s done in a rather declarative fashion. Although configuration is typically easy, you mustn’t forget that some Lifestyles involve long-lived objects that use resources as long as they’re around.

15.2.2 Releasing components

As discussed in section 8.2.2, it’s important to release objects when you’re done with them. Similar to Autofac and Simple Injector, MS.DI has no explicit Release method, but instead uses a concept called scopes. A scope can be regarded as a request-specific cache. As figure 15.3 illustrates, it defines a boundary where components can be reused.

An IServiceScope defines a cache that you can use for a particular duration or purpose; the most obvious example is a web request. When a Scoped component is requested from an IServiceScope, you always receive the same instance. The difference from true Singletons is that if you query a second scope, you’ll get another instance.

15-03.eps

Figure 15.3 Microsoft.Extensions.DependencyInjection’s scopes act as containers that can share components for a limited duration or purpose.

One of the important features of scopes is that they let you properly release components when the scope completes. You create a new scope with the CreateScope method of a particular IServiceProvider implementation, and release all appropriate components by invoking its Dispose method:

using (IServiceScope scope = container.CreateScope())  ①  
{
    IMeal meal = scope.ServiceProvider
        .GetRequiredService<IMeal>();    ②  

    meal.Consume();    ③  

}    ④  

A new scope is created from the container by invoking the CreateScope method. The return value implements IDisposable, so you can wrap it in a using block. Because IServiceScope contains a ServiceProvider property that implements the same interface that the container itself implements, you can use the scope to resolve components in exactly the same way as with the container itself.

When you’re done with the scope, you can dispose of it. With a using block, this happens automatically when you exit that block, but you can also choose to explicitly dispose of it by invoking the Dispose method. When you dispose of the scope, you also release all the components that were created by the scope; here, it means that you release the meal object graph.

Note that Dependencies of a component are always resolved at or below the component’s scope. For example, if you need a Transient Dependency injected into a Singleton, that Transient Dependency will come from the root container, even if you’re resolving the Singleton from a nested scope. This tracks the Transient within the root container and prevents it from being disposed of when the scope gets disposed of. The Singleton consumer would otherwise break, because it’s kept alive in the root container while depending on a component that was disposed of.

Earlier in this section, you saw how to configure components as Singletons or Transients. Configuring a component to have a Scoped Lifestyle is done in a similar way:

services.AddScoped<IIngredient, SauceBéarnaise>();

Similar to the AddTransient and AddSingleton methods, you can use the AddScoped method to state that the component’s lifetime should follow the scope that created the instance.

Due to their nature, Singletons are never released for the lifetime of the container itself. Still, you can release even those components if you don’t need the container any longer. This is done by disposing of the container itself:

container.Dispose();

In practice, this isn’t nearly as important as disposing of a scope, because the lifetime of a container tends to correlate closely with the lifetime of the application it supports. You normally keep the container around as long as the application runs, so you’d only dispose of it when the application shuts down. In this case, memory would be reclaimed by the operating system.

This completes our tour of Lifetime Management with MS.DI. Components can be configured with mixed Lifestyles, and this is true even when you register multiple implementations of the same Abstraction. Until now, you’ve allowed the container to wire Dependencies by implicitly assuming that all components use Constructor Injection. But this isn’t always the case. In the next section, we’ll review how to deal with classes that must be instantiated in special ways.

15.3 Registering difficult APIs

Until now, we’ve considered how you can configure components that use Constructor Injection. One of the many benefits of Constructor Injection is that DI Containers such as MS.DI can easily understand how to compose and create all classes in a Dependency graph. This becomes less clear when APIs are less well behaved.

In this section, you’ll see how to deal with primitive constructor arguments and static factories. These all require your special attention. Let’s start by looking at classes that take primitive types, such as strings or integers, as constructor arguments.

15.3.1 Configuring primitive Dependencies

As long as you inject Abstractions into consumers, all is well. But it becomes more difficult when a constructor depends on a primitive type, such as a string, a number, or an enum. This is particularly the case for data access implementations that take a connection string as constructor parameter, but it’s a more general issue that applies to all strings and numbers.

Conceptually, it doesn’t always make sense to register a string or number as a component in a container. Using generic type constraints, MS.DI even blocks the registration of value types like numbers and enums from its generic API. With the non-generic API, on the other hand, this is still possible. Consider as an example this constructor:

public ChiliConCarne(Spiciness spiciness)

In this example, Spiciness is an enum:

public enum Spiciness { Mild, Medium, Hot }

If you want all consumers of Spiciness to use the same value, you can register Spiciness and ChiliConCarne independently of each other:

services.AddSingleton(    ①  
    typeof(Spiciness), Spiciness.Medium);    ①  

services.AddTransient<ICourse, ChiliConCarne>();  ②  

When you subsequently resolve ChiliConCarne, it’ll have a Medium Spiciness, as will all other components with a Dependency on Spiciness. If you’d rather control the relationship between ChiliConCarne and Spiciness on a finer level, you can use a code block, which is something we get back to in a moment in section 15.3.3.

The option described here uses Auto-Wiring to provide a concrete value to a component. A more convenient solution, however, is to extract the primitive Dependencies into Parameter Objects.

15.3.2 Extracting primitive Dependencies to Parameter Objects

In section 10.3.3, we discussed how the introduction of Parameter Objects allowed mitigating the Open/Closed Principle violation that IProductService caused. Parameter Objects, however, are also a great tool to mitigate ambiguity. For example, the Spiciness of a course could be described in more general terms as a flavoring. Flavoring might include other properties, such as saltiness, so you can wrap Spiciness and the saltiness in a Flavoring class:

public class Flavoring
{
    public readonly Spiciness Spiciness;
    public readonly bool ExtraSalty;

    public Flavoring(Spiciness spiciness, bool extraSalty)
    {
        this.Spiciness = spiciness;
        this.ExtraSalty = extraSalty;
    }
}

As we mentioned in section 10.3.3, it’s perfectly fine for Parameter Objects to have one parameter. The goal is to remove ambiguity, and not just on the technical level. Such a Parameter Object’s name might do a better job describing what your code does on a functional level, as the Flavoring class so elegantly does. With the introduction of the Flavoring Parameter Object, it now becomes possible to Auto-Wire any ICourse implementation that requires some flavoring without introducing ambiguity:

var flavoring = new Flavoring(Spiciness.Medium, extraSalty: true);
services.AddSingleton<Flavoring>(flavoring);

container.AddTransient<ICourse, ChiliConCarne>();

This code creates a single instance of the Flavoring class. Flavoring becomes a configuration object for courses. Because there’ll only be one Flavoring instance, you can register it in MS.DI using the AddSingleton<T> overload that accepts a precreated instance.

Extracting primitive Dependencies into Parameter Objects should be your preference over the previously discussed option, because Parameter Objects remove ambiguity, at both the functional and technical levels. It does, however, require a change to a component’s constructor, which might not always be feasible. In this case, registering a delegate is your second-best pick.

15.3.3 Registering objects with code blocks

Another option for creating a component with a primitive value is to use one of the Add... methods, which let you supply a delegate that creates the component:

services.AddTransient<ICourse>(c => new ChiliConCarne(Spiciness.Hot));

You already saw this AddTransient method overload previously, when we discussed torn Lifestyles in section 15.1.2. The ChiliConCarne constructor is invoked with a hot Spiciness every time the ICourse service is resolved. The following example shows the definition of this AddTransient<TService> extension method:

public static IServiceCollection AddTransient<TService>(
    this IServiceCollection services,
    Func<IServiceProvider, TService> implementationFactory)
    where TService : class;

As you can see, this AddTransient method accepts a parameter of type Func<IServiceProvider, TService>. With respect to the previous registration, when an ICourse is resolved, MS.DI will call the supplied delegate and supply it with the IServiceProvider belonging to the current IServiceScope. With it, your code block can resolve instances that originate from the same IServiceScope. We’ll demonstrate this in the next section.

When it comes to the ChiliConCarne class, you have a choice between Auto-Wiring or using a code block. But other classes are more restrictive: they can’t be instantiated through a public constructor. Instead, you must use some sort of factory to create instances of the type. This is always troublesome for DI Containers, because, by default, they look after public constructors. Consider this example constructor for the public JunkFood class:

internal JunkFood(string name)

Even though the JunkFood class might be public, the constructor is internal. In this example, instances of JunkFood should instead be created through the static JunkFoodFactory class:

public static class JunkFoodFactory
{
    public static JunkFood Create(string name)
    {
        return new JunkFood(name);
    }
}

From MS.DI’s perspective, this is a problematic API, because there are no unambiguous and well-established conventions around static factories. It needs help — and you can give that help by providing a code block it can execute to create the instance:

services.AddTransient<IMeal>(c => JunkFoodFactory.Create("chicken meal"));

This time, you use the AddTransient method to create the component by invoking a static factory within the code block. JunkFoodFactory.Create will be invoked every time IMeal is resolved, and the result will be returned.

If you have to write the code to create the instance, how is this in any way better than invoking the code directly? By using a code block inside a AddTransient method call, you still gain something:

  • You map from IMeal to JunkFood. This allows consuming classes to stay loosely coupled.
  • Lifestyles can still be configured. Although the code block will be invoked to create the instance, it may not be invoked every time the instance is requested. It is by default, but if you change it to a Singleton, the code block will only be invoked once, and the result cached and reused thereafter.

In this section, you’ve seen how you can use MS.DI to deal with more-difficult creational APIs. Up until this point, the code examples have been fairly straightforward. This will quickly change when you start to work with multiple components, so let’s now turn our attention in that direction.

15.4 Working with multiple components

As alluded to in section 12.1.2, DI Containers thrive on distinctness but have a hard time with ambiguity. When using Constructor Injection, a single constructor is preferred over overloaded constructors, because it’s evident which constructor to use when there’s no choice. This is also the case when mapping from Abstractions to concrete types. If you attempt to map multiple concrete types to the same Abstraction, you introduce ambiguity.

Despite the undesirable qualities of ambiguity, you often need to work with multiple implementations of a single Abstraction. This can be the case in these situations:

  • Different concrete types are used for different consumers.
  • Dependencies are sequences.
  • Decorators or Composites are in use.

In this section, we’ll look at each of these cases and see how you can address each with MS.DI. When we’re done, you should have a good feel for what you can do with MS.DI and where the boundaries lie when multiple implementations of the same Abstraction are in play. Let’s first see how you can provide more fine-grained control than Auto-Wiring provides.

15.4.1 Selecting among multiple candidates

Auto-Wiring is convenient and powerful but provides little control. As long as all Abstractions are distinctly mapped to concrete types, you have no problems. But as soon as you introduce more implementations of the same interface, ambiguity rears its ugly head. Let’s first recap how MS.DI deals with multiple registrations of the same Abstraction.

Configuring multiple implementations of the same service

As you saw in section 15.1.2, you can register multiple implementations of the same interface:

services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<IIngredient, Steak>();

This example registers both the Steak and SauceBéarnaise classes as the IIngredient service. The last registration wins, so if you resolve IIngredient with GetRequired—Service<IIngredient>(), you’ll get a Steak instance.

You can also ask the container to resolve all IIngredient components. MS.DI has a dedicated method to do that, called GetServices. Here’s an example:

IEnumerable<IIngredient> ingredients =
    scope.ServiceProvider.GetServices<IIngredient>();  ①  

Under the hood, GetServices delegates to GetRequiredService, while requesting an IEnumerable<IIngredient>>. You can also ask the container to resolve all IIngredient components using GetRequiredService instead:

IEnumerable<IIngredient> ingredients = scope.ServiceProvider
    .GetRequiredService<IEnumerable<IIngredient>>();

Notice that you use the normal GetRequiredService method, but that you request IEnumerable<IIngredient>. The container interprets this as a convention and gives you all the IIngredient components it has.

When there are multiple implementations of a certain Abstraction, there’ll often be a consumer that depends on a sequence. Sometimes, however, components need to work with a fixed set or a subset of Dependencies of the same Abstraction, which is what we’ll discuss next.

Removing ambiguity using code blocks

As useful as Auto-Wiring is, sometimes you need to override the normal behavior to provide fine-grained control over which Dependencies go where, but it may also be that you need to address an ambiguous API. As an example, consider this constructor:

public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)

In this case, you have three identically typed Dependencies, each of which represents a different concept. In most cases, you want to map each of the Dependencies to a separate type.

As stated previously, when compared to both Autofac and Simple Injector, MS.DI is limited in functionality. Where Autofac provides keyed registrations, and Simple Injector provides conditional registrations to deal with this kind of ambiguity, MS.DI falls short in this respect. There isn’t any built-in functionality to do this. To wire up such an ambiguous API with MS.DI, you have to revert to using a code block.

Listing 15.4 Wiring ThreeCourseMeal by resolving courses in a code block

services.AddTransient<IMeal>(c => new ThreeCourseMeal(  ①  
    entrée: c.GetRequiredService<Rillettes>(),    ②  
    mainCourse: c.GetRequiredService<CordonBleu>(),    ②  
    dessert: c.GetRequiredService<CrèmeBrûlée>()));    ②  

This registration reverts from Auto-Wiring and constructs the ThreeCourseMeal using a delegate instead. Fortunately, the three ICourse implementations themselves are still Auto-Wired. To bring Auto-Wiring back for the ThreeCourseMeal, you make use of MS.DI’s ActivatorUtilities class.

Removing ambiguity using ActivatorUtilities

The lack of Auto-Wiring of ThreeCourseMeal isn’t that problematic in this example because, in this case, you override all constructor arguments. This could be different if ThreeCourseMeal contained more Dependencies:

public ThreeCourseMeal(
    ICourse entrée,
    ICourse mainCourse,
    ICourse dessert,
    ...    ①  
    )

MS.DI contains a utility class called ActivatorUtilities that allows Auto-Wiring a class’s Dependencies, while overriding other Dependencies by explicitly supplying their values. Using ActivatorUtilities, you can rewrite the previous registration.

Listing 15.5 Wiring ThreeCourseMeal using ActivatorUtilities

services.AddTransient<IMeal>(c =>
    ActivatorUtilities.CreateInstance<ThreeCourseMeal>(  ①  
        c,    ②  
        new object[]    ③  
        {    ③  
            c.GetRequiredService<Rillettes>(),    ③  
            c.GetRequiredService<CordonBleu>(),    ③  
            c.GetRequiredService<MousseAuChocolat>()    ③  
        }));

This example makes use of the ActivatorUtilities’s CreateInstance<T> method, defined as follows:

public static T CreateInstance<T>(
    IServiceProvider provider,
    params object[] parameters);

The CreateInstance<T> method creates a new instance of the supplied T. It goes through the supplied parameters array and matches each parameter to a compatible constructor parameter. Then it resolves the remaining, unmatched constructor parameters with the supplied IServiceProvider.

Because all three resolved courses implement ICourse, there’s still ambiguity in the call. CreateInstance<T> resolves this ambiguity by applying the supplied parameters from left to right. This means that because Rillettes is the first element in the parameters array, it’ll be applied to the first compatible parameter of the ThreeCourseMeal constructor. This is the entrée parameter of type ICourse.

When compared to listing 15.4, there’s a big downside to listing 15.5. Listing 15.4 is verified by the compiler. Any refactoring to the constructor would either allow that code to stay working or fail with a compile error.

The opposite is true with listing 15.5. If the three ICourse constructor parameters are rearranged, code will keep compiling, and ActivatorUtilities would even be able to construct a new ThreeCourseMeal. But unless listing 15.5 is changed according to that rearrangement, the courses are injected in an incorrect order, which will likely cause the application to behave incorrectly. Unfortunately, no refactoring tool will signal that the registration must be changed too.

Even the related registrations of Autofac and Simple Injector (listings 13.7 and 14.9) do a better job of preventing errors. Although neither listing is type-safe, because both listings match on exact parameter names, a change to the ThreeCourseMeal would at least cause an exception when the class is resolved. This is always better than failing silently, which is what could happen in the case of listing 15.5.

Overriding Auto-Wiring by explicitly mapping parameters to components is a universally applicable solution. Where you use named registrations with Autofac and conditional registrations with Simple Injector, with MS.DI, you override parameters by passing in manually resolved concrete types. This can be brittle if you have many types to manage. A better solution is to design your own API to get rid of that ambiguity. It often leads to a better overall design.

In the next section, you’ll see how to use the less ambiguous and more flexible approach where you allow any number of courses in a meal. To this end, you must learn how MS.DI deals with sequences.

15.4.2 Wiring sequences

In section 6.1.1, we discussed how Constructor Injection acts as a warning system for Single Responsibility Principle violations. The lesson then was that instead of viewing Constructor Over-injection as a weakness of the Constructor Injection pattern, you should rather rejoice that it makes problematic design so obvious.

When it comes to DI Containers and ambiguity, we see a similar relationship. DI Containers generally don’t deal with ambiguity in a graceful manner. Although you can make a DI Container deal with it, it can seem awkward. This is often an indication that you could improve the design of your code.

In this section, we’ll look at an example that demonstrates how you can refactor away from ambiguity. It’ll also show how MS.DI deals with sequences.

Refactoring to a better course by removing ambiguity

In section 15.4.1, you saw how the ThreeCourseMeal and its inherent ambiguity forced you to either abandon Auto-Wiring or make use of the rather verbose call to ActivatorUtilities. A simple generalization moves toward an implementation of IMeal that takes an arbitrary number of ICourse instances instead of exactly three, as was the case with the ThreeCourseMeal class:

public Meal(IEnumerable<ICourse> courses)

Notice that, instead of requiring three distinct ICourse instances in the constructor, the single dependency on an IEnumerable<ICourse> instance lets you provide any number of courses to the Meal class — from zero to ... a lot! This solves the issue with ambiguity, because there’s now only a single Dependency. In addition, it also improves the API and implementation by providing a single, general-purpose class that can model different types of meal: from a simple meal with a single course to an elaborate 12-course dinner.

In this section, we’ll look at how you can configure MS.DI to wire up Meal instances with appropriate ICourse Dependencies. When we’re done, you should have a good idea of the options available when you need to configure instances with sequences of Dependencies.

Auto-Wiring sequences

MS.DI understands sequences, so if you want to use all registered components of a given service, Auto-Wiring just works. As an example, you can configure the IMeal service and its courses like this:

services.AddTransient<ICourse, Rillettes>();
services.AddTransient<ICourse, CordonBleu>();
services.AddTransient<ICourse, MousseAuChocolat>();

services.AddTransient<IMeal, Meal>();

Notice that this is a completely standard mapping from Abstractions to concrete types. MS.DI automatically understands the Meal constructor and determines that the correct course of action is to resolve all ICourse components. When you resolve IMeal, you get a Meal instance with the ICourse components Rillettes, CordonBleu, and MousseAuChocolat.

MS.DI automatically handles sequences, and unless you specify otherwise, it does what you’d expect it to do: it resolves a sequence of Dependencies to all registered components of that type. Only when you need to explicitly pick only some components from a larger set do you need to do more. Let’s see how you can do that.

Picking only some components from a larger set

MS.DI’s default strategy of injecting all components is often the correct policy, but as figure 15.4 shows, there may be cases where you want to pick only some registered components from the larger set of all registered components.

15-04.eps

Figure 15.4 Picking components from a larger set of all registered components

When you previously let MS.DI Auto-Wire all configured instances, it corresponded to the situation depicted on the right side of the figure. If you want to register a component as shown on the left side, you must explicitly define which components should be used. In order to achieve this, you can use the AddTransient method that accepts a delegate. This time around, you’re dealing with the Meal constructor, which only takes a single parameter.

Listing 15.6 Injecting an ICourse subset into Meal

services.AddScoped<Rillettes>();    ①  
services.AddTransient<LobsterBisque>();    ①  
services.AddScoped<CordonBleu>();    ①  
services.AddScoped<OssoBuco>();    ①  
services.AddSingleton<MousseAuChocolat>();    ①  
services.AddTransient<CrèmeBrûlée>();    ①  

services.AddTransient<ICourse>(    ②  
    c => c.GetRequiredService<Rillettes>());    ②  
services.AddTransient<ICourse(    ②  
    c => c.GetRequiredService<LobsterBisque>());    ②  
services.AddTransient<ICourse>(    ②  
    c => c.GetRequiredService<CordonBleu>());    ②  
services.AddTransient<ICourse(    ②  
    c => c.GetRequiredService<OssoBuco>());    ②  
services.AddTransient<ICourse>(    ②  
    c => c.GetRequiredService<MousseAuChocolat>());  ②  
services.AddTransient<ICourse(    ②  
    c => c.GetRequiredService<CrèmeBrûlée>());    ②  

services.AddTransient<IMeal>(c = new Meal(
    new ICourse[]    ③  
    {    ③  
        c.GetRequiredService<Rillettes>(),    ③  
        c.GetRequiredService<CordonBleu>(),    ③  
        c.GetRequiredService<MousseAuChocolat>()    ③  
    }));    ③  

MS.DI natively understands sequences; unless you need to explicitly pick only some components from all services of a given type, MS.DI automatically does the right thing. Auto-Wiring works not only with single instances, but also for sequences, and the container maps a sequence to all configured instances of the corresponding type. A perhaps less intuitive use of having multiple instances of the same Abstraction is the Decorators design pattern, which we’ll discuss next.

15.4.3 Wiring Decorators

In section 9.1.1, we discussed how the Decorator design pattern is useful when implementing Cross-Cutting Concerns. By definition, Decorators introduce multiple types of the same Abstraction. At the very least, you have two implementations of an Abstraction: the Decorator itself and the decorated type. If you stack the Decorators, you can have even more. This is another example of having multiple registrations of the same service. Unlike the previous sections, these registrations aren’t conceptually equal, but rather Dependencies of each other.

Decorating non-generic Abstractions

MS.DI has no built-in support for Decorators, and this is one of the areas where the limitations of MS.DI can hinder productivity. Nonetheless, we’ll show how you can, to some degree, work around these limitations.

You can hack around this omission by, again, making use of the ActivatorUtilities class. The following example shows how to use this class to apply Breading to VealCutlet:

services.AddTransient<IIngredient>(c =>    ①  
    ActivatorUtilities.CreateInstance<Breading>(  ①  
        c,
        ActivatorUtilities    ②  
            .CreateInstance<VealCutlet>(c)));    ②  

As you learned in chapter 9, you get veal cordon bleu when you slit open a pocket in the veal cutlet and add ham, cheese, and garlic into the pocket before breading the cutlet. The following example shows how to add a HamCheeseGarlic Decorator in between VealCutlet and the Breading Decorator:

services.AddTransient<IIngredient>(c =>
    ActivatorUtilities.CreateInstance<Breading>(
        c,
        ActivatorUtilities
            .CreateInstance<HamCheeseGarlic>(    ①  
            c,
            ActivatorUtilities
                .CreateInstance<VealCutlet>(c))));

By making HamCheeseGarlic become a Dependency of Breading, and VealCutlet a Dependency of HamCheeseGarlic, the HamCheeseGarlic Decorator becomes the middle class in the object graph. This results in an object graph equal to the following Pure DI version:

new Breading(    ①  
    new HamCheeseGarlic(    ①  
        new VealCutlet()));    ①  

As you might guess, chaining Decorators with MS.DI is cumbersome and verbose. Let’s add insult to injury by taking a look at what happens if you try to apply Decorators to generic Abstractions.

Decorating generic Abstractions

During the course of chapter 10, we defined multiple generic Decorators that could be applied to any ICommandService<TCommand> implementation. In the remainder of this chapter, we’ll set our ingredients and courses aside, and we’ll take a look at how to register these generic Decorators using MS.DI. The following listing demonstrates how to register all ICommandService<TCommand> implementations with the three Decorators presented in section 10.3.

Listing 15.7 Decorating generic Auto-Registered Abstractions

Assembly assembly = typeof(AdjustInventoryService).Assembly;

var mappings =
    from type in assembly.GetTypes()    ①  
    where !type.IsAbstract    ①  
    where !type.IsGenericType    ①  
    from i in type.GetInterfaces()    ①  
    where i.IsGenericType    ①  
    where i.GetGenericTypeDefinition()    ①  
        == typeof(ICommandService<>)    ①  
    select new { service = i, implementation = type };  ①  

foreach (var mapping in mappings)
{
    Type commandType =    ②  
        mapping.service.GetGenericArguments()[0];    ②  

    Type secureDecoratoryType =    ③  
        typeof(SecureCommandServiceDecorator<>)    ③  
            .MakeGenericType(commandType);    ③  
    Type transactionDecoratorType =    ③  
        typeof(TransactionCommandServiceDecorator<>)    ③  
            .MakeGenericType(commandType);    ③  
    Type auditingDecoratorType =    ③  
        typeof(AuditingCommandServiceDecorator<>)    ③  
            .MakeGenericType(commandType);    ③  

    services.AddTransient(mapping.service, c =>    ④  
        ActivatorUtilities.CreateInstance(    ④  
            c,    ④  
            secureDecoratoryType,    ④  
            ActivatorUtilities.CreateInstance(    ④  
                c,    ④  
                transactionDecoratorType,    ④  
                ActivatorUtilities.CreateInstance(    ④  
                    c,    ④  
                    auditingDecoratorType,    ④  
                    ActivatorUtilities.CreateInstance(  ④  
                        c,    ④  
                        mapping.implementation)))));    ④  
}
15-05.eps

Figure 15.5 Enriching a real command service with transaction, auditing, and security aspects

The result of the configuration of listing 15.7 is figure 15.5, which we discussed previously in section 10.3.4.

In case you think that listing 15.7 looks rather complicated, unfortunately, this is just the beginning. That listing presents many shortcomings, some of which are difficult to work around. These include the following:

  • Creation of closed-generic Decorator types can become difficult when either of the generic type arguments of the Decorator don’t exactly match that of the Abstraction.4 
  • It’s impossible to add open-generic implementations that get Decorators applied without being forced to explicitly make the registration for each closed-generic Abstraction.
  • Applying Decorators conditionally, for instance, based on generic type arguments, gets complicated.
  • With an alternative Lifestyle, it becomes complex to prevent Torn Lifestyles in case an implementation implements multiple interfaces.
  • It’s hard to differentiate Lifestyles; all Decorators in the chain get the same Lifestyle.

You could try working through these limitations one-by-one and suggest improvements to listing 15.7, but you’d effectively be developing a new DI Container on top of MS.DI, which is something we discourage. This wouldn’t be productive. Good alternatives, such as Autofac and Simple Injector, are a better pick for this scenario.5 

Although consumers that rely on sequences of Dependencies can be the most intuitive use of multiple instances of the same Abstraction, Decorators are another good example. But there’s a third and perhaps a bit surprising case where multiple instances come into play, which is the Composite design pattern.

15.4.4 Wiring Composites

During the course of this book, we discussed the Composite design pattern on several occasions. In section 6.1.2, for instance, you created a CompositeNotificationService (listing 6.4) that both implemented INotificationService and wrapped a sequence of INotificationService implementations.

Wiring non-generic Composites

Let’s take a look at how you can register Composites, such as the CompositeNotification—Service of chapter 6 in MS.DI. The following listing shows this class again.

Listing 15.8 The CompositeNotificationService Composite from chapter 6

public class CompositeNotificationService : INotificationService
{
    private readonly IEnumerable<INotificationService> services;

    public CompositeNotificationService(
        IEnumerable<INotificationService> services)
    {
        this.services = services;
    }

    public void OrderApproved(Order order)
    {
        foreach (INotificationService service in this.services)
        {
            service.OrderApproved(order);
        }
    }
}

Registering a Composite requires it to be added as a default registration, while injecting it with a sequence of resolved instances:

services.AddTransient<OrderApprovedReceiptSender>();
services.AddTransient<AccountingNotifier>();
services.AddTransient<OrderFulfillment>();

services.AddTransient<INotificationService>(c =>
    new CompositeNotificationService(
        new INotificationService[]
        {
            c.GetRequiredService<OrderApprovedReceiptSender>(),
            c.GetRequiredService<AccountingNotifier>(),
            c.GetRequiredService<OrderFulfillment>(),
        }));

In this example, three INotificationService implementations are registered by their concrete type using the Auto-Wiring API of MS.DI. The CompositeNotificationService, on the other hand, is registered using a delegate. Inside the delegate, the Composite is newed up manually and injected with an array of INotificationService instances. By specifying the concrete types, the previously made registrations are resolved.

Because the number of notification services will likely grow over time, you can reduce the burden on your Composition Root by applying Auto-Registration. Because MS.DI lacks any features in this respect, as we discussed previously, you need to scan the assemblies yourself.

Listing 15.9 Registering CompositeNotificationService

Assembly assembly = typeof(OrderFulfillment).Assembly;

Type[] types = (
    from type in assembly.GetTypes()
    where !type.IsAbstract
    where typeof(INotificationService).IsAssignableFrom(type)
    select type)
    .ToArray();    ①  

foreach (Type type in types)
{
    services.AddTransient(type);
}

services.AddTransient<INotificationService>(c =>
    new CompositeNotificationService(
        types.Select(t =>
            (INotificationService)c.GetRequiredService(t))
        .ToArray()));

Compared to the Decorator example of listing 15.7, listing 15.9 looks reasonably simple. The assembly is scanned for INotificationService implementations, and each found type is appended to the services collection. The array of types is used by the CompositeNotificationService registration. The Composite is injected with a sequence of INotificationService instances that are resolved by iterating through the array of types.

You might be getting used to the level of complexity and verbosity that you need when dealing with MS.DI, but unfortunately, we’re not done yet. Our LINQ query will register any non-generic implementation that implements INotificationService. When you try to run the previous code, depending on which assembly your Composite is located, MS.DI might throw the following exception:

Exception of type 'System.StackOverflowException' was thrown.

Ouch! Stack overflow exceptions are really painful, because they abort the running process and are hard to debug. Besides, this generic exception gives no detailed information about what caused the stack overflow. Instead, you want MS.DI to throw a descriptive exception explaining the cycle, as both Autofac and Simple Injector do.

This stack overflow exception is caused by a cyclic Dependency in CompositeNotificationService. The Composite is picked up by the LINQ query and resolved as part of the sequence. This results in the Composite being dependent on itself. This is an object graph that’s impossible for MS.DI, or any DI Container for that matter, to construct. CompositeNotificationService became a part of the sequence because our LINQ query found all non-generic INotificationService implementations, which includes the Composite.

There are multiple ways around this. The simplest solution is to move the Composite to a different assembly; for instance, the assembly containing the Composition Root. This prevents the LINQ query from selecting the type. Another option is to filter CompositeNotificationService out of the list:

Type[] types = (
    from type in assembly.GetTypes()
    where !type.IsAbstract
    where typeof(INotificationService)
        .IsAssignableFrom(type)
    where type != typeof(CompositeNotificationService)    ①  
    select type)
    .ToArray();

Composite classes, however, aren’t the only classes that might require removal. You’ll have to do the same for any Decorator. This isn’t particularly difficult, but because there typically will be more Decorator implementations, you might be better off querying the type information to find out whether the type represents a Decorator or not. Here’s how you can filter out Decorators as well:

Type[] types = (
    from type in assembly.GetTypes()
    where !type.IsAbstract
    where typeof(INotificationService).IsAssignableFrom(type)
    where type != typeof(CompositeNotificationService)
    where type => !IsDecoratorFor<INotificationService>(type)
    select type)
    .ToArray();

And the following code shows the IsDecoratorFor method:

private static bool IsDecoratorFor<T>(Type type)
{
    return typeof(T).IsAssignableFrom(type) &&
        type.GetConstructors()[0].GetParameters()
            .Any(p => p.ParameterType == typeof(T));
}

The IsDecoratorFor method expects a type to have only a single constructor. A type is considered to be a Decorator when it both implements the given T Abstraction and when its constructor also requires a T.

Wiring generic Composites

In section 15.4.3, you saw how to register generic Decorators. In this section, we’ll take a look at how you can register Composites for generic Abstractions.

In section 6.1.3, you specified the CompositeEventHandler<TEvent> class (listing 6.12) as a Composite implementation over a sequence of IEventHandler<TEvent> implementations. Let’s see if you can register the Composite with its wrapped event handler implementations. To pull this off in MS.DI, you’ll have to get creative, because you have to work around a few unfortunate limitations.

We found that the easiest way to hide event handler implementations behind a Composite is by not registering those implementations at all, and instead moving the construction of the handlers to the Composite. This isn’t pretty, but it gets the job done. In order to hide handlers behind a Composite, you have to rewrite the CompositeEventHandler<TEvent> implementation of listing 6.12 to that in listing 15.10.

Listing 15.10 MS.DI–compatible CompositeEventHandler<TEvent> implementation

public class CompositeSettings    ①  
{    ①  
    public Type[] AllHandlerTypes { get; set; }    ①  
}    ①  

public class CompositeEventHandler<TEvent>
    : IEventHandler<TEvent>
{
    private readonly IServiceProvider provider;
    private readonly CompositeSettings settings;

    public CompositeEventHandler(
        IServiceProvider provider,    ②  
        CompositeSettings settings)    ②  
    {
        this.provider = provider;
        this.settings = settings;
    }

    public void Handle(TEvent e)
    {
        foreach (var handler in this.GetHandlers())    ③  
        {    ③  
            handler.Handle(e);    ③  
        }    ③  
    }

    IEnumerable<IEventHandler<TEvent>> GetHandlers()
    {
        return
            from type in this.settings.AllHandlerTypes
            where typeof(IEventHandler<TEvent>)    ④  
                .IsAssignableFrom(type)    ④  
            select (IEventHandler<TEvent>)
                ActivatorUtilities.CreateInstance(    ⑤  
                    this.provider, type);    ⑤  
    }
}

Compared to the original implementation of listing 6.12, this Composite implementation is more complex. It also takes a hard dependency on MS.DI itself by making use of its IServiceProvider and ActivatorUtilities. In view of this dependency, this Composite certainly belongs inside the Composition Root, because the rest of the application should stay oblivious to the use of a DI Container.

Instead of depending on an IEventHandler<TEvent> sequence, the Composite depends on a Parameter Object that contains all handler types, which includes types that can’t be cast to the specific closed-generic IEventHandler<TEvent> of the Composite. Because of this, the Composite takes on part of the job that the DI Container is supposed to do. It filters out all incompatible types by calling typeof(IEventHandler<TEvent>).IsAssignableFrom(type). This leaves you with a registration of the Composite and the scanning of all event handlers.

Listing 15.11 Registering CompositeEventHandler<TEvent>

var handlerTypes =    ①  
    from type in assembly.GetTypes()    ①  
    where !type.IsAbstract    ①  
    where !type.IsGenericType    ①  
    let serviceTypes = type.GetInterfaces()    ①  
        .Where(i => i.IsGenericType &&    ①  
            i.GetGenericTypeDefinition()    ①  
                == typeof(IEventHandler<>))    ①  
    where serviceTypes.Any()    ①  
    select type;    ①  

services.AddSingleton(new CompositeSettings    ②  
{    ②  
    AllHandlerTypes = handlerTypes.ToArray()  ②  
});    ②  

services.AddTransient(    ③  
    typeof(IEventHandler<>),    ③  
    typeof(CompositeEventHandler<>));    ③  

Together with the fat Composite implementation, this last listing effectively implements the Composite pattern in combination with MS.DI.

Even though we’ve managed to work around some of the limitations of MS.DI, you might be less lucky in other cases. For instance, you might run out of luck if the sequence of elements consists of both non-generic and generic implementations, when generic implementations contain generic type constraints or when Decorators need to be conditional.

We do admit that this is an unpleasant solution. We preferred writing less code to show you how to apply MS.DI to the patterns presented in this book, but not all is peaches and cream, unfortunately. That’s why, in our day-to-day development jobs, we prefer Pure DI or one of the mature DI Containers, such as Autofac and Simple Injector.

No matter which DI Container you select, or even if you prefer Pure DI, we hope that this book has conveyed one important point — DI doesn’t rely on a particular technology, such as a particular DI Container. An application can, and should, be designed using the DI-friendly patterns and practices presented in this book. When you succeed in doing that, selection of a DI Container becomes of less importance. A DI Container is a tool that composes your application, but ideally, you should be able to replace one container with another without rewriting any part of your application other than the Composition Root.

Summary

  • The Microsoft.Extensions.DependencyInjection (MS.DI) DI Container has a limited set of features. A comprehensive API that addresses Auto-Registration, Decorators, and Composites is missing. This makes it less suited for development of applications that are designed around the principles and patterns presented in this book.
  • MS.DI enforces a strict separation of concerns between configuring and consuming a container. You configure components using a ServiceCollection instance, but a ServiceCollection can’t resolve components. When you’re done configuring a ServiceCollection, you use it to build a ServiceProvider that you can use to resolve components.
  • With MS.DI, resolving from the root container directly is a bad practice. This will easily lead to memory leaks or concurrency bugs. Instead, you should always resolve from an IServiceScope.
  • MS.DI supports the three standard Lifestyles: Transient, Singleton, and Scoped.
..................Content has been hidden....................

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