In this chapter
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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:
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?
For the sake of argument, imagine that you have a disposable Abstraction like the following IOrderRepository
interface.
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.
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.
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();
}
You ask the injected Repository to delete the product by supplying the product ID. EditProductViewModel can safely hold a reference to the Repository because the IProductRepository interface doesn’t derive from IDisposable.
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 Delete method creates a WCF channel, which is an ephemeral disposable. The channel is both created and disposed of within the same method call.
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.
Warning Although the using
statement is a best practice when it comes to working with short-lived disposables, this doesn’t hold true when it comes to WCF. Against all guidelines, WCF proxy classes can throw exceptions when calling Dispose
. This causes you to lose the original exception information, in case an exception was thrown within the using
block. Instead of relying on a using
statement, you must write a finally
block and ignore any exceptions thrown by Dispose
. We only referenced using
here to demonstrate the general concept of implementing an ephemeral disposable.6
6 For more information, see https://mng.bz/5Y6z.
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.
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.
Tip Strive to implement services so they don’t hold references to disposables, but rather create and dispose of them on demand as illustrated in listing 8.6. This makes memory management simpler because the service can be garbage collected like other objects.
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 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.
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.
Warning An object might require its Dependencies to be called during disposal, which causes problems when these Dependencies are already disposed of. Disposal should therefore happen in the opposite order of creation — this means from the outside in.
CommerceContext
is our project-specific version of Entity Framework Core’s DbContext
, which implements IDisposable
. In the past, we’ve witnessed many discussions with colleagues and developers on online forums about the need to dispose of DbContext
instances. These discussions typically came from the observation that a DbContext
uses database connections as ephemeral disposables; connections are opened and closed in the same method call. Calling SaveChanges
on DbContext
, for instance, creates and opens a database connection, and then disposes of that connection once all changes are saved.
Well, things have changed in Entity Framework Core 2.0. With the introduction of version 2, it now supports DbContext
pooling, a feature similar to ADO.NET’s connection pooling. It allows the same DbContext
instance to be reused, which can improve application performance under certain conditions. DbContext
instances, however, are returned back to their pool when Dispose
is called, so not calling Dispose
on a DbContext
instance might starve the pool.
The moral of this story is that you should always make sure disposable objects are correctly disposed of. Even if you determined that you could omit a call to Dispose
in your specific case, an external component, such as Entity Framework Core, is free to change that behavior any time in the future.
Note To learn about Entity Framework Core in detail, the book Entity Framework Core in Action by Jon Smith (Manning, 2018) is a good place to start.
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); ③
}
Creates the instance that requires disposal
Tracks that instance by associating it with the current request
The TrackDisposable method stores disposable instances in a list that’s associated with the request by storing it in the HttpContext.Items dictionary. If the list doesn’t exist, it’ll be created. The disposable instance is appended to the list.
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.
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(); ③
}
}
}
Gets the list of tracked disposables from the items dictionary
Reverses the order of the list of disposables so instances can be disposed of in the opposite order of their creation
Loops through the collection and disposes of all instances one by one
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.
Tip DI Containers are particularly good at Lifetime Management. DI Containers can deal with complex combinations of Lifestyles, and they offer opportunities, such as a Release
method, to explicitly release components when you’re finished with them. When you find yourself in the situation where maintaining your Composition Root using Pure DI becomes difficult, consider switching to a DI Container instead. (We’ll go into more detail when discussing DI Containers in chapter 12.)
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.
Note Releasing will be demanded after the Composition Root has finished using the resolved root object.
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); ③
}
Requests a CurrencyParser root object
Uses that root object
Demands its release after the operation is completed
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.
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.
Name | Description |
Singleton | A single instance is perpetually reused. |
Transient | New instances are always served. |
Scoped | At most, one instance of each type is served per an implicitly or explicitly defined scope. |
Note We use comparable examples throughout this section. But to allow us to focus on the essentials, we’ll compose shallow hierarchies, and we’ll sometimes ignore the issue with disposable Dependencies to avoid that added complexity.
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.
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.
Note Within the scope of a single Composer, there’ll only be one instance of a component with the Singleton Lifestyle. Each and every time a consumer requests the component, the same instance is served.
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.
Use the Singleton Lifestyle whenever possible. Two main issues that might prevent you from using a Singleton follow:
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.
7 Michael T. Nygard, Release It! Design and Deploy Production-Ready Software (Pragmatic Bookshelf, 2007), 104.
Let’s take a closer look at an in-memory Repository. We’ll explore an example of this next.
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.
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)); ③
}
}
Storage locations for Singleton instances keep the Singleton Dependencies referenced for the lifetime of the Composer.
Creates Singletons in the Composer’s constructor
Every time the Composer is asked to resolve a HomeController instance, it creates a Transient ProductService with the two Singletons injected into it.
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.
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.
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.
Warning When it comes to the Transient Lifestyle, be aware that DI Containers can behave differently. Although some DI Containers track Transient components and tend to dispose of them when their consumer goes out of scope, others don’t and, therefore, the Transients aren’t disposed.
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.
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.
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.
Warning Although you can mix Dependencies with different Lifestyles, 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 by storing them in its private
fields. Failing to do so leads to Captive Dependencies, which we’ll address in section 8.4.1.
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()))); ①
}
Both the ProductService and SqlUserRepository classes require an IUserContext Dependency. When the AspNetUserContextAdapter is transient, each consumer gets its own private instance, so ProductService gets one instance, and SqlUserRepository gets another.
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.
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.
8 Threads are still used sequentially, one after the other, not in parallel.
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.
Definition Scoped Dependencies behave like Singleton Dependencies within a single, well-defined scope or request but aren’t shared across scopes. Each scope has its own cache of associated Dependencies.
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.
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.
Tip 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 to have one DbContext
instance per web 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.
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.
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); ③
}
}
}
Loads all known currencies
Calculates the exchange rates for the given amount in the target currency
Prints the requested exchange rates to the console
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.
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))); ④
}
}
Storage fields for Singletons. In this case, there’s only a connection string, but a typical application will have more singleton instances.
Public method that allows the transient root type CurrencyRateDisplayer to be composed
Creates the Scoped Dependencies
Injects the Scoped Dependencies into a transient object graph
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() { ... }
}
Creates new Money based on the incoming command-line arguments. This is the amount for which the exchange rates will be displayed.
Creates a System.Timers.Timer.9 Triggers the Timer once a minute. When an interval elapses, calls the DisplayRates method.
9 The System.Timers.Timer
class is part of .NET Standard 2.0 and .NET Core 2.0.
After the timer is started, the program waits for user input and quits when that happens.
The Composer is requested to resolve a CurrencyRateDisplayer for the current request.
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.
Note As a simplification, the previous example omitted the release of the created object graph. Listings 8.7 and 8.8 demonstrate this concept in the context of an ASP.NET Core MVC application. A solution that works with a console application would look similar, so we’ll leave that as an exercise for you.
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.
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.
Subject | Type | Description |
Captive Dependencies | Bug | Keeps Dependencies referenced beyond their expected lifetime |
Leaky Abstractions | Design issue | Uses Leaky Abstractions, leaking Lifestyle choices to consumers |
Per-thread Lifestyle | Bug | Causes 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.
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.
Definition 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.
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.
Listing 8.15 Captive Dependency example
public class Composer
{
private readonly IProductRepository repository;
public Composer(string connectionString)
{
this.repository = new SqlProductRepository( ①
new CommerceContext(connectionString)); ②
}
...
}
Creates SqlProductRepository as Singleton and stores it for reuse
Injects CommerceContext into the Singleton. CommerceContext has now become a Captive Dependency; it isn’t thread-safe and isn’t intended to be reused by multiple threads.
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.
Important A component should only reference Dependencies that have an expected lifetime that’s equal to or longer than that of the component itself.
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) { ... }
}
Storage fields for Singletons
Creates Singletons
Creates Scoped Dependencies
Supplies factory methods with the created Scoped Dependencies
Composes object graph containing Transient, Scoped, and Singleton instances
Note When working with a DI Container, the problem of Captive Dependencies is so widespread that some DI Containers will do analysis on constructed object graphs to detect them.10
10 All DI Containers covered in this book contain features for the detection of Captive Dependencies.
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
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.
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 AbstractionLet’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>
.
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); ②
}
}
Instead of depending on IUserContext, ProductService now depends on Lazy<IUserContext>. This way, the IUserContext instance is created only when it’s needed. This is bad because Lazy<IUserContext> is a Leaky Abstraction.
The Value property on Lazy<IUserContext> ensures that the IUserContext Dependency is created once. When the GetFeaturedProducts method on IProductRepository returns an empty list, the select clause is never executed, and the Value property will never get called, preventing IUserContext from being created.
Listing 8.18 shows the structure of the Composition Root for the ProductService
of listing 8.17.
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)); ②
Delays the creation of the real AspNetUserContextAdapter Dependency by wrapping its creation inside the Lazy<IUserContext>
Because ProductService now depends on Lazy<IUserContext> rather than IUserContext, you inject Lazy<IUserContext> directly into its constructor.
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.
11 For more on Virtual Proxies, see Erich Gamma et. al., Design Patterns, 208.
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); ③
}
}
Implements IUserContext
Depends on Lazy<IUserContext> to allow the IUserContext to be constructed lazily
Only when the Proxy’s IsInRole method is invoked will the real IUserContext implementation be constructed and invoked.
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
.
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)); ②
Creates the Virtual Proxy that wraps a Lazy<T>, allowing the creation of the real Dependency in a lazy fashion
Because LazyUserContextProxy implements IUserContext, you’re now able to let ProductService depend on IUserContext while injecting LazyUserContextProxy into its constructor.
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 AbstractionJust 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.
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");
}
...
}
}
Injects a collection of ILogger Dependencies
Loops through the collection and operates on them
Note We’ll ignore for a moment the advice of section 5.3.2, where we stated that you shouldn’t pollute your application’s code base with logging. Chapter 10 describes in detail how to design your application to account for Cross-Cutting Concerns.
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 ①
{ ①
} ①
}
Empty catch clause allows continuing logging in case of a failure
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); ③
}
}
}
Forwards the call to the underlying logger implementation
Forwards the exception to the fallback logger, which is the next logger, if any, in the list
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
.
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)); ④
}
}
}
}
}
CompositeLogger implements ILogger.
CompositeLogger depends on IList<ILogger> to allow forwarding the log requests to all available ILogger components.
Implements ILogger’s Log method. In this case, we assume ILogger contains one single method accepting a LogEntry method.12
12 To get an idea of why we defined ILogger
using a single Log
method, take a look at the following Stack Overflow question: https://mng.bz/QgdG.
Wraps the exception thrown by the failed logger in a LogEntry object so it can be passed on to the fallback logger
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>
:
ILogger composite =
new CompositeLogger(new ILogger[] ①
{
new SqlLogger(connectionString),
new WindowsEventLogLogger(source: "MyApp"),
new FileLogger(directory: "c:\logs")
});
new Component(composite); ②
Constructs a Composite with multiple ILogger implementations
Constructs the new Component using the 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.
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.
Warning Some DI Containers refer to this method as the per-thread Lifestyle and have built-in support for it — avoid this!
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.
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); ③
}
A static field is marked with the [ThreadStatic] attribute. The CLR ensures that such a field isn’t shared between threads, instead providing each executing thread a separate instance of the field. If the field is accessed on a different thread, it’ll contain a different value.
In case the thread on which the current code is executing doesn’t have an initialized CommerceContext yet, a new instance is created and stored in the corresponding thread-static field.
Injects the per-thread Dependency into a transient object graph
Although this might look innocent, that couldn’t be further from the truth. We’ll discuss two problems with this listing next.
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.
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.
Note The object graph of Request 1 in figure 8.12 moves from one thread to another, although the Dependency is thread specific. The Dependency effectively becomes a Captive Dependency once the object graph is moved to another thread.
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.
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); ②
}
Creates the Scoped Dependency
Injects the Scoped Dependency into a transient object graph
Note The given solution could reduce the lifetime of the Dependency considerably. This typically won’t be a problem, but if it is, consider pooling the Dependency, or wrap access to a thread-static Dependency behind a proxy, which only accesses the Dependency from within its method. This prevents the Dependency from being accidentally moved from thread to thread. We’ll leave this as an exercise for the reader.
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.
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.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.3.147.70.247