Context: a vital pattern nobody talks about
In the enterprise, you will learn some common approaches to structuring code for better testability and separation of concerns — like wrapping DateTime.Now with an ICurrentTimeService.
Another powerful — but often overlooked — pattern is the use of context services. Despite how essential they become in real-world systems, especially in web applications, they rarely get the spotlight they deserve. This article dives into the Context Pattern: what it is, why it matters, and how to use it effectively.
Bringing a State into a Stateless World
The web is inherently stateless — each HTTP request operates independently, with no built-in memory of what came before. However, often the boundary of an operation spans multiple HTTP requests, making it necessary to maintain access to stateful data.
This is where the Context Pattern comes into play.
The Context Pattern is a design approach that encapsulates information about the current state or the “context” in which the system is operating.
You may already have seen some implementation of this pattern from Microsoft:
HttpContextIHttpContextAccessorControllerContextActionContextExceptionContextFunctionContext
Now, it is time to create your own 😎.
Implementing the Context Pattern
Typically, you would create the following contexts:
ICurrentUserContext— encapsulates information about the currently authenticated user. It allows business logic to know who is performing the actionITenantContext— represents the current tenantIWebSiteContext— the current site-specific information (URL, common route parameters, configurations, etc)ICorrelationContext— keeps track of correlation and tracing information used to track and link operations across distributed systemsIWorkflowContext— holds data related to the current workflow or process execution (workflow ID, current step or state, user tasks, etc). It helps orchestrate long-running processes by keeping track of where the operation is within a predefined business flow
There are no strict rules on how to implement the Context Pattern, and the exact design often depends on the specific requirements of your application. Typically, you start by defining a simple interface that exposes the contextual data your services need. For example, an interface for accessing current user information might look like this:
interface ICurrentUserContext
{
string? GetId();
string? GetEmail();
}The implementation then wraps the underlying framework-specific details, such as the HttpContext, to provide a clean and testable abstraction:
class CurrentUserContext : ICurrentUserContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CurrentUser(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string? GetEmail()
{
return _httpContextAccessor
.HttpContext?
.User?
.FindFirstValue(ClaimTypes.Email);
}
. . .
}Ways to Provide Context in Your Application
The most straightforward way to use these context providers is by injecting them into your services through constructor injection. This approach ensures that all methods within the service have consistent and easy access to context data without cluttering method signatures.
public class OrderService
{
private readonly ApplicationDbContext _dbContext;
private readonly ICurrentUserContext _currentUserContext;
public OrderService(
ApplicationDbContext dbContext,
ICurrentUserContext currentUserContext) 👈
{
_dbContext = dbContext;
_currentUserContext = currentUserContext;
}
public async Task<Order> CreateOrderAsync(int id)
{
var newOrder = new Order
{
Id = id,
UserId = _currentUserContext.GetId()
};
_dbContext.Orders.Add(newOrder);
await _dbContext.SaveChangesAsync();
return newOrder;
}
}Alternatively, you can pass context data explicitly through method parameters.
public class OrderService
{
private readonly ApplicationDbContext _dbContext;
public OrderService(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Order> CreateOrderAsync(
int id,
UserContext user) 👈
{
var newOrder = new Order
{
Id = id,
UserId = user.Id
};
_dbContext.Orders.Add(newOrder);
await _dbContext.SaveChangesAsync();
return newOrder;
}
}This approach may be preferred when the context varies per operation, increases explicitness, forces context to be set, potentially improving clarity, or is used in scenarios where DI is not available.
Context Initializations
In many cases, context services do not require explicit initialization because they resolve their values dynamically at runtime, as shown in previous examples.
Get iamprovidence’s stories in your inbox
Join Medium for free to get updates from this writer.
However, when you have multiple ways to initialize context or need to set context based on specific conditions, middleware or dedicated loaders are effective approaches.
internal class CurrentUserContextMiddleware : IMiddleware
{
private readonly IUserContextSettable _userContextSettable;
public CurrentUserContextMiddleware(
IUserContextSettable userContextSettable)
{
_userContextSettable = userContextSettable;
}
public async Task InvokeAsync(HttpContext httpContext, RequestDelegate next)
{
var clientId = httpContext.User?.FindFirstValue(CustomClaims.ClientId)
if (clientId == ClientIds.AdminApi)
{
_userContextSettable.SetContext(1, "Admin"); 👈
}
else if (clientId == ClientIds.PublicApi)
{
var id = httpContext.User?.FindFirstValue(ClaimTypes.Id);
var email = httpContext.User?.FindFirstValue(ClaimTypes.Email);
_userContextSettable.SetContext(id, email); 👈
}
await next.Invoke(context);
}
}This middleware approach lets you centralize context initialization logic early in the request pipeline, ensuring downstream services have access to fully prepared context data.
In some systems, especially those orchestrating complex workflows, context needs to be loaded, modified, and saved at each step of the process. This often means fetching context data from a database, performing operations, and persisting changes back.
You can represent these scenarios using different context classes for each operation:
// Marker interface for workflow contexts
public interface IWorkflowContext { }
// Context for creating an order
public class CreateOrderContext : IWorkflowContext
{
public Order Order { get; set; }
}
// Context for processing payouts
public class PayoutContext : IWorkflowContext
{
public Payout Payout { get; set; }
public User User { get; set; }
}A factory can then instantiate the appropriate context based on the workflow type:
public class WorkflowContextLoader : IWorkflowContextLoader
{
. . .
public async Task<IWorkflowContext> LoadAsync(WorkflowEntry entry)
{
var workflow = await _workflowRepo.GetAsync(entry.WorkflowId);
if (workflow.Type == WorkflowType.CreateOrder) 👈
{
var order = await _orderRepository.GetOrderAsync(entry.EntityId);
return new CreateOrderContext
{
Order = order,
};
}
if (workflow.Type == WorkflowType.PayoutProcessing) 👈
{
var payout = await _payoutRepository.GetPayoutAsync(entry.EntityId);
var user = await _userRepository.GetPayoutAsync(payout.CreatedByUserId);
return new PayoutContext
{
Payout = payout,
User = user,
};
}
throw new InvalidOperationException("Unsupported workflow type.");
}
}Use the factory to create and initialize context before passing it into services when context creation involves complex logic or external service calls.
Sum It Up
The Context Pattern is a powerful tool for managing state and cross-cutting concerns in modern applications. It offers several key benefits:
- shared state across operations and layers — maintain consistent contextual data throughout a request or workflow without relying on manual data passing
- simplified method signatures — avoid the clutter of repeatedly passing the same parameters through multiple layers. If you’re doing that, it’s often a sign that a context object is needed
- centralized access to request-scoped data — encapsulate user, tenant, or other environment-specific data in one place, improving maintainability, clarity, and code navigation
- improved testability — context interfaces are easy to mock, making unit tests cleaner and more reliable
- framework decoupling — abstract away infrastructure concerns like
HttpContext, so your business logic stays clean, focused, and portable
This pattern is widely used across many real-world projects:
- you can see it in Jason Taylor’s Clean Architecture template. Notice how the interface and implementation reside in different layers
- E-Shop on Containers from Microsoft has the user context too
- you can have different contexts in modular monoliths reflecting the ubiquitous language of each module (
UserContext,MemberContext, etc) - and this is my small state machine where context is serialized into cookies to persist information throughout the multi-step order creation workflow🙃
So yes — context isn’t just useful, it’s pretty much essential 😅.
💬 Let me know in the comments: How does your context look? In which scenarios do you apply this pattern?
👏 Clap for this article if you found something new
☕️ You can also buy me a coffee with the link below
✅ And most importantly, follow and share it with friends
😉 Keep learning








