8
Object lifetime

In this chapter

  • Managing Dependency Lifetime
  • Working with disposable Dependencies
  • Using Singleton, Transient, and Scoped Lifestyles
  • Preventing or fixing bad Lifestyle choices

The passing of time has a profound effect on most food and drink, but the consequences vary. Personally, we find 12-month-old Gruyère more interesting than 6-month-old Gruyère, but Mark prefers his asparagus fresher than either of those.1  In many cases, it’s easy to assess the proper age of an item; but in certain cases, doing so becomes complex. This is most notable when it comes to wine (see figure 8.1).

Wines tend to get better with age — until they suddenly become too old and lose most of their flavor. This depends on many factors, including the origin and vintage of the wine. Although wines interest us, we don’t ever expect we’ll be able to predict when a wine will peak. For that, we rely on experts: books at home and sommeliers at restaurants. They understand wines better than we do, so we happily let them take control.

08-01.tif

Figure 8.1 Wine, cheese, and asparagus. Although the combination may be a bit off, their age greatly affects their overall qualities.

Unless you dove straight into this chapter without reading any of the previous ones, you know that letting go of control is a key concept in DI. This stems from the Inversion of Control principle, where you delegate control of your Dependencies to a third party, but it also implies more than just letting someone else pick an implementation of a required Abstraction. When you allow a Composer to supply a Dependency, you must also accept that you can’t control its lifetime.

Just as the sommelier intimately knows the contents of the restaurant’s wine cellar and can make a far more informed decision than we can, we should trust the Composer to be able to control the lifetime of Dependencies more efficiently than the consumer. Composing and managing components is its single responsibility.

In this chapter, we’ll explore Dependency Lifetime Management. Understanding this topic is important because, just as you can have a subpar experience if you drink a wine at the wrong age (both your own age and the wine’s), you can experience degraded performance from configuring Dependency Lifetime incorrectly. Even worse, you may get the Lifetime Management equivalent of spoiled food: resource leaks. Understanding the principles of correctly managing the lifecycles of components should enable you to make informed decisions to configure your applications correctly.

We’ll start with a general introduction to Dependency Lifetime Management, followed by a discussion about disposable Dependencies. This first part of the chapter is meant to provide all the background information and guiding principles you need in order to make knowledgeable decisions about your own applications' lifecycles, scope, and configurations.

After that, we’ll look at different lifetime strategies. This part of the chapter takes the form of a catalog of available Lifestyles. In most cases, one of these stock Lifestyle patterns will provide a good match for a given challenge, so understanding them in advance equips you to deal with many difficult situations.

We’ll finish the chapter with some bad habits, or anti-patterns, concerning Lifetime Management. When we’re finished, you should have a good grasp of Lifetime Management and common Lifestyle do’s and don’ts. First, let’s look at Object Lifetime and how it relates to DI in general.

8.1 Managing Dependency Lifetime

Up to this point, we’ve mostly discussed how DI enables you to compose Dependencies. The previous chapter explored this subject in great detail, but, as we alluded to in section 1.4, Object Composition is just one aspect of DI. Managing Object Lifetime is another.

The first time we were introduced to the idea that the scope of DI includes Lifetime Management, we failed to understand the deep connection between Object Composition and Object Lifetime. We finally got it, and it’s simple, so let’s take a look!

In this section, we’ll introduce Lifetime Management and how it applies to Dependencies. We’ll look at the general case of composing objects and how it has implications for the lifetimes of Dependencies. First, we’ll investigate why Object Composition implies Lifetime Management.

8.1.1 Introducing Lifetime Management

When we accept that we should let go of our psychological need for control over Dependencies and instead request them through Constructor Injection or one of the other DI patterns, we must let go completely. To understand why, we’ll examine the issue progressively. Let’s begin by reviewing what the standard .NET object lifecycle means for Dependencies. You likely already know this, but bear with us for the next half page while we establish the context.

Simple Dependency lifecycle

You know that DI means you let a third party (typically our Composition Root) serve the Dependencies you need. This also means you must let it manage the Dependencies’ lifetimes. This is easiest to understand when it comes to object creation. Here’s a (slightly restructured) code fragment from the sample e-commerce application’s Composition Root. (You can see the complete example in listing 7.8.)

var productRepository =
    new SqlProductRepository(
        new CommerceContext(connectionString));

var productService =
    new ProductService(
        productRepository,
        userContext);

We hope that it’s evident that the ProductService class doesn’t control when productRepository is created. In this case, SqlProductRepository is likely to be created within the same millisecond; but as a thought experiment, we could insert a call to Thread.Sleep between these two lines of code to demonstrate that you can arbitrarily separate them over time. That would be a pretty weird thing to do, but the point is that not all objects of a Dependency graph have to be created at the same time.

Consumers don’t control creation of their Dependencies, but what about destruction? As a general rule, you don’t control when objects are destroyed in .NET. The garbage collector cleans up unused objects, but unless you’re dealing with disposable objects, you can’t explicitly destroy an object.

Objects are eligible for garbage collection when they go out of scope. Conversely, they last as long as someone else holds a reference to them. Although a consumer can’t explicitly destroy an object — that’s up to the garbage collector — it can keep the object alive by holding on to the reference. This is what you do when you use Constructor Injection, because you save the Dependency in a private field:

public class HomeController
{
    private readonly IProductService service;

    public HomeController(IProductService service)    ①  
    {
        this.service = service;    ②  
    }
}

This means that when the consumer goes out of scope, so can the Dependency. Even when a consumer goes out of scope, however, the Dependency can live on if other objects hold a reference to it. Otherwise, it’ll be garbage collected. Because you’re an experienced .NET developer, this is probably old news to you, but now the discussion should begin to get more interesting.

Adding complexity to the Dependency lifecycle

Until now our analysis of the Dependency lifecycle has been mundane, but now we can add some complexity. What happens when more than one consumer requires the same Dependency? One option is to supply each consumer their own instance, as shown in figure 8.2.

08-02.eps

Figure 8.2 Composing multiple, unique instances of a Dependency

The following listing composes multiple consumers with multiple instances of the same Dependency, shown in figure 8.2.

Listing 8.1 Composing with multiple instances of the same Dependency

var repository1 = new SqlProductRepository(connString);  ①  
var repository2 = new SqlProductRepository(connString);  ①  


var productService = new ProductService(repository1);    ②  


var calculator = new DiscountCalculator(repository2);    ③  

When it comes to the lifecycles of each Repository in listing 8.1, nothing has changed compared to the previously discussed sample e-commerce application’s Composition Root. Each Dependency goes out of scope and is garbage-collected when its consumers go out of scope. This can happen at different times, but the situation is only marginally different than before. It would be a somewhat different situation if both consumers were to share the same Dependency, as shown in figure 8.3.

08-03.eps

Figure 8.3 Reusing the same instance of a Dependency by injecting it into multiple consumers

When you apply this to listing 8.1, you get the code in listing 8.2.

Listing 8.2 Composing with a single instance of the same Dependency

var repository = new SqlProductRepository(connString);

var productService = new ProductService(repository);    ①  

var calculator = new DiscountCalculator(repository);    ①  

When comparing listings 8.1 and 8.2, you don’t find that one is inherently better than the other. As we’ll discuss in section 8.3, there are several factors to consider when it comes to when and how you want to reuse a Dependency.

The lifecycle for the Repository Dependency has changed distinctly, compared with the previous example. Both consumers must go out of scope before the variable repository can be eligible for garbage collection, and they can do so at different times. The situation becomes less predictable when the Dependency reaches the end of its lifetime. This trait is only reinforced when the number of consumers increases.

Given enough consumers, it’s likely that there’ll always be one around to keep the Dependency alive. This may sound like a problem, but it rarely is: instead of a multitude of similar instances, you have only one, which saves memory. This is such a desirable quality that we formalize it in a Lifestyle pattern called the Singleton Lifestyle. Don’t confuse this with the Singleton design pattern, although there are similarities.2  We’ll go into greater detail about this subject in section 8.3.1.

The key point to appreciate is that the Composer has a greater degree of influence over the lifetime of Dependencies than any single consumer. The Composer decides when instances are created, and by its choice of whether to share instances, it determines whether a Dependency goes out of scope with a single consumer, or whether all consumers must go out of scope before the Dependency can be released.

This is comparable to visiting a restaurant with a good sommelier. The sommelier spends a large proportion of the day managing and evolving the wine cellar: buying new wines, sampling the available bottles to track how they develop, and working with the chefs to identify optimal matches to the food being served. When we’re presented with the wine list, it includes only what the sommelier deems fit to offer for today’s menu. We’re free to select a wine according to our personal taste, but we don’t presume to know more about the restaurant’s selection of wines and how they go with the food than the sommelier does. The sommelier will often decide to keep lots of bottles in stock for years; and as you’ll see in the next section, a Composer may decide to keep instances alive by holding on to their references.

8.1.2 Managing lifetime with Pure DI

The previous section explained how you can vary the composition of Dependencies to influence their lifetimes. In this section, we’ll look at how to implement this using Pure DI, while applying the two most commonly used Lifestyles: Transient and Singleton.

In chapter 7, you created specialized classes to compose applications. One of these was a CommerceControllerActivator for an ASP.NET Core MVC application — our Composer. Listing 7.8 shows the implementation of its Create method.

As you may recall, the Create method creates the entire object graph on the fly each time it’s invoked. Each Dependency is private to the issued controller, and there’s no sharing. When the controller instance goes out of scope (which it does every time the server has replied to a request), all the Dependencies go out of scope too. This is often called a Transient Lifestyle, which we’ll talk more about in section 8.3.2.

Let’s analyze the object graphs created by the CommerceControllerActivator and shown in figure 8.4 to see if there’s room for improvement. Both the AspNetUserContextAdapter and RouteCalculator classes are completely stateless services, so there’s no reason to create a new instance every time you need to service a request. The connection string is also unlikely to change, so you can reuse it across requests. The SqlProductRepository class, on the other hand, relies on an Entity Framework DbContext (implemented by our CommerceContext), which mustn’t be shared across requests.3 

08-04.eps

Figure 8.4 Object graphs as created by CommerceControllerActivator, which creates HomeController and RouteController instances with their Dependencies

Given this particular configuration, a better implementation of CommerceControllerActivator would reuse the same instances of both AspNetUserContextAdapter and RouteCalculator, while creating new instances of ProductService and SqlProductRepository. In short, you should configure AspNetUserContextAdapter and RouteCalculator to use the Singleton Lifestyle, and ProductService and SqlProductRepository as Transient. The following listing shows how to implement this change.

Listing 8.3 Managing lifetime within the CommerceControllerActivator

public class CommerceControllerActivator : IControllerActivator
{
    private readonly string connectionString;
    private readonly IUserContext userContext;    ①  
    private readonly RouteCalculator calculator;    ①  

    public CommerceControllerActivator(string connectionString)
    {
        this.connectionString = connectionString;

        this.userContext =    ②  
            new AspNetUserContextAdapter();    ②  
    ②  
        this.calculator =    ②  
            new RouteCalculator(    ②  
                this.CreateRouteAlgorithms());    ②  
    }

    public object Create(ControllerContext context)
    {
        Type type = context.ActionDescriptor
            .ControllerTypeInfo.AsType();

        switch (type.Name)
        {
            case "HomeController":
                return this.CreateHomeController();

            case "RouteController":
                return this.CreateRouteController();

            default:
                throw new Exception("Unknown controller " + type.Name);

        }
    }

    private HomeController CreateHomeController()
    {
        return new HomeController(    ③  
            new ProductService(    ③  
                new SqlProductRepository(    ③  
                    new CommerceContext(  ③  
                        this.connectionString)),  ③  
                this.userContext));    ③  
    }    ③  
    ③  
    private RouteController CreateRouteController()    ③  
    {    ③  
        return new RouteController(this.calculator);    ③  
    }

    public void Release(ControllerContext context,    ④  
        object controller) { ... }    ④  
}

In an MVC application, it’s practical to load configuration values in the Startup class. That’s why in listing 8.3, the connection string is supplied to the constructor of the CommerceControllerActivator.

The code in listing 8.3 is functionally equivalent to the code in listing 7.8 — it’s just slightly more efficient because some of the Dependencies are shared. By holding on to the Dependencies you create, you can keep them alive for as long as you want. In this example, CommerceControllerActivator created both Singleton Dependencies as soon as it was initialized, but it could also have used lazy initialization.

The ability to fine-tune each Dependency’s Lifestyle can be important for performance reasons, but can also be important for correct behavior. For instance, the Mediator design pattern relies on a shared director through which several components communicate.4  This only works when the Mediator is shared among the involved collaborators.

So far, we’ve discussed how Inversion of Control implies that consumers can’t manage the lifetimes of their Dependencies, because they don’t control creation of objects; and because .NET uses garbage collection, consumers can’t explicitly destroy objects, either. This leaves a question unanswered: what about disposable Dependencies? We’ll now turn our attention to that delicate question.

8.2 Working with disposable Dependencies

Although .NET is a managed platform with a garbage collector, it can still interact with unmanaged code. When this happens, .NET code interacts with unmanaged memory that isn’t garbage-collected. To prevent memory leaks, you must have a mechanism with which to deterministically release unmanaged memory. This is the key purpose of the IDisposable interface.

It’s likely that some Dependency implementations will contain unmanaged resources. As an example, ADO.NET connections are disposable because they tend to use unmanaged memory. As a result, database-related implementations like Repositories backed by databases are likely to be disposable themselves. How should we model disposable Dependencies? Should we also let Abstractions be disposable? That might look like this:

smell.tif
public interface IMyDependency : IDisposable

If you feel the urge to add IDisposable to your interface, it’s probably because you have a particular implementation in mind. But you must not let that knowledge leak through to the interface design. Doing so would make it more difficult for other classes to implement the interface and would introduce vagueness into the Abstraction.

Who’s responsible for disposing of a disposable Dependency? Could it be the consumer?

8.2.1 Consuming disposable Dependencies

For the sake of argument, imagine that you have a disposable Abstraction like the following IOrderRepository interface.

smell.tif

Listing 8.4 IOrderRepository implementing IDisposable

public interface IOrderRepository : IDisposable

How should an OrderService class deal with such a Dependency? Most design guidelines (including Visual Studio’s built-in Code Analysis) would insist that if a class holds a disposable resource as a member, it should itself implement IDisposable and dispose of the resource. The next listing shows how.

bad.tif

Listing 8.5 OrderService depending on disposable Dependency

public sealed class OrderService : IDisposable    ①  
{
    private readonly IOrderRepository repository;

    public OrderService(IOrderRepository repository)
    {
        this.repository = repository;
    }

    public void Dispose()
    {
        this.repository.Dispose();    ②  
    }
}

But this turns out to be a bad idea because the repository member was originally injected, and it can be shared by other consumers:

var repository =
    new SqlOrderRepository(connectionString);

var validator = new OrderValidator(repository);    ①  
var orderService = new OrderService(repository);    ①  

orderService.AcceptOrder(order);
orderService.Dispose();    ②  

validator.Validate(order);    ③  

It would be less dangerous not to dispose of the injected Repository, but this means you’re ignoring the fact that the Abstraction is disposable. Besides, in this case, the Abstraction exposes more members than used by the client, which is an Interface Segregation Principle violation (see section 6.2.1). Declaring an Abstraction as deriving from IDisposable provides no benefit.

Then again, there can be scenarios where you need to signal the beginning and end of a short-lived scope; IDisposable is sometimes used for that purpose. Before we examine how a Composer can manage the lifetime of a disposable Dependency, we should consider how to deal with such ephemeral disposables.

Creating ephemeral disposables

Many APIs in the .NET BCL use IDisposable to signal that a particular scope has ended. One of the more prominent examples is WCF proxies.

It’s important to remember that the use of IDisposable for such purposes need not indicate a Leaky Abstraction, because these types aren’t always Abstractions in the first place. On the other hand, some of them are; and when that’s the case, how do you deal with them?

Fortunately, after an object is disposed of, you can’t reuse it. If you want to invoke the same API again, you must create a new instance. As an example that fits well with how you use WCF proxies or ADO.NET commands, you create the proxy, invoke its operations, and dispose of it as soon as you’re finished. How can you reconcile this with DI if you consider disposable Abstractions to be Leaky Abstractions?

As always, hiding the messy details behind an interface can be helpful. Returning to the UWP application from section 7.2, we used an IProductRepository Abstraction to hide the details of communicating with a data store from the presentation logic layer. During this discussion, we ignored the details of such an implementation because it wasn’t that relevant at that moment. But let’s assume that the UWP application must communicate with a WCF web service. From the EditProductViewModel’s perspective, this is how you delete a product:

private void DeleteProduct()
{
    this.productRepository.Delete(this.Model.Id);    ①  
    this.whenDone();
}

Another picture forms when we look at the WCF implementation of that interface. Here’s the implementation of WcfProductRepository with its Delete method.

Listing 8.6 Using a WCF channel as an ephemeral disposable

public class WcfProductRepository : IProductRepository
{
    private readonly ChannelFactory<IProductManagementService> factory;

    public WcfProductRepository(
        ChannelFactory<IProductManagementService> factory)
    {
        this.factory = factory;
    }

    public void Delete(Guid productId)    ①  
    {    ①  
        using (var channel =    ①  
            this.factory.CreateChannel())    ①  
        {    ①  
            channel.DeleteProduct(productId);    ①  
        }    ①  
    }
    ...
}

The WcfProductRepository class has no mutable state, so you inject a ChannelFactory<TChannel> that you can use to create a channel. Channel is just another word for a WCF proxy, and it’s the autogenerated client interface you get for free when you create a service reference with Visual Studio or svcutil.exe.

Because this interface derives from IDisposable, you can wrap it in a using statement. You then use the channel to delete the product. When you exit the using scope, the channel is disposed of.

Every time you invoke a method on the WcfProductRepository class, it quickly opens a new channel and disposes of it after use. Its lifetime is extremely short, which is why we call such a disposable Abstraction an ephemeral disposable.

But wait! Didn’t we claim that a disposable Abstraction is a Leaky Abstraction? Yes, we did, but we have to balance pragmatic concerns against principles. In this case, at least, WcfProductRepository and IProductManagementService are defined in the same WCF-specific library. This ensures that the Leaky Abstraction can be confined to code that has a reasonable expectation of knowing about and managing that complexity.

Notice that the ephemeral disposable is never injected into the consumer. Instead, a factory is used, and you use that factory to control the lifetime of the ephemeral disposable.

ChannelFactory<TChannel> is thread-safe and can be injected as a Singleton. In this case, you might wonder why we choose to inject ChannelFactory<TChannel> into the WcfProductRepository’s constructor; you can create it internally and store it in a static field. This, however, causes WcfProductRepository to be implicitly dependent on a configuration file, which needs to exist to create a new WcfProductRepository. As we discussed in 2.2.3, only the finished application should rely on configuration files.

In summary, disposable Abstractions are Leaky Abstractions. Sometimes we must accept such a leak to avoid bugs (such as refused WCF connections); but when we do that, we should do our best to contain that leak so it doesn’t propagate throughout an entire application. We’ve now examined how to consume disposable Dependencies. Let’s turn our attention to how we can serve and manage them for consumers.

8.2.2 Managing disposable Dependencies

Because we so adamantly insist that disposable Abstractions are Leaky Abstractions, the consequence is that Abstractions shouldn’t be disposable. On the other hand, sometimes implementations are disposable; if you don’t properly dispose of them, you’ll have resource leaks in your applications. Someone or something must dispose of them.

As always, this responsibility falls on the Composer. It, better than anything else, knows when it creates a disposable instance, so it also knows when the instance needs to be disposed of. It’s easy for the Composer to keep a reference to the disposable instance and invoke its Dispose method at an appropriate time. The challenge lies in identifying when it’s the appropriate time. How do you know when all consumers have gone out of scope?

Unless you’re informed when that happens, you don’t know. Often, however, your code lives inside some sort of context with a well-defined lifetime, as well as events that tell you when a specific scope completes. In ASP.NET Core, for instance, you can scope instances around a single web request. At the end of a web request, the framework tells IControllerActivator, which is typically our Composer, that it should release all Dependencies for a given object. It’s then up to the Composer to keep track of those Dependencies and to decide whether anything must be disposed of based on their Lifestyles.

Releasing Dependencies

Releasing an object graph isn’t the same as disposing of it. As we stated in the introduction, releasing is the process of determining which Dependencies can be dereferenced and possibly disposed of, and which Dependencies should be kept alive to be reused. It’s the Composer that decides whether a released object should be disposed of or reused.

The release of an object graph is a signal to the Composer that the root of the graph is going out of scope, so if the root itself implements IDisposable, then it should be disposed of. But the root’s Dependencies can be shared with other roots, so the Composer may decide to keep some of them around, because it knows other objects still rely on them. Figure 8.5 illustrates the sequence of events.

08-05.eps

Figure 8.5 The sequence of events for releasing Dependencies

To release Dependencies, a Composer must track all the disposable Dependencies it has ever served, and to which consumers it has served them, so that it can dispose of them when the last consumer is released. And a Composer must take care to dispose of objects in the correct order.

Let’s go back to the CommerceControllerActivator example from listing 8.3. As it turns out, there’s a bug in that listing, because CommerceContext implements IDisposable. The code in listing 8.3 creates new instances of CommerceContext, but it never disposes of those instances. This could cause resource leaks, so let’s fix that bug with a new version of the Composer.

First, keep in mind that the Composer for a web application must be able to service many concurrent requests, so it has to associate each CommerceContext instance with either the root object it creates or with the request it’s associated with. In the following example, we’ll use the request to track disposable objects, because this saves us from having to define a static dictionary instance. A static mutable state is more difficult to use correctly, because it must be implemented in a thread-safe manner. The next listing shows how CommerceControllerActivator resolves requests for HomeController instances.

Listing 8.7 Associating disposable Dependencies with a web request

private HomeController CreateHomeController(ControllerContext context)
{
    var dbContext =
        new CommerceContext(this.connectionString);    ①  

    TrackDisposable(context, dbContext);    ②  

    return new HomeController(
        new ProductService(
            new SqlProductRepository(dbContext),
            this.userContext));
}

private static void TrackDisposable(
    ControllerContext context, IDisposable disposable)
{    ③  
    IDictionary<object, object> items =    ③  
        context.HttpContext.Items;    ③  
    ③  
    object list;    ③  
    ③  
    if (!items.TryGetValue("Disposables", out list))    ③  
    {    ③  
        list = new List<IDisposable>();    ③  
        items["Disposables"] = list;    ③  
    }    ③  
    ③  
    ((List<IDisposable>)list).Add(disposable);    ③  
}

The CreateHomeController method starts by resolving all the Dependencies. This is similar to the implementation in listing 8.3, but before returning the resolved service, it must store the Dependency with the request in such a way that it can be disposed of when the controller gets released. The application flow of listing 8.7 is shown in figure 8.6.

08-06.eps

Figure 8.6 Tracking disposable Dependencies

When we implemented the CommerceControllerActivator in listing 7.8, we left the Release method empty. So far, we haven’t implemented this method, relying on the garbage collector to do the job; but with disposable Dependencies, it’s essential that you take this opportunity to clean up. Here’s the implementation.

Listing 8.8 Releasing disposable Dependencies

public void Release(ControllerContext context, object controller)
{
    var disposables =    ①  
        (List<IDisposable>)context.HttpContext    ①  
            .Items["Disposables"];    ①  

    if (disposables != null)
    {
        disposables.Reverse();    ②  

        foreach (IDisposable disposable in disposables)  ③  
        {    ③  
            disposable.Dispose();    ③  
        }
    }
}

This Release method takes a shortcut that prevents some disposables from being disposed of if an exception is thrown. If you’re meticulous, you’ll need to ensure that disposal of instances continues, even if one throws an exception, preferably by using try and finally statements. We’ll leave this as an exercise for the reader.

In the context of ASP.NET Core MVC, the given solution using TrackDisposable and Release can be reduced to a simple call to HttpContext.Response.RegisterForDispose, because that would effectively do the same thing. It both implements opposite-order disposal and continues disposing of objects in case of a failure. Because this chapter isn’t about ASP.NET Core MVC in particular, we wanted to provide you with a more generic solution that illustrates the basic idea.

Where should Dependencies be released?

After reading all this, two questions remain: where should object graphs be released, and who is responsible for doing this? It’s important to note that the code that has requested an object graph is also responsible for requesting its release. Because the request for an object graph is typically part of the Composition Root, so is the initiation of its release.

The following listing shows the Main method of the console application of section 7.1 again, but now with an additional Release method.

Listing 8.9 The Composition Root that releases the resolved object graph

static void Main(string[] args)
{
    string connStr = LoadConnectionString();

    CurrencyParser parser =
        CreateCurrencyParser(connStr);    ①  

    ICommand command = parser.Parse(args);    ②  
    command.Execute();    ②  

    Release(parser);    ③  
}

When building a console application, you’re in full control of the application. As we discussed in section 7.1, there’s no Inversion of Control. If you’re using a framework, you’ll often see the framework take control over both requesting the object graph and demanding its release. ASP.NET Core MVC is a good example of this. In the case of MVC, it’s the framework that calls CommerceControllerActivator’s Create and Release methods. In between those calls, it uses a resolved controller instance.

We’ve now discussed Lifetime Management in some detail. As a consumer, you can’t manage the lifetime of injected Dependencies; that responsibility falls on the Composer who can decide to share a single instance among many consumers or give each consumer its own private instance. These Singleton and Transient Lifestyles are only the most common members of a larger set of Lifestyles, and we’ll use the next section to work our way through a catalog of the most common lifecycle strategies.

8.3 Lifestyle catalog

Now that we’ve covered the principles behind Lifetime Management, we’ll spend some time looking at common Lifestyle patterns. As we described in the introduction, a Lifestyle is a formalized way of describing the intended lifetime of a Dependency. This gives us a common vocabulary, just as design patterns do. It makes it easier to reason about when and how a Dependency is expected to go out of scope — and if it’ll be reused.

This section discusses the three most common Lifestyles described in table 8.1. Because you’ve already encountered both Singleton and Transient, we’ll begin with those.

Table 8.1 Lifestyle patterns covered in this section
NameDescription
SingletonA single instance is perpetually reused.
TransientNew instances are always served.
ScopedAt most, one instance of each type is served per an implicitly or explicitly defined scope.

The use of a Scoped Lifestyle is widespread; most exotic Lifestyles are variations of it. Compared to advanced Lifestyles, a Singleton Lifestyle may seem mundane, but it’s nevertheless a common and appropriate lifecycle strategy.

8.3.1 The Singleton Lifestyle

In this book, we’ve implicitly used the Singleton Lifestyle from time to time. The name is both clear and somewhat confusing at the same time. It makes sense, however, because the resulting behavior is similar to the Singleton design pattern, but the structure is different.

With both the Singleton Lifestyle and the Singleton design pattern, there’s only one instance of a Dependency, but the similarity ends there. The Singleton design pattern provides a global point of access to its instance, which is similar to the Ambient Context anti-pattern we discussed in section 5.3. A consumer, however, can’t access a Singleton-scoped Dependency through a static member. If you ask two different Composers to serve an instance, you’ll get two different instances. It’s important, therefore, that you don’t confuse the Singleton Lifestyle with the Singleton design pattern.

Because only a single instance is in use, the Singleton Lifestyle generally consumes a minimal amount of memory and is efficient. The only time this isn’t the case is when the instance is used rarely but consumes large amounts of memory. In such cases, the instance can be wrapped in a Virtual Proxy, as we’ll discuss in section 8.4.2.

When to use the Singleton Lifestyle

Use the Singleton Lifestyle whenever possible. Two main issues that might prevent you from using a Singleton follow:

  • When a component isn’t thread-safe. Because the Singleton instance is potentially shared among many consumers, it must be able to handle concurrent access.
  • When one of the component’s Dependencies has a lifetime that’s expected to be shorter, possibly because it isn’t thread-safe. Giving the component a Singleton Lifestyle would keep its Dependencies alive for too long. In that case, such a Dependency becomes a Captive Dependency. We’ll go into more detail about Captive Dependencies in section 8.4.1.

All stateless services are, by definition, thread-safe, as are immutable types and, obviously, classes specifically designed to be thread-safe. In these cases, there’s no reason not to configure them as Singletons.

In addition to the argument for efficiency, some Dependencies may work as intended only if they’re shared. For example, this is the case for implementations of the Circuit Breaker7  design pattern that we’ll discuss in chapter 9, as well as in-memory caches. In these cases, it’s essential that the implementations are thread-safe.

Let’s take a closer look at an in-memory Repository. We’ll explore an example of this next.

Example: Using a thread-safe in-memory Repository

Let’s once more turn our attention to implementing a CommerceControllerActivator like those from sections 7.3.1 and 8.1.2. Instead of using a SQL Server–based IProductRepository, you could use a thread-safe, in-memory implementation. For an in-memory data store to make sense, it must be shared among all requests, so it has to be thread-safe. This is illustrated in figure 8.7.

08-07.eps

Figure 8.7 When multiple ProductService instances running on separate threads access a shared resource, such as an in-memory IProductRepository, you must ensure that the shared resource is thread-safe.

Instead of explicitly implementing such a Repository using the Singleton design pattern, you should use a concrete class and scope it appropriately using the Singleton Lifestyle. The next listing shows how a Composer can return new instances every time it’s asked to resolve a HomeController, whereas IProductRepository is shared among all instances.

Listing 8.10 Managing a Singleton Lifestyle

public class CommerceControllerActivator : IControllerActivator
{
    private readonly IUserContext userContext;    ①  
    private readonly IProductRepository repository;    ①  

    public CommerceControllerActivator()
    {
        this.userContext = new FakeUserContext();    ②  
        this.repository = new InMemoryProductRepository();    ②  
    }
    ...
    private HomeController CreateHomeController()
    {
        return new HomeController(    ③  
            new ProductService(    ③  
                this.repository,    ③  
                this.userContext));    ③  
    }
}

Note that in this example, both repository and userContext encompass Singleton Lifestyles. You can, however, mix Lifestyles if you want. Figure 8.8 shows what happens with CommerceControllerActivator at runtime.

08-08.eps

Figure 8.8 Composing Singletons using CommerceControllerActivator

The Singleton Lifestyle is one of the easiest Lifestyles to implement. All it requires is that you keep a reference to the object and serve the same object every time it’s requested. The instance doesn’t go out of scope until the Composer goes out of scope. When that happens, the Composer should dispose of the object if it’s a disposable type.

Another Lifestyle that’s trivial to implement is the Transient Lifestyle. Let’s look at that next.

8.3.2 The Transient Lifestyle

The Transient Lifestyle involves returning a new instance every time it’s requested. Unless the instance returned implements IDisposable, there’s nothing to keep track of. Conversely, when the instance implements IDisposable, the Composer must keep it in mind and explicitly dispose of it when asked to release the applicable object graph. Most of the examples in this book of constructed object graphs implicitly used the Transient Lifestyle.

It’s worth noting that in desktop and similar applications, we tend to resolve the entire object hierarchy only once: at application startup. This means that even for Transient components, only a few instances could be created, and they can be around for a long time. In the degenerate case where there’s only one consumer per Dependency, the end result of resolving a graph of pure Transient components is equivalent to resolving a graph of pure Singletons, or any mix thereof. This is because the graph is resolved only once, so the difference in behavior is never realized.

When to use the Transient Lifestyle

The Transient Lifestyle is the safest choice of Lifestyles, but also one of the least efficient. It can cause a myriad of instances to be created and garbage collected, even when a single instance would have sufficed.

If you have doubts about the thread-safety of a component, however, the Transient Lifestyle is safe, because each consumer has its own instance of the Dependency. In many cases, you can safely exchange the Transient Lifestyle for a Scoped Lifestyle, where access to the Dependency is also guaranteed to be sequential.

Example: Resolving multiple Repositories

You saw several examples of using the Transient Lifestyle earlier in this chapter. In listing 8.3, the Repository is created and injected on the spot in the resolving method, and the Composer keeps no reference to it. In listings 8.8 and 8.9, you subsequently saw how to deal with a Transient disposable component.

In these examples, you may have noticed that the userContext stays a Singleton throughout. This is a purely stateless service, so there’s no reason to create a new instance for every ProductService created. The noteworthy point is that you can mix Dependencies with different Lifestyles.

When multiple components require the same Dependency, each is given a separate instance. The following listing shows a method resolving an ASP.NET Core MVC controller.

Listing 8.11 Resolving TransientAspNetUserContextAdapter instances

private HomeController CreateHomeController()
{
    return new HomeController(
        new ProductService(
            new SqlProductRepository(this.connStr),
            new AspNetUserContextAdapter(),    ①  
            new SqlUserRepository(
                this.connStr,
                new AspNetUserContextAdapter())));    ①  
}

The Transient Lifestyle implies that every consumer receives a private instance of the Dependency, even when multiple consumers in the same object graph have the same Dependency (as is the case in the previous listing). If many consumers share the same Dependency, this approach can be inefficient; but if the implementation isn’t thread-safe, the more efficient Singleton Lifestyle is inappropriate. In such cases, the Scoped Lifestyle may be a better fit.

8.3.3 The Scoped Lifestyle

As users of a web application, we’d like a response from the application as quickly as possible, even when other users are accessing the system at the same time. We don’t want our request to be put on a queue together with all the other users’ requests. We might have to wait an inordinate amount of time for a response if there are many requests ahead of ours. To address this issue, web applications handle requests concurrently. The ASP.NET Core infrastructure shields us from this by letting each request execute in its own context and with its own instance of controllers (if you use ASP.NET Core MVC).

Because of concurrency, Dependencies that aren’t thread-safe can’t be used as Singletons. On the other hand, using them as Transients can be inefficient or even downright problematic if you need to share a Dependency between different consumers within the same request.

Although the ASP.NET Core engine executes a single request asynchronously, and the execution of a single request typically involves multiple threads, it does guarantee that code is executed in a sequential manner — at least when you properly await asynchronous operations.8  This means that if you can share a Dependency within a single request, thread-safety isn’t an issue. Section 8.4.3 provides more details on how the asynchronous, multi-threaded approach works in ASP.NET Core.

Although the concept of a web request is limited to web applications and web services, the concept of a request is broader. Most long-running applications use requests to execute single operations. For example, when building a service application that processes items one by one from a queue, you can imagine each processed item as an individual request, consisting of its own set of Dependencies.

The same could hold for desktop or phone applications. Although the top root types (views or ViewModels) could potentially live for a long time, you could see a button press as a request, and you could scope this operation and give it its own isolated bubble with its own set of Dependencies. This leads to the concept of a Scoped Lifestyle, where you decide to reuse instances within a given scope. Figure 8.9 demonstrates how the Scoped Lifestyle works.

08-09.eps

Figure 8.9 The Scoped Lifestyle indicates that you create, at most, one instance per specified scope.

Note that DI Containers might have specialized versions of the Scoped Lifestyle that target a specific technology. Also, any disposable components should be disposed of when the scope ends.

When to use the Scoped Lifestyle

The Scoped Lifestyle makes sense for long-running applications that are tasked with processing operations that need to run with some degree of isolation. Isolation is required when these operations are processed in parallel, or when each operation contains its own state. Web applications are a great example of where the Scoped Lifestyle works well, because web applications typically process requests in parallel, and those requests typically contain some mutable state that’s specific to the request. But even if a web application starts some background operation that isn’t related to a web request, the Scoped Lifestyle is valuable. Even these background operations can typically be mapped to the concept of a request.

As with all Lifestyles, you can mix the Scoped Lifestyle with others so that, for example, some Dependencies are configured as Singletons, and others are shared per request.

Example: Composing a long-running application using a scoped DbContext

In this example, you’ll see how to compose a long-running console application with a scoped DbContext Dependency. This console application is a variation of the UpdateCurrency program we discussed in section 7.1.

Just as with the UpdateCurrency program, this new console application reads currency exchange rates. The goal of this version, however, is to output the exchange rates of a particular currency amount once a minute and to continue to do so until the user stops the application. Figure 8.10 outlines the application’s main classes.

08-10.eps

Figure 8.10 The class diagram of the CurrencyMonitoring program

The CurrencyMonitoring program reuses the SqlExchangeRateProvider and CommerceContext from the UpdateCurrency program of chapter 7 and the ICurrencyConverter Abstraction from chapter 4. The ICurrencyRepository Abstraction and its accompanying SqlCurrencyRepository implementation are new. The CurrencyRateDisplayer is also new and is specific to this program; it’s shown in the following listing.

Listing 8.12 The CurrencyRateDisplayer class

public class CurrencyRateDisplayer
{
    private readonly ICurrencyRepository repository;
    private readonly ICurrencyConverter converter;

    public CurrencyRateDisplayer(
        ICurrencyRepository repository,
        ICurrencyConverter converter)
    {
        this.repository = repository;
        this.converter = converter;
    }

    public void DisplayRatesFor(Money amount)
    {
        Console.WriteLine(
            "Exchange rates for {0} at {1}:",
            amount,
            DateTime.Now);

        IEnumerable<Currency> currencies =
            this.repository.GetAllCurrencies();    ①  

        foreach (Currency target in currencies)
        {
            Money rate = this.converter.Exchange(    ②  
                amount,    ②  
                target);    ②  

            Console.WriteLine(rate);    ③  
        }
    }
}

You can run the application from the command line using "EUR 1.00" as argument. Doing so outputs the following text:

Exchange rates for EUR 1.00000 at 12/10/2018 22:55:00.
CAD 1.48864
USD 1.13636
DKK 7.46591
EUR 1.00000
GBP 0.89773

To piece the application together, you need to create the application’s Composition Root. The Composition Root, in this case, consists of two classes, as shown in figure 8.11.

08-11.eps

Figure 8.11 The application’s infrastructure consists of two classes, Program and Composer.

The Program class uses the Composer class to resolve the application’s object graph. Listing 8.13 shows the Composer class with its CreateRateDisplayer method. It ensures that for each resolve, only one instance of the scoped CommerceContext Dependency is created.

Listing 8.13 The Composer class, responsible for composing object graphs

public class Composer
{
    private readonly string connectionString;    ①  

    public Composer(string connectionString)
    {
        this.connectionString = connectionString;
    }

    public CurrencyRateDisplayer CreateRateDisplayer()    ②  
    {
        var context =    ③  
            new CommerceContext(this.connectionString);  ③  

        return new CurrencyRateDisplayer(
            new SqlCurrencyRepository(
                context),    ④  
            new CurrencyConverter(
                new SqlExchangeRateProvider(
                    context)));    ④  
    }
}

The remaining part of the Composition Root is the application’s entry point: the Program class. It’s responsible for reading the input arguments and configuration file, and setting up the Timer that runs once a minute to display exchange rates. The following listing shows it in full glory.

Listing 8.14 The application’s entry point that manages scopes

public static class Program
{
    private static Composer composer;

    public static void Main(string[] args)
    {
        var money = new Money(    ①  
            currency: new Currency(code: args[0]),    ①  
            amount: decimal.Parse(args[1]));    ①  

        composer = new Composer(LoadConnectionString());

        var timer = new Timer(interval: 60000);    ②  
        timer.Elapsed += (s, e) => DisplayRates(money);  ②  
        timer.Start();    ②  

        Console.WriteLine("Press any key to exit.");
        Console.ReadLine();    ③  
    }

    private static void DisplayRates(Money money)
    {
        CurrencyRateDisplayer displayer =
            composer.CreateRateDisplayer();    ④  

        displayer.DisplayRatesFor(money);
    }

    private static string LoadConnectionString() { ... }
}

The Program class configures a Timer that calls the DisplayRates method when it elapses. Even though you only call DisplayRates once per minute, in this example, you could easily call DisplayRates in parallel over multiple threads or even make DisplayRates asynchronous. This would still work because each call creates and manages its set of scoped instances, allowing each operation to run in isolation from the others.

Whereas a Transient Lifestyle implies that every consumer receives a private instance of a Dependency, a Scoped Lifestyle ensures that all consumers of all resolved graphs for that scope get the same instance. Besides common Lifestyle patterns, such as Singleton, Transient, and Scoped, there are also patterns that you can define as code smells or even anti-patterns. A few of those bad Lifestyle choices are discussed in the following section.

8.4 Bad Lifestyle choices

As we all know, some lifestyle choices are bad for our heath, smoking being one of them. The same holds true when it comes to applying Lifestyles in DI. You can make many mistakes. In this section, we discuss the choices shown in table 8.2.

Table 8.2 Bad Lifestyle choices covered in this section
SubjectTypeDescription
Captive DependenciesBugKeeps Dependencies referenced beyond their expected lifetime
Leaky AbstractionsDesign issueUses Leaky Abstractions, leaking Lifestyle choices to consumers
Per-thread LifestyleBugCauses concurrency bugs by tying instances to the lifetime of a thread

As table 8.2 states, Captive Dependencies and the per-thread Lifestyle can cause bugs in your application. More often than not, these bugs only appear after deploying the application to production, because they are concurrency related. When we start the application, as developers, we typically run it for a short period of time, one request at a time. The same holds true for testers that typically go through the application in an orderly fashion. This might hide such problems, which only pop up when multiple users access the application concurrently.

When we leak details of our Lifestyle choices to our consumers, this typically won’t lead to bugs — or at least, not immediately. It does, however, complicate the Dependency’s consumers and their tests, and might cause sweeping changes throughout the code base. In the end, this increases the chance of bugs.

8.4.1 Captive Dependencies

When it comes to lifetime management, a common pitfall is that of Captive Dependencies. This happens when a Dependency is kept alive by a consumer for longer than you intended it to be. This might even cause it to be reused by multiple threads or requests concurrently, even though the Dependency isn’t thread-safe.

An all-too-common example of a Captive Dependency is when a short-lived Dependency is injected into a Singleton consumer. A Singleton is kept alive for the lifetime of the Composer, and so will its Dependency. The following listing illustrates this problem.

bad.tif

Listing 8.15 Captive Dependency example

public class Composer
{
    private readonly IProductRepository repository;

    public Composer(string connectionString)
    {
        this.repository = new SqlProductRepository(    ①  
            new CommerceContext(connectionString));    ②  
    }
    ...
}

Because there’s only one instance of SqlProductRepository for the entire application, and CommerceContext is referenced by SqlProductRepository in its private field, there will be effectively just one instance of CommerceContext too. This is a problem, because CommerceContext isn’t thread-safe and isn’t intended to outlive a single request. Because CommerceContext is kept captive by SqlProductRepository past its expected release time, we call CommerceContext a Captive Dependency.

Captive Dependencies are a common problem when you’re working with a DI Container. This is caused by the dynamic nature of DI Containers that make it easy to lose track of the shape of the object graphs you’re building. As the previous example showed, however, the problem can also arise when working with Pure DI. By carefully structuring code in the Pure DI Composition Root, you can reduce the chance of running into this problem. The following listing shows an example of this approach.

Listing 8.16 Mitigating Captive Dependencies with Pure DI

public class CommerceControllerActivator : IControllerActivator
{
    private readonly string connStr;    ①  
    private readonly IUserContext userContext;    ①  

    public CommerceControllerActivator(string connectionString)
    {
        this.connStr = connectionString;    ②  
        this.userContext =    ②  
            new AspNetUserContextAdapter();    ②  
    }

    public object Create(ControllerContext ctx)
    {
        var context = new CommerceContext(this.connStr);    ③  
        var provider = new SqlExchangeRateProvider(context);    ③  
    ③  

        Type type = ctx.ActionDescriptor
            .ControllerTypeInfo.AsType();

        if (type == typeof(HomeController))
        {
            return this.CreateHomeController(context);    ④  
        }
        else if (type == typeof(ExchangeController))
        {
            return this.CreateExchangeController(
                context, provider);    ④  
        }
        else
        {
            throw new Exception("Unknown controller " + type.Name);
        }
    }

    private HomeController CreateHomeController(
        CommerceContext context)
    {
        return new HomeController(    ⑤  
            new ProductService(    ⑤  
                new SqlProductRepository(    ⑤  
                    context),    ⑤  
                this.userContext));    ⑤  
    }

    private RouteController CreateExchangeController(
        CommerceContext context,
        IExchangeRateProvider provider) { ... }
}

Listing 8.16 separates the creation of all Dependencies into three distinct phases. When you separate these phases, it becomes much easier to detect and prevent Captive Dependencies. These phases are

  • Singletons created during application start-up
  • Scoped instances created at the start of a request
  • Based on the request, a particular object graph that consists of Transient, Scoped, and Singleton instances

With this model, all the application’s Scoped Dependencies are created for each request, even when they aren’t used. This might seem inefficient, but remember that, as we discussed in section 4.2.2, component constructors should be free from all logic except guard checks and when storing incoming Dependencies. This makes construction fast and prevents most performance issues; the creation of a few unused Dependencies is a non-issue.

From a misconfiguration perspective, Captive Dependencies are one of the most common, hardest-to-spot configurations or programming errors related to bad Lifestyle choices. More often than we’d like to admit, we’ve wasted many hours trying to find bugs caused by Captive Dependencies. That’s why we consider tool support for spotting Captive Dependencies invaluable when you’re using a DI Container. Although Captive Dependencies are typically caused by configuration or programming errors, other inconvenient Lifestyle choices are design flaws, such as when you’re forcing Lifestyle choices on consumers.

8.4.2 Using Leaky Abstractions to leak Lifestyle choices to consumers

Another case where you might end up with a bad Lifestyle choice is when you need to postpone the creation of a Dependency. When you have a Dependency that’s rarely needed and is costly to create, you might prefer to create such an instance on the fly, after the object graph is composed. This is a valid concern. What isn’t, however, is pushing such a concern on to the Dependency’s consumers. If you do this, you’re leaking details about the implementation and implementation choices of the Composition Root to the consumer. The Dependency becomes a Leaky Abstraction, and you’re violating the Dependency Inversion Principle.

In this section, we’ll show two common examples of how you can cause your Lifestyle choice to be leaked to a Dependency’s consumer. Both examples have the same solution: create a wrapper class that hides the Lifestyle choice and functions as an implementation of the original Abstraction rather than the Leaky Abstraction.

Lazy<T> as a Leaky Abstraction

Let’s again return to our regularly reused ProductService example that was first introduced in listing 3.9. Let’s imagine that one of its Dependencies is costly to create, and not all code paths in the application require its existence.

This is something you might be tempted to solve by using .NET’s System.Lazy<T> class. A Lazy<T> allows access to an underlying value through its Value property. That value, however, will only be created when it’s requested for the first time. After that, the Lazy<T> caches the value for as long as the Lazy<T> instance exists.

This is useful, because it allows you to delay the creation of Dependencies. It’s an error, however, to inject Lazy<T> directly into a consumer’s constructor, as we’ll discuss later. The next listing shows an example of such an erroneous use of Lazy<T>.

bad.tif

Listing 8.17 Lazy<T> as Leaky Abstraction

public class ProductService : IProductService
{
    private readonly IProductRepository repository;
    private readonly Lazy<IUserContext> userContext;   

    public ProductService(
        IProductRepository repository,
        Lazy<IUserContext> userContext)  ①      
    {
        this.repository = repository;
        this.userContext = userContext;
    }

    public IEnumerable<DiscountedProduct> GetFeaturedProducts()
    {
        return
            from product in this.repository
                .GetFeaturedProducts()
            select product.ApplyDiscountFor(
                this.userContext.Value);    ②  
    }
}

Listing 8.18 shows the structure of the Composition Root for the ProductService of listing 8.17.

bad.tif

Listing 8.18 Composing a ProductService that depends on Lazy<IUserContext>

Lazy<IUserContext> lazyUserContext =
    new Lazy<IUserContext>(    ①  
        () => new AspNetUserContextAdapter())

new HomeController(
    new ProductService(
        new SqlProductRepository(
            new CommerceContext(connectionString)),
        lazyUserContext));    ②  

After seeing this code, you might wonder what’s so bad about it. The following discussion lists several problems with such a design, but it’s important that you know there’s nothing wrong with the use of Lazy<T> inside your Composition Root — injecting Lazy<T> into an application component, however, leads to Leaky Abstractions. Now, back to the problems.

First, letting a consumer depend on Lazy<IUserContext> complicates the consumer and its unit tests. You might think that having to call userContext.Value is a small price to pay for being able to lazy load an expensive Dependency, but it isn’t. When creating unit tests, not only do you have to create Lazy<T> instances that wrap the original Dependency, but you also have to write extra tests to verify whether that Value isn’t being called at the wrong time.

Because making the Dependency lazy seems important enough as a performance optimization, it would be weird not to verify whether you implemented it correctly. This is, at least, one extra test you need to write for every consumer of that Dependency. There might be dozens of consumers for such a Dependency, and they all need the extra tests to verify their correctness.

Second, changing an existing Dependency to a lazy Dependency later in the development process causes sweeping changes throughout the application. This can present a serious amount of effort when there are dozens of consumers for that Dependency, because, as discussed in the previous point, not only do the consumers themselves need to be altered, but all of their tests need to be changed too. Making these kinds of rippling changes is time consuming and risky.

To prevent this, you could make all Dependencies lazy by default, because, in theory, every Dependency could potentially become expensive in the future. This would prevent you from having to make any future cascading changes. But this would be madness, and we hope you agree that this isn’t a good path to pursue. This is especially true if you consider that every Dependency could potentially become a list of implementations, as we’ll discuss shortly. This would lead to making all Dependencies IEnumerable<Lazy<T>> by default, which would be, even more so, insane.

Last, because the amount of changes you have to make and the number of tests you need to add, it becomes quite easy to make programming mistakes that would completely nullify these changes. For instance, if you create a new component that accidentally depends on IUserContext instead of Lazy<IUserContext>, it means that every graph that contains that component will always get an eagerly loaded IUserContext implementation.

This doesn’t mean that you aren’t allowed to construct your Dependencies lazily, though. We’d like, however, to repeat our statement from section 4.2.1: you should keep the constructors of your components free of any logic other than Guard Clauses and the storing of incoming Dependencies. This makes the construction of your classes fast and reliable, and will prevent such components from ever becoming expensive to instantiate.

In some cases, however, you’ll have no choice; for instance, when dealing with third-party components you have little control over. In that case, Lazy<T> is a great tool. But rather than letting all consumers depend on Lazy<T>, you should hide Lazy<T> behind a Virtual Proxy and place that Virtual Proxy within the Composition Root.11  The following listing provides an example of this.

good.tif

Listing 8.19 Virtual Proxy wrapping Lazy<T>

public class LazyUserContextProxy : IUserContext    ①  
{
    private readonly Lazy<IUserContext> userContext;    ②  

    public LazyUserContextProxy(
        Lazy<IUserContext> userContext)
    {
        this.userContext = userContext;
    }

    public bool IsInRole(Role role)
    {
        IUserContext real = this.userContext.Value;    ③  
        return real.IsInRole(role);    ③  
    }
}

This new LazyUserContextProxy allows ProductService to dependent on IUserContext instead of Lazy<IUserContext>. Here’s ProductService’s new constructor:

public ProductService(
    IProductRepository repository,
    IUserContext userContext)

The next listing shows how you can compose the object graph for HomeController while injecting LazyUserContextProxy into ProductService.

good.tif

Listing 8.20 Composing a ProductService by injecting a Virtual Proxy

IUserContext lazyProxy =
    new LazyUserContextProxy(    ①  
        new Lazy<IUserContext>(    ①  
            () => new AspNetUserContextAdapter()));    ①  

new HomeController(
    new ProductService(
        new SqlProductRepository(
            new CommerceContext(connectionString)),
        lazyProxy));    ②  

As listing 8.19 shows, it’s not a bad thing per se to have a class depending on Lazy<T>, but you want to centralize this inside the Composition Root and only have a single class that takes this dependency on Lazy<IUserContext>. Depending on Func<T> has practically the same effect as depending on Lazy<T>, and the solution is similar. Doing so prevents your code from being complicated, unit tests from being added, sweeping changes from being made, and unfortunate bugs from being introduced. As you’ll see next, the same arguments hold for injecting IEnumerable<T> too.

IEnumerable<T> as a Leaky Abstraction

Just as with using Lazy<T> to delay the creation of Dependencies, there are many cases where you need to work with a collection of Dependencies of a certain Abstraction. For this purpose, you can make use of one of the BCL collection Abstractions, such as IEnumerable<T>. Although, in itself, there’s nothing wrong with using IEnumerable<T> as an Abstraction to present a collection of Dependencies, using it in the wrong place can, once again, lead to a Leaky Abstraction. The following listing shows how IEnumerable<T> can be used incorrectly.

bad.tif

Listing 8.21 IEnumerable<T> as a Leaky Abstraction

public class Component
{
    private readonly IEnumerable<ILogger> loggers;

    public Component(IEnumerable<ILogger> loggers)    ①  
    {
        this.loggers = loggers;
    }

    public void DoSomething()
    {
        foreach (var logger in this.loggers)    ②  
        {
            logger.Log("DoSomething called");
        }

        ...
    }
}

We’d like to prevent consumers from having to deal with the fact that there might be multiple instances of a certain Dependency. This is an implementation detail that’s leaking out through the IEnumerable<ILogger> Dependency. As we explained previously, every Dependency could potentially have multiple implementations, but your consumers shouldn’t need to be aware of this. Just as with the previous Lazy<T> example, this leakage increases the system’s complexity and maintenance costs when you have multiple consumers of such a Dependency, because every consumer has to deal with looping over the collection. So do consumer’s tests.

Although experienced developers spit out foreach constructs like this in a matter of seconds, things get more complicated when the collection of Dependencies needs to be processed differently. For example, let’s say that logging should continue even if one of the loggers fails:

foreach (var logger in this.loggers)
{
    try
    {
        logger.Log("DoSomething called");
    }
    catch    ①  
    {    ①  
    }    ①  
}

Or, perhaps you not only want to continue processing, but also log that error to the next logger. This way, the next logger functions as a fallback for the failed logger:

for (int index = 0; index < this.loggers.Count; index++)
{
    try
    {
        this.loggers[index].Log("DoSomething called");    ②  
    }
    catch (Exception ex)
    {
        if (loggers.Count > index + 1)
        {
            loggers[index + 1].Log(ex);    ③  
        }
    }
}

Or perhaps — well, we think you get the idea. It’d be rather painful to have these kinds of code constructs all over the place. If you want to change your logging strategy, it causes you to make cascading changes throughout the application. Ideally, we’d like to centralize this knowledge to one single location.

You can fix this design problem using the Composite design pattern. You should be familiar with the Composite design pattern by now, as we’ve discussed it in chapters 1 and 6 (see figure 1.8, and listings 6.4 and 6.12). The next listing shows a Composite for ILogger.

good.tif

Listing 8.22 Composite wrapping IEnumerable<T>

public class CompositeLogger : ILogger    ①  
{
    private readonly IList<ILogger> loggers;    ②  

    public CompositeLogger(IList<ILogger> loggers)
    {
        this.loggers = loggers;
    }

    public void Log(LogEntry entry)    ③  
    {
        for (int index = 0; index < this.loggers.Count; index++)
        {
            try
            {
                this.loggers[index].Log(entry);
            }
            catch (Exception ex)
            {
                if (loggers.Count > index + 1)
                {
                    var logger = loggers[index + 1];
                    logger.Log(new LogEntry(ex));    ④  
                }
            }
        }
    }
}

The following snippet shows how you can compose the object graph for Component using this new CompositeLogger, keeping Component dependent on a single ILogger instead of an IEnumerable<ILogger>:

good.tif
ILogger composite =
    new CompositeLogger(new ILogger[]    ①  
    {
        new SqlLogger(connectionString),
        new WindowsEventLogLogger(source: "MyApp"),
        new FileLogger(directory: "c:\logs")
    });

new Component(composite);    ②  

As you’ve seen many times before, good application design follows the Dependency Inversion Principle and prevents Leaky Abstractions. This results in cleaner code that’s more maintainable and more resilient to programming errors. Let’s now look at a different smell, which doesn’t affect the application’s design per se, but potentially causes hard-to-fix concurrency problems.

8.4.3 Causing concurrency bugs by tying instances to the lifetime of a thread

Sometimes you’re dealing with Dependencies that aren’t thread-safe but don’t necessarily need to be tied to the lifetime of a request. A tempting solution is to synchronizing the lifetime of such a Dependency to the lifetime of a thread. Although seductive, such practice is error prone.

Listing 8.23 shows how the CreateCurrencyParser method, previously discussed in listing 7.2, makes use of a SqlExchangeRateProvider Dependency. This is created once for each thread in the application.

bad.tif

Listing 8.23 A Dependency’s lifetime tied to the lifetime of a thread

[ThreadStatic]    ①  
private static CommerceContext context;    ①  

static CurrencyParser CreateCurrencyParser(
    string connectionString)
{
    if (context == null)    ②  
    {    ②  
        context = new CommerceContext(    ②  
           connectionString);    ②  
    }    ②  

    return new CurrencyParser(    ③  
        new SqlExchangeRateProvider(context),    ③  
        context);    ③  
}

Although this might look innocent, that couldn’t be further from the truth. We’ll discuss two problems with this listing next.

The lifetime of a thread is often unclear

It can be hard to predict what the lifespan of a thread is. When you create and start a thread using new Thread().Start(), you’ll get a fresh block of thread-static memory. This means that if you call CreateCurrencyParser in such a thread, the thread-static fields will all be unset, resulting in new instances being created.

When starting threads from the thread pool using ThreadPool.QueueUserWorkItem, however, you’ll possibly get an existing thread from the pool or a newly created thread, depending on what’s in the thread pool. Even if you aren’t creating threads yourself, the framework might be (as we’ve discussed regarding, for example, ASP.NET Core). This means that while some threads have a lifetime that’s rather short, others live for the duration of the entire application. Further complications arise when operations aren’t guaranteed to run on a single thread.

Asynchronous application models cause multi-threading issues

Modern application frameworks are inherently asynchronous in nature. Even though your code might not implement the new asynchronous programming patterns using the async and await keywords, the framework you’re using might still decide to finish a request on a different thread than it was started on. ASP.NET Core is, for instance, completely built around this asynchronous programming model. But even older frameworks, such as ASP.NET Web API and ASP.NET Web Forms, allow requests to run asynchronously.

This is a problem for Dependencies that are tied to a particular thread. When a request continues on a different thread, it still references the same Dependencies, even though some of them are tied to the original thread. Figure 8.12 illustrates this.

08-12.eps

Figure 8.12 Thread-specific Dependencies can cause concurrency bugs in asynchronous environments.

Using thread-specific Dependencies while running in an asynchronous context is a particularly bad idea, because it could lead to concurrency problems, which are typically hard to find and reproduce. Such a problem would only occur if the thread-specific Dependency isn’t thread-safe — they typically aren’t. Otherwise, the Singleton Lifestyle would have worked just fine.

The solution to this problem is to scope things around a request or operation, and there are several ways to achieve this. Instead of linking the lifetime of the Dependency to that of a thread, make its lifetime scoped to the request, as discussed in section 8.3.3. The following listing demonstrates this once more.

good.tif

Listing 8.24 Storing Scoped Dependencies in local variables

static CurrencyParser CreateCurrencyParser(
    string connectionString)
{
    var context = new CommerceContext(    ①  
        connectionString);    ①  

    return new CurrencyParser(    ②  
        new SqlExchangeRateProvider(context),    ②  
        context);    ②  
}

The Lifestyles examined in this chapter represent the most common types, but you may have more exotic needs that aren’t satisfactorily addressed. When we find ourselves in such a situation, our immediate response should be to realize that our approach must be wrong, and if we change our design a bit, everything will fit nicely into standard patterns.

This realization is often a disappointment, but it leads to better and more maintainable code. The point is that if you feel the need to implement a custom Lifestyle or create a Leaky Abstraction, you should first seriously reconsider your design. For this reason, we decided to leave specialized Lifestyles out of this book. We can often handle such situations better with a redesign or Interception, as you’ll see in the next chapter.

Summary

  • Composer is a unifying term, referring to any object or method that composes Dependencies. It’s an important part of the Composition Root.
  • The Composer can be a DI Container, but it can also be any method that constructs object graphs manually using Pure DI.
  • The Composer has a greater degree of influence over the lifetime of Dependencies than any single consumer can have. The Composer decides when instances are created, and, by its choice of whether to share instances, it determines whether a Dependency goes out of scope with a single consumer or whether all consumers must go out of scope before the Dependency can be released.
  • A Lifestyle is a formalized way of describing the intended lifetime of a Dependency.
  • The ability to fine tune each Dependency’s Lifestyle is important for performance reasons but can also be important for correct behavior. Some Dependencies must be shared between several consumers for the system to work correctly.
  • The Liskov Substitution Principle states that you must be able to substitute the Abstraction for an arbitrary implementation without changing the correctness of the system.
  • Failing to adhere to the Liskov Substitution Principle makes applications fragile, because it disallows replacing Dependencies that might cause a consumer to break.
  • An ephemeral disposable is an object with a clear and short lifetime that typically doesn’t exceed a single method call.
  • Diligently work to implement services so they don’t hold references to disposables, but rather create and dispose of them on demand. This makes memory management simpler, because the service can be garbage collected like other objects.
  • The responsibility of disposing of Dependencies falls to the Composer. It, better than anything else, knows when it creates a disposable instance, so it also knows that the instance needs to be disposed of.
  • Releasing is the process of determining which Dependencies can be dereferenced and (possibly) disposed of. The Composition Root signals the Composer to release a resolved Dependency.
  • A Composer must take care of the correct order of disposal for objects. An object might require its Dependencies to be called during disposal, which causes problems if these Dependencies are already disposed of. Disposal should, therefore, happen in the opposite order of object creation.
  • The Transient Lifestyle involves returning a new instance every time it’s requested. Each consumer gets its own instance of the Dependency.
  • Within the scope of a single Composer, there’ll only be one instance of a component with the Singleton Lifestyle. Each time a consumer requests the component, the same instance is served.
  • Scoped Dependencies behave like singletons within a single, well-defined scope or request, but aren’t shared across scopes. Each scope has its own set of associated Dependencies.
  • The Scoped Lifestyle makes sense for long-running applications that are tasked with processing operations that need to run in some degree of isolation. Isolation is required when these operations are processed in parallel, or when each operation contains its own state.
  • If you ever need to compose an Entity Framework Core DbContext in a web request, a Scoped Lifestyle is an excellent choice. DbContext instances aren’t thread-safe, but you typically only want one DbContext instance per web request.
  • Object graphs can consist of Dependencies of different Lifestyles, but you should make sure that a consumer only has Dependencies with a lifetime that’s equal to or exceeds its own, because a consumer will keep its Dependencies alive. Failing to do so leads to Captive Dependencies.
  • A Captive Dependency is a Dependency that’s inadvertently kept alive for too long, because its consumer was given a lifetime that exceeds the Dependency’s expected lifetime.
  • Captive Dependencies are a common source of bugs when working with a DI Container, although the problem can also arise when working with Pure DI.
  • When applying Pure DI, a careful structure of the Composition Root can reduce the chance of running into problems.
  • When working with a DI Container, Captive Dependencies are such a widespread problem that some DI Containers perform analysis on constructed object graphs to detect them.
  • Sometimes you need to postpone the creation of a Dependency. Injecting the Dependency as a Lazy<T>, Func<T>, or IEnumerable<T>, however, is a bad idea because it causes the Dependency to become a Leaky Abstraction. Instead, you should hide this knowledge behind a Proxy or Composite.
  • Don’t bind the lifetime of a Dependency to the lifetime of a thread. The lifetime of a thread is often unclear, and using it in an asynchronous framework can cause multi-threading issues. Instead, use a proper Scoped Lifestyle or hide access to the thread-static value behind a Proxy.
..................Content has been hidden....................

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