DEV Community

Cristiano Rodrigues for Unhacked

Posted on

Entendendo o Ambient Context Pattern

Imagine ter acesso a informações cruciais em todos os cantos da sua aplicação sem a necessidade de passá-las através de parâmetros de métodos. É exatamente isso que o Ambient Context Pattern oferece. Este padrão de design resolve de forma elegante o desafio do acesso a dados contextuais globais, tornando-se especialmente útil quando certas informações precisam estar ao alcance de várias partes do seu código. É particularmente valioso em situações onde a passagem tradicional de dados pode se tornar um pesadelo de manutenção ou resultar em um código tão verboso que perde sua eficácia.

Compreendendo o Ambient Context Pattern

O Ambient Context Pattern estabelece um mecanismo para compartilhar informações contextuais através de toda a aplicação de forma transparente. Este padrão se destaca em diversos cenários de uso.

No contexto de rastreamento de operações, o padrão facilita a implementação de logging, permitindo o acompanhamento detalhado das operações através de diferentes componentes do sistema.

O gerenciamento do contexto do usuário é outro cenário, oferecendo um meio eficiente de manter informações de autenticação, preferências do usuário e definições de permissões e roles acessíveis de forma consistente em toda a aplicação.

Para o contexto de requisição, o padrão proporciona um gerenciamento de headers HTTP, tokens de segurança e informações de roteamento. No âmbito do contexto de ambiente, ele facilita o controle de configurações de cultura, variáveis de ambiente e feature flags.

Ambient Context Pattern

Vamos ao ponto-chave: o Ambient Context Pattern é centrado em uma classe que armazena o contexto ao longo de toda a requisição. Veja um exemplo prático em C#:

public class AmbientContext
{
    private static AsyncLocal<AmbientContext> _current = new AsyncLocal<AmbientContext>();

    public static AmbientContext Current
    {
        get => _current.Value ?? (_current.Value = new AmbientContext());
        set => _current.Value = value;
    }

    public string RequestId { get; set; }
    public string UserId { get; set; }
    public string Culture { get; set; }
    public Dictionary<string, string> Headers { get; set; }
    public Dictionary<string, object> Items { get; set; }

    public AmbientContext()
    {
        Headers = new Dictionary<string, string>();
        Items = new Dictionary<string, object>();
    }
}
Enter fullscreen mode Exit fullscreen mode

A utilização de AsyncLocal é fundamental aqui, garantindo o isolamento correto do contexto entre threads e requisições assíncronas. Isso proporciona uma solução robusta para gerenciar o estado do contexto, sem preocupações de interferência cruzada.

Interface de Acesso ao Contexto

Para promover um acoplamento mais flexível e facilitar testes, podemos implementar uma interface de acesso:

public interface IAmbientContextAccessor
{
    AmbientContext Context { get; }
    void SetContext(AmbientContext context);
}

public class AmbientContextAccessor : IAmbientContextAccessor
{
    public AmbientContext Context => AmbientContext.Current;

    public void SetContext(AmbientContext context)
    {
        AmbientContext.Current = context;
    }
}
Enter fullscreen mode Exit fullscreen mode

Integração com o Pipeline HTTP usando um Middleware

Para uma integraçã com o pipeline HTTP, podemos empregar um middleware personalizado. Essa abordagem permite estabelecer um fluxo de processamento padrão, facilitando a gestão do ciclo de vida da requisição. Com isso, o Ambient Context pode ser perfeitamente alinhado com as etapas do pipeline, garantindo que o contexto seja adequadamente inicializado, manipulado e finalizado ao longo da execução da requisição.

public class AmbientContextMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IAmbientContextAccessor _contextAccessor;

    public AmbientContextMiddleware(RequestDelegate next, IAmbientContextAccessor contextAccessor)
    {
        _next = next;
        _contextAccessor = contextAccessor;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        var ambientContext = new AmbientContext
        {
            RequestId = httpContext.TraceIdentifier,
            UserId = httpContext.User?.Identity?.Name,
            Culture = CultureInfo.CurrentCulture.Name
        };

        foreach (var header in httpContext.Request.Headers)
        {
            ambientContext.Headers[header.Key] = header.Value.ToString();
        }

        _contextAccessor.SetContext(ambientContext);

        try
        {
            await _next(httpContext);
        }
        finally
        {
            _contextAccessor.SetContext(null);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuração na Aplicação

A integração do Ambient Context Pattern à sua aplicação começa com uma configuração simples durante a inicialização. Com isso, o contexto fica disponível e funcionando corretamente em todo o ciclo de vida da aplicação.

builder.Services.AddSingleton<IAmbientContextAccessor,AmbientContextAccessor>();
Enter fullscreen mode Exit fullscreen mode

Utilização em Serviços

O acesso ao contexto é facilitado pela injeção de dependência em serviços da aplicação, permitindo uma integração transparente com o Ambient Context Pattern.

public class UserService
{
    private readonly IAmbientContextAccessor _contextAccessor;

    public UserService(IAmbientContextAccessor contextAccessor)
    {
        _contextAccessor = contextAccessor;
    }

    public async Task<UserInfo> GetCurrentUserInfoAsync()
    {
        var context = _contextAccessor.Context;

        return new UserInfo
        {
            UserId = context.UserId,
            Culture = context.Culture,
            RequestId = context.RequestId
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

O HttpContextAccessor no .NET

O acesso ao HttpContext é simplificado pelo HttpContextAccessor, uma implementação do Ambient Context Pattern no ecossistema .NET. Isso resolve o desafio de acessar o HttpContext em locais onde a passagem como parâmetro seria impraticável.

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Provides access to the current <see cref="HttpContext"/>, if one is available.
/// </summary>
/// <remarks>
/// This interface should be used with caution. It relies on <see cref="System.Threading.AsyncLocal{T}" /> which can have a negative performance impact on async calls.
/// It also creates a dependency on "ambient state" which can make testing more difficult.
/// </remarks>
public interface IHttpContextAccessor
{
    /// <summary>
    /// Gets or sets the current <see cref="HttpContext"/>. Returns <see langword="null" /> if there is no active <see cref="HttpContext" />.
    /// </summary>
    HttpContext? HttpContext { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Provides an implementation of <see cref="IHttpContextAccessor" /> based on the current execution context.
/// </summary>
[DebuggerDisplay("HttpContext = {HttpContext}")]
public class HttpContextAccessor : IHttpContextAccessor
{
    private static readonly 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 sealed class HttpContextHolder
    {
        public HttpContext? Context;
    }
}
Enter fullscreen mode Exit fullscreen mode

Aspectos Arquiteturais e Considerações

Ao implementar o Ambient Context Pattern, é essencial considerar alguns aspectos arquiteturais chave para garantir um funcionamento eficiente.

Gerenciamento do Ciclo de Vida (Isolamento e Limpeza) - Cada requisição deve ter seu próprio contexto isolado, garantindo processamento independente de dados. Além disso, a limpeza do contexto após o processamento é crucial para manter a integridade da aplicação. O uso de AsyncLocal assegura o isolamento adequado entre threads, fundamental em ambientes altamente concorrentes.

Segurança de Thread e Performance - A implementação garante thread safety, eliminando problemas de compartilhamento de estado, e apresenta um overhead mínimo com o uso de AsyncLocal. Isso permite um desempenho eficiente sem comprometer a segurança entre threads.

Testabilidade Facilitada - A interface IAmbientContextAccessor facilita a testabilidade, permitindo a substituição fácil do contexto em testes unitários. Além disso, o middleware pode ser substituído em testes, e o contexto pode ser manipulado conforme necessário para diferentes cenários de teste.

Recomendações de Uso

A implementação do Ambient Context Pattern deve ser utilizado com moderação, reservando-o para informações verdadeiramente globais e evitando o armazenamento de dados específicos de negócio. A manutenção da simplicidade do contexto é crucial para evitar complexidade desnecessária.

A garantia de thread safety deve ser uma prioridade, utilizando AsyncLocal consistentemente para contextos por requisição e evitando estado mutável compartilhado. Quando possível, a implementação de imutabilidade pode trazer benefícios adicionais de segurança e previsibilidade.

Para facilitar os testes, deve-se fornecer mecanismos para substituir o contexto em testes unitários e manter a capacidade de injetar contextos simulados.

Conclusão

O Ambient Context Pattern, como visto no exemplo do HttpContextAccessor no .NET, é uma ferramenta poderosa se usada corretamente. Para aproveitá-lo ao máximo, é preciso entender seus pontos fortes e fracos.

Para fazer o padrão funcionar de forma apropriada, é simples: encontre o equilíbrio entre facilidade de uso e independência entre partes. Use-o apenas para informações compartilhadas em toda a aplicação e faça com cuidado. O resultado é uma estrutura de aplicação mais simples, sem comprometer a manutenção ou testes.

Lembre-se: antes de usar, faça a conta. Pergunte-se se a simplicidade vale a relação mais próxima entre as partes do sistema. Com uma implementação bem pensada, o Ambient Context Pattern pode transformar sua arquitetura de software, tornando-a mais elegante e fácil de manter.

Top comments (0)