Skip to content

Latest commit

 

History

History
122 lines (95 loc) · 7.22 KB

share-data-with-async-local.md

File metadata and controls

122 lines (95 loc) · 7.22 KB

Share data across the lifetime of an HTTP request

Motivation

I want to share data across diferent objects during the lifetime of an HTTP request.

This could be achieved by creating a type to hold the data and registering it on the IServiceCollection with a lifetime of Scoped. This type could then be injected into the constructor of other objects and the data it holds would be on a per HTTP request basis.

This approach however does not work for all scenarios. For instance, this won't work if you want to share data per HTTP request and access it on an delegating handler that is configured for an HTTP client. In this case, due to the lifecycle of the HttpMessageHandler that are added to HttpClients the scoped service approach does not yield the expected results. Read DI scopes in IHttpClientFactory message handlers don't work like you think they do for a more detailed explanation of this scenario and code examples.

An approach that will work in all scenarios can be achieved by using the AsyncLocal<T> type whose description on the documentation is as follows:

  • Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.

How to

Example 1

Let's take a look to how the AsyncLocal<T> is used to implement the HttpContextAccessor class which provides access to the HttpContext that must be unique per HTTP request.

using System.Threading;

namespace Microsoft.AspNetCore.Http
{
    /// <summary>
    /// Provides an implementation of <see cref="IHttpContextAccessor" /> based on the current execution context. 
    /// </summary>
    public class HttpContextAccessor : IHttpContextAccessor
    {
        private static AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>();

        /// <inheritdoc/>
        public HttpContext? HttpContext
        {
            get
            {
                return  _httpContextCurrent.Value?.Context;
            }
            set
            {
                var holder = _httpContextCurrent.Value;
                if (holder != null)
                {
                    // Clear current HttpContext trapped in the AsyncLocals, as its done.
                    holder.Context = null;
                }

                if (value != null)
                {
                    // Use an object indirection to hold the HttpContext in the AsyncLocal,
                    // so it can be cleared in all ExecutionContexts when its cleared.
                    _httpContextCurrent.Value = new HttpContextHolder { Context = value };
                }
            }
        }

        private class HttpContextHolder
        {
            public HttpContext? Context;
        }
    }
}

This implementation contains an extra private class, the HttpContextHolder to make sure the HttpContext is clearead from all ExecutionContexts when it's no longer needed. Notice the use of the AsyncLocal<HttpContextHolder>.

The idea is that the HttpContextAccessor is then added to the IServiceCollection so that you can take a dependency on IHttpContextAccessor by injecting it via the constructor whenever you require access to the HttpContext instance.

Example 2

Another example is on the Header propagation middleware. This middleware also uses the AsyncLocal<T> class to enable header values to propagate as ambient data throught the execution of an HTTP request. In this scenario the headers are propagated from the incoming HTTP request into the server to the outgoing HTTP requests made by an HttpClient.

Let's look at the implementation, particularly at the part that makes use of the AsyncLocal<T>, which is the HeaderPropagationValues class:

using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation
{
    /// <summary>
    /// Contains the outbound header values for the <see cref="HeaderPropagationMessageHandler"/>.
    /// </summary>
    public class HeaderPropagationValues
    {
        private readonly static AsyncLocal<IDictionary<string, StringValues>?> _headers = new AsyncLocal<IDictionary<string, StringValues>?>();

        /// <summary>
        /// Gets or sets the headers values collected by the <see cref="HeaderPropagationMiddleware"/> from the current request
        /// that can be propagated.
        /// </summary>
        /// <remarks>
        /// The keys of <see cref="Headers"/> correspond to <see cref="HeaderPropagationEntry.CapturedHeaderName"/>.
        /// </remarks>
        public IDictionary<string, StringValues>? Headers
        {
            get
            {
                return _headers.Value;
            }
            set
            {
                _headers.Value = value;
            }
        }
    }
}

Notice how the HeaderPropagationValues is added to the IServiceCollection as a singleton.

This is done so that you can have access to the HeaderPropagationValues wherever you require it. For instance, when adding the delegating handler to the HttpClient that provides the header propagation.

With the above you have now set the stage so that you can set values on a instance of HeaderPropagationValues which will be unique per HTTP request. You can see on the HeaderPropagationMiddleware how the constructor takes in an instance of HeaderPropagationValues and sets the headers to be propagated which are later accessed on the HeaderPropagationMessageHandler which is used to set the headers to be propagated on the outgoing HttpClient request.

Demos

Analyse the code of the demo app AmbientDataDemo to gain a better understanding of how to use the AsyncLocal<T> type to provide ambient data for your app.