
Ambient Context Antipattern in Dependency Injection
Author - Abdul Rahman (Bhai)
Dependency Injection
7 Articles
Table of Contents
What we gonna do?
The Ambient Context is a subtle anti-pattern in dependency injection that appears when you access contextual information through static properties or thread-local storage instead of explicitly injecting dependencies. In this article, let's learn about the Ambient Context anti-pattern in .NET and understand why it undermines the benefits of proper Dependency Injection.
Why we gonna do?
The Ambient Context anti-pattern occurs when classes access contextual information (like current user, request data, or configuration) through static accessors instead of receiving them as explicit dependencies. While this pattern provides convenient access to context information, it creates several serious problems.
Common examples include accessing HttpContext.Current, Thread.CurrentPrincipal, or custom static context holders. These seem convenient but introduce hidden dependencies and make testing extremely difficult.
Why Ambient Context is Problematic
Ambient Context creates several issues:
- Hidden dependencies - Context access is not visible in the class interface
- Non-deterministic behavior - Context might not be available when expected
- Threading issues - Thread-local storage can cause unpredictable behavior
- Testing difficulties - Hard to control context during unit tests
- Temporal coupling - Code assumes context is available at specific times
- Reduced reusability - Classes become tied to specific runtime environments
How we gonna do?
Example: OrderService Using Ambient Context
Here's a typical example of the Ambient Context anti-pattern using HttpContext access:
// Ambient Context - accessing current user through static property
public class OrderService
{
private readonly IOrderRepository orderRepository;
public OrderService(IOrderRepository orderRepository)
{
this.orderRepository = orderRepository;
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
// Ambient Context anti-pattern - hidden dependency on HttpContext
var currentUser = HttpContext.Current?.User?.Identity?.Name;
if (string.IsNullOrEmpty(currentUser))
throw new InvalidOperationException("User context not available");
var order = new Order
{
UserId = currentUser,
Items = request.Items,
CreatedAt = DateTime.UtcNow
};
return await orderRepository.SaveAsync(order);
}
public async Task<IEnumerable<Order>> GetUserOrdersAsync()
{
// Another hidden dependency on ambient context
var currentUser = HttpContext.Current?.User?.Identity?.Name;
if (string.IsNullOrEmpty(currentUser))
throw new InvalidOperationException("User context not available");
return await orderRepository.GetOrdersByUserAsync(currentUser);
}
}
Another Example: Configuration Through Ambient Context
Here's another common pattern using static configuration access:
// Static configuration accessor
public static class AppConfig
{
public static string ConnectionString =>
ConfigurationManager.ConnectionStrings["DefaultConnection"]?.ConnectionString;
public static int MaxRetryAttempts =>
int.Parse(ConfigurationManager.AppSettings["MaxRetryAttempts"] ?? "3");
}
// Service using ambient context for configuration
public class EmailService
{
public async Task SendEmailAsync(string to, string subject, string body)
{
// Hidden dependency on ambient configuration
var smtpServer = AppConfig.SmtpServer;
var smtpPort = AppConfig.SmtpPort;
var maxRetries = AppConfig.MaxRetryAttempts;
using var client = new SmtpClient(smtpServer, smtpPort);
var attempt = 0;
while (attempt < maxRetries)
{
try
{
await client.SendMailAsync(to, "noreply@company.com", subject, body);
break;
}
catch (SmtpException) when (attempt < maxRetries - 1)
{
attempt++;
await Task.Delay(1000 * attempt); // Exponential backoff
}
}
}
}
Problems with Ambient Context Approach
1. Hidden Dependencies
The OrderService constructor doesn't reveal that it depends on HttpContext. You can't tell what the class needs just by looking at its public interface.
2. Testing Difficulties
Unit testing becomes complex because you must set up the ambient context:
[Test]
public async Task CreateOrderAsync_WithValidRequest_CreatesOrder()
{
// Difficult to set up HttpContext for testing
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "testuser@example.com")
}));
// This is problematic - setting static/global state
HttpContext.Current = httpContext;
try
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var orderService = new OrderService(mockRepository.Object);
var request = new CreateOrderRequest { Items = new[] { "Item1", "Item2" } };
// Act
var result = await orderService.CreateOrderAsync(request);
// Assert
Assert.AreEqual("testuser@example.com", result.UserId);
}
finally
{
// Clean up global state
HttpContext.Current = null;
}
}
3. Threading and Concurrency Issues
Thread-local storage can cause unpredictable behavior in async scenarios:
// Problematic: Thread-local ambient context
public static class UserContext
{
private static readonly ThreadLocal<string> currentUserId = new ThreadLocal<string>();
public static string CurrentUserId
{
get => currentUserId.Value;
set => currentUserId.Value = value;
}
}
// This can fail in async scenarios where execution continues on different threads
public async Task ProcessOrderAsync()
{
UserContext.CurrentUserId = "user123";
// After await, execution might continue on a different thread
// where UserContext.CurrentUserId is null!
await SomeAsyncOperation();
var userId = UserContext.CurrentUserId; // Might be null!
}
Refactoring from Ambient Context to Proper DI
The solution is to explicitly inject the required context as dependencies:
// Create explicit abstractions for context information
public interface IUserContext
{
string GetCurrentUserId();
bool IsAuthenticated { get; }
}
public interface IEmailConfiguration
{
string SmtpServer { get; }
int SmtpPort { get; }
int MaxRetryAttempts { get; }
}
// Refactored OrderService with explicit dependencies
public class OrderService
{
private readonly IOrderRepository orderRepository;
private readonly IUserContext userContext;
public OrderService(IOrderRepository orderRepository, IUserContext userContext)
{
this.orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
this.userContext = userContext ?? throw new ArgumentNullException(nameof(userContext));
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
if (!userContext.IsAuthenticated)
throw new UnauthorizedAccessException("User must be authenticated");
var currentUser = userContext.GetCurrentUserId();
var order = new Order
{
UserId = currentUser,
Items = request.Items,
CreatedAt = DateTime.UtcNow
};
return await orderRepository.SaveAsync(order);
}
public async Task<IEnumerable<Order>> GetUserOrdersAsync()
{
if (!userContext.IsAuthenticated)
throw new UnauthorizedAccessException("User must be authenticated");
var currentUser = userContext.GetCurrentUserId();
return await orderRepository.GetOrdersByUserAsync(currentUser);
}
}
// Refactored EmailService with explicit configuration dependency
public class EmailService
{
private readonly IEmailConfiguration config;
public EmailService(IEmailConfiguration config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
}
public async Task SendEmailAsync(string to, string subject, string body)
{
using var client = new SmtpClient(config.SmtpServer, config.SmtpPort);
var attempt = 0;
while (attempt < config.MaxRetryAttempts)
{
try
{
await client.SendMailAsync(to, "noreply@company.com", subject, body);
break;
}
catch (SmtpException) when (attempt < config.MaxRetryAttempts - 1)
{
attempt++;
await Task.Delay(1000 * attempt);
}
}
}
}
Implementation of Context Abstractions
Here's how you might implement the context abstractions:
// ASP.NET Core implementation
public class HttpUserContext : IUserContext
{
private readonly IHttpContextAccessor httpContextAccessor;
public HttpUserContext(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetCurrentUserId()
{
var httpContext = httpContextAccessor.HttpContext;
return httpContext?.User?.Identity?.Name ??
throw new InvalidOperationException("No authenticated user found");
}
public bool IsAuthenticated =>
httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
}
// Configuration implementation
public class EmailConfiguration : IEmailConfiguration
{
private readonly IConfiguration configuration;
public EmailConfiguration(IConfiguration configuration)
{
this.configuration = configuration;
}
public string SmtpServer => configuration["Email:SmtpServer"];
public int SmtpPort => int.Parse(configuration["Email:SmtpPort"]);
public int MaxRetryAttempts => int.Parse(configuration["Email:MaxRetryAttempts"]);
}
Registration in DI Container
// In Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Register the abstractions and implementations
services.AddHttpContextAccessor(); // Required for HttpUserContext
services.AddScoped<IUserContext, HttpUserContext>();
services.AddSingleton<IEmailConfiguration, EmailConfiguration>();
// Register your services
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<OrderService>();
services.AddScoped<EmailService>();
}
Easy Unit Testing
With explicit dependencies, testing becomes straightforward:
[Test]
public async Task CreateOrderAsync_WithAuthenticatedUser_CreatesOrder()
{
// Arrange - Clean and simple
var mockRepository = new Mock<IOrderRepository>();
var mockUserContext = new Mock<IUserContext>();
mockUserContext.Setup(x => x.IsAuthenticated).Returns(true);
mockUserContext.Setup(x => x.GetCurrentUserId()).Returns("testuser@example.com");
var orderService = new OrderService(mockRepository.Object, mockUserContext.Object);
var request = new CreateOrderRequest { Items = new[] { "Item1", "Item2" } };
// Act
var result = await orderService.CreateOrderAsync(request);
// Assert
Assert.AreEqual("testuser@example.com", result.UserId);
mockRepository.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Once);
}
[Test]
public async Task CreateOrderAsync_WithUnauthenticatedUser_ThrowsException()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var mockUserContext = new Mock<IUserContext>();
mockUserContext.Setup(x => x.IsAuthenticated).Returns(false);
var orderService = new OrderService(mockRepository.Object, mockUserContext.Object);
var request = new CreateOrderRequest { Items = new[] { "Item1" } };
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => orderService.CreateOrderAsync(request)
);
}
Summary
The Ambient Context anti-pattern creates hidden dependencies and makes code difficult to test and reason about. While accessing context through static properties might seem convenient, it introduces non-deterministic behavior, threading issues, and tight coupling to specific runtime environments.
The solution is to create explicit abstractions for contextual information and inject them as dependencies through Constructor Injection. This approach makes dependencies visible, enables easy unit testing, eliminates threading issues, and improves code reusability. Remember: if your class needs contextual information, make it an explicit dependency rather than hiding it behind static accessors.