on
9-minute read
The Ambient Composition Model
To be able to achieve anything useful, your application code makes use of runtime data that comes in many shapes and forms. Providing access to that data can be accomplished in many ways. The way you provide object graphs with runtime data can affect the way you compose them using Dependency Injection. There are two competing models to choose from. This article describes the less common model: the Ambient Composition Model. This article is the third of a five-part series on Dependency Injection composition models.
Posts in this series:
- DI Composition Models: A Primer
- The Closure Composition Model
- The Ambient Composition Model (this article)
- DI Composition Models: A Comparison
- In Praise of the Singleton Object Graph
The goal of this article is to objectively describe the Ambient Composition Model by providing examples to highlight the difference between it and the Closure Composition Model (CCM). In the fourth part, I’ll discuss the respective advantages and disadvantages of both models.
Let’s continue the example of the hypothetical web shop with its ShoppingBasketController
and ShoppingBasketRepository
, which I introduced in the previous articles. The following example shows the construction of the ShoppingBasketController
’s object graph once more:
new ShoppingBasketController(
new AddShoppingBasketItemHandler(
new ShoppingBasketRepository(...)));
Let’s assume for a moment that the web application’s basket feature requires the user’s identity—not an unusual assumption.
Perhaps it is AddShoppingBasketItemHandler
that requires access to the user’s identity. The following example shows the updated ShoppingBasketController
object graph. This time, AddShoppingBasketItemHandler
depends on an IUserContext
abstraction, implemented by an AspNetUserContextAdapter
:
new ShoppingBasketController(
new AddShoppingBasketItemHandler(
new AspNetUserContextAdapter(),
new ShoppingBasketRepository(...)));
AddShoppingBasketItemHandler
’s Handle
method can use the supplied IUserContext
dependency to load the current user’s shopping basket:
public void Handle(AddShoppingBasketItem command)
{
var basket = this.repository.GetBasket(this.userContext.UserName)
?? new ShoppingBasket(this.userContext.UserName);
basket.AddItem(new ShoppingBasketItem(
productId: command.ProductId,
amount: command.Amount));
this.repository.Save(basket);
}
Inside your Composition Root you can define this ASP.NET-specific IUserContext
adapter as follows:
class AspNetUserContextAdapter : IUserContext
{
public string UserName => HttpContext.Current.User.Identity.Name;
}
Notice that this implementation does not require the data to be provided to the class through its constructor or a property, as you would do when applying the CCM. Instead, it makes use of the static HttpContext.Current
property, which returns the web request’s current HttpContext
object. By means of that HttpContext
instance, the current username is retrieved.
The HttpContext
instance is provided to the adapter as ambient data. This means that the returned data is local to the current operation. In this case, the HttpContext.Current
property “knows” in which “operation” it is running and will automatically return the correct instance for the current web request.
This stateless AspNetUserContextAdapter
is a demonstration of the Ambient Composition Model (ACM).
The following figure captures the essence of the ACM.
The following sequence diagram shows the basic flow of data using the ACM.
The previous example used ASP.NET (classic) to demonstrate the ACM. Although the implementation will be a bit different, you can use this model in a similar fashion with ASP.NET Core, as I’ll show next.
Using the Ambient Composition Model in ASP.NET Core
When building an ASP.NET Core application, your adapter should be designed differently, but the idea is identical:
class AspNetCoreUserContextAdapter : IUserContext
{
private readonly IHttpContextAccessor accessor;
public AspNetCoreUserContextAdapter(IHttpContextAccessor accessor)
{
this.accessor = accessor;
}
public string UseName => this.accessor.HttpContext.User.Identity.Name;
}
In this case, the IUserContext
implementation depends on ASP.NET Core’s IHttpContextAccessor abstraction to provide access to the web request’s current HttpContext
. ASP.NET Core uses a single instance for IHttpContextAccessor
, which internally stores HttpContext
as ambient data using an AsyncLocal<T>
field. The effect of IHttpContextAccessor
is identical to ASP.NET classic’s HttpContext.Current
.
In both examples, runtime data is store outside the graph. This absence of a captured variable allows classes to be reused and even registered with the Singleton Lifestyle. This might even allow the adapter’s consumers (for example, AddShoppingBasketItemHandler
) to become singletons as well.
You must be careful, though, not to let your application components directly depend on ambient data.
Encapsulation of ambient data
Some developers might frown on the idea of using ambient data, but as long as its usage is encapsulated inside the Composition Root, it is perfectly fine. A Composition Root is not reused, but instead specific to one particular application, and the Composition Root knows best how data can be shared across its components.
You should, however, prevent the use of ambient state outside the Composition Root, which is one reason why you would want to hide calls to .NET’s DateTime.Now
property behind an ITimeProvider
abstraction of some sort, as shown in the next example:
class DefaultTimeProvider : ITimeProvider
{
public DateTime Now => DateTime.Now;
}
The ITimeProvider
abstraction allows consuming code to become testable. Its DefaultTimeProvider
implementation applies the ACM—the static DateTime.Now
property provides a runtime value, while the value is never stored as a captured variable inside the class. This, again, allows the class to be stateless and immutable—two interesting properties.
Although the CCM is the prevalent model, you’ll see that most applications apply a combination of both models. At the one hand, you are likely using the CCM by capturing DbContext
instances in repositories, while at the same time you’re making use of the ACM by injecting stateless IUserContext
or ITimeProvider
implementations.
But instead of using the CCM to store DbContext
instances as captured variables, as demonstrated in the previous article, you can apply the ACM, which is what I’ll demonstrate next.
Applying the Ambient Composition Model to a DbContext
Instead of supplying a ShoppingBasketDbContext
to the constructor of ShoppingBasketRepository
, you can supply an IShoppingBasketContextProvider
—much like ASP.NET Core’s IHttpContextAccessor
—that allows the repository to retrieve the correct DbContext
. The provider’s implementation would be responsible for ensuring that the same DbContext
is returned for every call within the same request—but a new one for another request. This changes ShoppingBasketRepository
to the following:
public class ShoppingBasketRepository : IShoppingBasketRepository
{
private readonly IShoppingBasketContextProvider provider;
public ShoppingBasketRepository(IShoppingBasketContextProvider provider)
{
this.provider = provider;
}
public ShoppingBasket GetById(Guid id) =>
this.provider.Context.ShoppingBaskets.Find(id)
?? throw new KeyNotFoundException(id.ToString());
}
ShoppingBasketRepository
now retrieves DbContext
from the injected IShoppingBasketContextProvider
. The provider is queried for DbContext
only when its GetById
method is called, and its value is never stored inside the repository.
A simplified version of the object graph for this altered ShoppingBasketRepository
might look like the following:
// Create Singletons
var contextProvider =
new AmbientShoppingBasketContextProvider(connectionString);
// Create Transient components
new ShoppingBasketController(
new AddShoppingBasketItemHandler(
new AspNetUserContextAdapter(),
new ShoppingBasketRepository(contextProvider)));
In this example, ShoppingBasketRepository
is injected with AmbientShoppingBasketContextProvider
, which in turn is supplied with a connection string. The following example shows AmbientShoppingBasketContextProvider
‘s code.
// This class will be part of your Composition Root
class AmbientShoppingBasketContextProvider : IShoppingBasketContextProvider
{
private readonly string connectionString;
private readonly AsyncLocal<ShoppingBasketDbContext> context;
public AmbientShoppingBasketContextProvider(string connectionString)
{
this.connectionString = connectionString;
this.context = new AsyncLocal<ShoppingBasketDbContext>();
}
public ShoppingBasketDbContext Context =>
this.context.Value ?? (this.context.Value = this.CreateNew());
private ShoppingBasketDbContext CreateNew() =>
new ShoppingBasketDbContext(this.connectionString);
}
Internally, AmbientShoppingBasketContextProvider
makes use of .NET’s AsyncLocal<T>
to ensure creation and caching of DbContext
. It provides a cache for a single asynchronous flow of operations (typically, within a request). In other words, AsyncLocal<T>
stores ambient data.
AmbientShoppingBasketContextProvider
is an adapter hiding the use of AsyncLocal<T>
from the application, preventing this implementation detail from leaking out. From the perspective of ShoppingBasketRepository
, it doesn’t know whether ambient state is involved or not. You could have transparently provided the repository with a “closure-esque” implementation.
This new graph for ShoppingBasketController
uses the ACM consistently. In this case, the DbContext
runtime data is not supplied any longer during object construction, but instead, it is created on the fly when requested the first time within a given request. The Composition Root ensures that runtime data is created and cached.
Applying the Ambient Composition Model to the user’s identity
The previous article demonstrated the CCM in the context of a queuing infrastructure. The example showed how the OrderCancellationReportGenerator
object graph was composed while injecting runtime data through the constructor. For completeness, here’s that example again:
// Composes the graph using the Closure Composition Model
IHandler<OrderCancelled> handler =
new OrderCancellationReportGenerator(
new OrderRepository(
new ClosureUserContext(
queueContext.UserName), External runtime data
new SalesDbContext(
connectionString)));
handler.Handle(queueContext.Message); External runtime data
Similar to the previous AmbientShoppingBasketContextProvider
, you can create an AmbientUserContext
implementation that replaces ClosureUserContext
as an implementation for IUserContext
:
class AmbientUserContext : IUserContext
{
public static readonly AsyncLocal<string> Name = new AsyncLocal<string>();
public string UserName =>
Name.Value ?? throw new InvalidOperationException("Not set.");
}
As part of the Composition Root, this AmbientUserContext
exposes an AsyncLocal<string>
field that allows the user’s identity to be set before the graph is used. This allows the Composition Root to be written like the following:
// Composes the graph using the Ambient Composition Model
IHandler<OrderCancelled> handler =
new OrderCancellationReportGenerator(
new OrderRepository(
new AmbientUserContext(),
new SalesDbContext(
connectionString)));
// Set the external runtime data before invoking the composed graph
AmbientUserContext.Name.Value = queueContext.UserName;
// Invoke the composed graph
handler.Handle(queueContext.Message);
In this example, it might seem weird to have AmbientUserContext
injected into the graph, while its ambient data is set directly after. But don’t forget that usually the construction of the graph is not done as close to initialization as shown here. The construction of such a graph is likely moved to another method, or done by the DI Container.
This completes the description of the ACM. In the next article, I will compare the ACM with the CCM and show why one might be preferred.
Summary
- The Ambient Composition Model (ACM) composes object graphs that do not store runtime data inside captured variables. Instead, runtime data is kept outside the graph and stored as ambient data. This ambient data is managed by the Composition Root and is provided to application components on request, long after those components have been constructed.
- While object graphs constructed using the Closure Composition Model (CCM) are inherently stateful, object graphs that apply the ACM become stateless and immutable.
- Ambient data should be used solely inside the Composition Root. Application code should be oblivious to how runtime data is acquired.
- Although the ACM is less common than the CCM, you’ll typically find that applications use both models intertwined.
HttpContext.Current
andDateTime.Now
used from inside the Composition Root are common examples of the ACM. In this article, their ambient data is hidden behindIUserContext
andITimeProvider
abstractions.
Comments
Wish to comment?
Found a typo?
Buy my book
Dependency Injection Principles, Practices, and Patterns. If you're interested to learn more about DI and software design in general, consider reading my book. Besides English, the book is available in Chinese, Italian, Polish, Russian, and Japanese.
I coauthored the book