-
Notifications
You must be signed in to change notification settings - Fork 12
Execution Context
The Execution Context is a powerful feature in LiteBus that provides access to contextual information throughout a single message processing pipeline. It allows pre-handlers, main handlers, and post-handlers to share data and control the execution flow without modifying the message contracts themselves.
The execution context is an object that holds metadata about the current mediation operation. It is created when a message is sent to a mediator and is disposed of when the operation completes.
LiteBus uses AsyncLocal<T>
to manage the context, which ensures that it is "ambient" and flows correctly across async/await
boundaries within a single logical thread of execution.
You can access the current execution context statically from anywhere in your code via AmbientExecutionContext.Current
.
using LiteBus.Messaging.Abstractions;
public class MyHandler : ICommandHandler<MyCommand>
{
public Task HandleAsync(MyCommand command, CancellationToken cancellationToken = default)
{
// Access the current context
IExecutionContext context = AmbientExecutionContext.Current;
// Use context properties
if (context.Tags.Contains("Admin"))
{
// ...
}
return Task.CompletedTask;
}
}
The Items
dictionary is a key-value collection (IDictionary<string, object>
) for sharing state between handlers in the same pipeline. This is useful for passing data discovered in a pre-handler to downstream handlers.
Example: Passing a User ID from a pre-handler to a post-handler for auditing.
// Pre-handler sets the user ID
public class UserContextPreHandler : ICommandPreHandler<CreateProductCommand>
{
public Task PreHandleAsync(CreateProductCommand command, CancellationToken cancellationToken = default)
{
var userId = GetCurrentUserIdFromHttpContext(); // Your logic here
AmbientExecutionContext.Current.Items["UserId"] = userId;
return Task.CompletedTask;
}
}
// Post-handler uses the user ID for auditing
public class AuditPostHandler : ICommandPostHandler<CreateProductCommand>
{
public Task PostHandleAsync(CreateProductCommand command, object? result, CancellationToken cancellationToken = default)
{
if (AmbientExecutionContext.Current.Items.TryGetValue("UserId", out var userIdObj) && userIdObj is string userId)
{
_auditLogger.Log(userId, "Created a new product.");
}
return Task.CompletedTask;
}
}
You can terminate the message pipeline at any point by calling Abort()
. This is commonly used in pre-handlers for validation or caching.
When Abort()
is called, LiteBus throws a LiteBusExecutionAbortedException
internally, which stops the pipeline. No further handlers (main or post) will be executed.
public class PermissionPreHandler : ICommandPreHandler<DeleteProductCommand>
{
public Task PreHandleAsync(DeleteProductCommand command, CancellationToken cancellationToken = default)
{
if (!CurrentUserHasPermission())
{
// Stop processing immediately
AmbientExecutionContext.Current.Abort();
}
return Task.CompletedTask;
}
}
If the message expects a result (e.g., IQuery<TResult>
), you must provide a result when aborting from a pre-handler. This is a powerful pattern for implementing caching.
public class CachingPreHandler : IQueryPreHandler<GetProductByIdQuery>
{
public Task PreHandleAsync(GetProductByIdQuery query, CancellationToken cancellationToken = default)
{
if (_cache.TryGetValue(query.ProductId, out ProductDto cachedProduct))
{
// Abort the pipeline and provide the cached value as the result
AmbientExecutionContext.Current.Abort(cachedProduct);
}
return Task.CompletedTask;
}
}
The Tags
collection contains the tags that were specified when the message was mediated. This allows handlers to dynamically change their behavior based on the context.
public class ProductQueryHandler : IQueryHandler<GetProductQuery, ProductDto>
{
public Task<ProductDto> HandleAsync(GetProductQuery query, CancellationToken cancellationToken = default)
{
var tags = AmbientExecutionContext.Current.Tags;
if (tags.Contains("IncludeExtraDetails"))
{
// Fetch and return a more detailed DTO
}
else
{
// Return a standard DTO
}
}
}
The CancellationToken
for the operation is also available on the execution context, which is the same token passed to the handler methods.
-
Use String Constants for Keys: To avoid typos, define the keys for the
Items
dictionary asconst string
in a shared class. -
Scope: Remember that the execution context is scoped to a single mediation call. It is not shared across different
SendAsync
orPublishAsync
calls. - Avoid Overuse: The context is for cross-cutting concerns. Core business data should always be part of the message contract itself.