Skip to content

Execution Context

A. Shafie edited this page Sep 26, 2025 · 1 revision

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.

What is the Execution Context?

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.

Accessing the Current Context

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;
    }
}

Key Features

1. Items Dictionary

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;
    }
}

2. Aborting Execution

You can terminate the message pipeline at any point by calling Abort(). This is commonly used in pre-handlers for validation or caching.

Aborting Without a Result

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;
    }
}

Aborting With a Result

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;
    }
}

3. Accessing Tags

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
        }
    }
}

4. Cancellation Token

The CancellationToken for the operation is also available on the execution context, which is the same token passed to the handler methods.

Best Practices

  1. Use String Constants for Keys: To avoid typos, define the keys for the Items dictionary as const string in a shared class.
  2. Scope: Remember that the execution context is scoped to a single mediation call. It is not shared across different SendAsync or PublishAsync calls.
  3. Avoid Overuse: The context is for cross-cutting concerns. Core business data should always be part of the message contract itself.
Clone this wiki locally