DEV Community

Cristiano Rodrigues for Unhacked

Posted on

2

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.

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay