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:

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

DEFINITION The Ambient Composition Model 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.

The following figure captures the essence of the ACM.

The essence of the Ambient Composition Model

The following sequence diagram shows the basic flow of data using the ACM.

The basic flow of the Ambient Composition Model

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

Comments



The comments are not loaded because you disabled JavaScript. That's okay—I disable JavaScript myself all the time. The comments for this blog post are loaded dynamically from GitHub. To read these comments, just click here.


Wish to comment?

You can add a comment to this post by commenting on this GitHub issue.


Found a typo?

The MarkDown file of this blog post can be found here. I accept pull requests.


Buy my book

Dependency Injection Principles, Practices, and Patterns Cover Small I coauthored the 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.