Mutation Hooks¶
Verified by tests
MutationEndpointBaseTests, MutationContextTests, GraphQLMutationBaseTests, GraphQLMutationLifecycleTests — library CI run #31657041675 (2026-08-13)
Mutation hooks provide extension points for adding cross-cutting concerns to your command endpoints. The MutationEndpointBase<TCommand, TResult> class defines three virtual methods you can override to hook into the execution lifecycle.
Quick Reference¶
| Hook | When Called | Common Uses |
|---|---|---|
OnBeforeExecuteAsync |
Before command dispatch | Validation, authorization, logging |
OnAfterExecuteAsync |
After successful dispatch | Notifications, audit logging |
OnErrorAsync |
When dispatch throws | Error handling, fallback results |
Where hooks live: the source generators emit endpoint classes as partial — {CommandClassName}Endpoint for REST and {CommandClassName}Mutation for GraphQL, in the <command namespace>.Generated namespace. You override hooks in your own partial half of those generated classes (the examples below use the REST naming). Do not restate generic constraints on overrides — they are inherited from the base declaration.
The Execution Lifecycle¶
The ExecuteAsync method orchestrates the hook lifecycle:
ExecuteAsync(command, ct)
|
+-> Check cancellation (throws if cancelled)
|
+-> Create MutationContext
|
+-> OnBeforeExecuteAsync(command, context, ct)
|
+-> DispatchCommandAsync(command, ct)
| |
| +-> Success: OnAfterExecuteAsync(command, result, context, ct)
| |
| +-> Exception: OnErrorAsync(command, ex, context, ct)
| |
| +-> Returns result: Use that result
| |
| +-> Returns null: Rethrow exception
|
+-> Return result
OnBeforeExecuteAsync¶
Called before command dispatch. Override to add validation, logging, authorization, or other pre-processing.
Signature: OnBeforeExecuteAsync
protected virtual ValueTask OnBeforeExecuteAsync(
TCommand command,
IMutationContext context,
CancellationToken ct) => ValueTask.CompletedTask;
Parameters:
- command - The command to be executed
- context - The mutation context with cancellation token and shared items
- ct - The cancellation token
Example: Validation¶
Example: Validation
public partial class CreateOrderCommandEndpoint {
private readonly IValidator<CreateOrderCommand> _validator;
protected override async ValueTask OnBeforeExecuteAsync(
CreateOrderCommand command,
IMutationContext context,
CancellationToken ct) {
// Validate command before dispatch
await _validator.ValidateAndThrowAsync(command, ct);
}
}
Example: Authorization¶
Example: Authorization
public partial class UpdateProductCommandEndpoint {
private readonly IAuthorizationService _authz;
private readonly ICurrentUser _user;
protected override async ValueTask OnBeforeExecuteAsync(
UpdateProductCommand command,
IMutationContext context,
CancellationToken ct) {
var result = await _authz.AuthorizeAsync(
_user.Principal,
command,
"ProductUpdatePolicy");
if (!result.Succeeded) {
throw new UnauthorizedAccessException("Not authorized to update product");
}
}
}
Example: Timing¶
Example: Timing
public partial class ProcessPaymentCommandEndpoint {
protected override ValueTask OnBeforeExecuteAsync(
ProcessPaymentCommand command,
IMutationContext context,
CancellationToken ct) {
// Store start time in context for use in OnAfterExecuteAsync
context.Items["StartTime"] = Stopwatch.GetTimestamp();
return ValueTask.CompletedTask;
}
}
OnAfterExecuteAsync¶
Called after successful command dispatch. Override to add post-processing, notifications, or audit logging. Not called if dispatch throws an exception.
Signature: OnAfterExecuteAsync
protected virtual ValueTask OnAfterExecuteAsync(
TCommand command,
TResult result,
IMutationContext context,
CancellationToken ct) => ValueTask.CompletedTask;
Parameters:
- command - The executed command
- result - The result from command execution
- context - The mutation context with cancellation token and shared items
- ct - The cancellation token
Example: Notifications¶
Example: Notifications
public partial class CreateOrderCommandEndpoint {
private readonly INotificationService _notifications;
protected override async ValueTask OnAfterExecuteAsync(
CreateOrderCommand command,
OrderResult result,
IMutationContext context,
CancellationToken ct) {
// Send notification after successful order creation
await _notifications.NotifyAsync(
$"Order {result.OrderId} created for customer {command.CustomerId}",
ct);
}
}
Example: Audit Logging¶
Example: Audit Logging
public partial class DeleteUserCommandEndpoint {
private readonly IAuditLogger _audit;
private readonly ICurrentUser _user;
protected override async ValueTask OnAfterExecuteAsync(
DeleteUserCommand command,
DeleteResult result,
IMutationContext context,
CancellationToken ct) {
await _audit.LogAsync(new AuditEntry {
Action = "UserDeleted",
PerformedBy = _user.Id,
TargetId = command.UserId,
Timestamp = DateTimeOffset.UtcNow
}, ct);
}
}
Example: Performance Logging¶
Example: Performance Logging
public partial class ProcessPaymentCommandEndpoint {
private readonly ILogger<ProcessPaymentCommandEndpoint> _logger;
protected override ValueTask OnAfterExecuteAsync(
ProcessPaymentCommand command,
PaymentResult result,
IMutationContext context,
CancellationToken ct) {
if (context.Items.TryGetValue("StartTime", out var startObj)
&& startObj is long startTime) {
var elapsed = Stopwatch.GetElapsedTime(startTime);
_logger.LogInformation(
"Payment {PaymentId} processed in {ElapsedMs}ms",
result.PaymentId,
elapsed.TotalMilliseconds);
}
return ValueTask.CompletedTask;
}
}
OnErrorAsync¶
Called when command dispatch throws an exception. Override to provide custom error handling, logging, or fallback results.
Signature: OnErrorAsync
protected virtual ValueTask<TResult?> OnErrorAsync(
TCommand command,
Exception ex,
IMutationContext context,
CancellationToken ct) => ValueTask.FromResult<TResult?>(default);
Parameters:
- command - The command that caused the error
- ex - The exception that was thrown
- context - The mutation context with cancellation token and shared items
- ct - The cancellation token
Returns:
- A result to return instead of throwing, or null to rethrow the exception
Example: Error Logging¶
Example: Error Logging
public partial class CreateOrderCommandEndpoint {
private readonly ILogger<CreateOrderCommandEndpoint> _logger;
protected override ValueTask<OrderResult?> OnErrorAsync(
CreateOrderCommand command,
Exception ex,
IMutationContext context,
CancellationToken ct) {
_logger.LogError(ex,
"Failed to create order for customer {CustomerId}",
command.CustomerId);
// Return null to rethrow the exception
return ValueTask.FromResult<OrderResult?>(null);
}
}
Example: Fallback Result¶
Example: Fallback Result
public partial class GetDataCommandEndpoint {
private readonly ICache _cache;
protected override async ValueTask<DataResult?> OnErrorAsync(
GetDataCommand command,
Exception ex,
IMutationContext context,
CancellationToken ct) {
// Try to return cached data as fallback
var cached = await _cache.GetAsync<DataResult>(command.Key, ct);
if (cached is not null) {
return cached with { FromCache = true };
}
// No cached data - rethrow original exception
return null;
}
}
Example: Error Transformation¶
Example: Error Transformation
public partial class ApiCommandEndpoint {
protected override ValueTask<ApiResult?> OnErrorAsync(
ApiCommand command,
Exception ex,
IMutationContext context,
CancellationToken ct) {
// Transform external API errors into domain errors
if (ex is HttpRequestException httpEx) {
return ValueTask.FromResult<ApiResult?>(new ApiResult {
Success = false,
ErrorCode = "EXTERNAL_API_ERROR",
ErrorMessage = "External service unavailable"
});
}
// Rethrow other exceptions
return ValueTask.FromResult<ApiResult?>(null);
}
}
IMutationContext¶
The IMutationContext interface provides context information during mutation execution. It is passed to all hooks and enables sharing state between them.
Interface: IMutationContext
public interface IMutationContext {
/// <summary>
/// The cancellation token for the current request.
/// </summary>
CancellationToken CancellationToken { get; }
/// <summary>
/// A dictionary for passing custom data between hooks.
/// Use this to share state between OnBeforeExecuteAsync and OnAfterExecuteAsync.
/// </summary>
IDictionary<string, object?> Items { get; }
}
Using Items for State Sharing¶
The Items dictionary enables passing data between hooks:
Using Items for State Sharing
public partial class CreateOrderCommandEndpoint {
protected override ValueTask OnBeforeExecuteAsync(
CreateOrderCommand command,
IMutationContext context,
CancellationToken ct) {
// Store data in context
context.Items["RequestId"] = Guid.NewGuid();
context.Items["StartTime"] = DateTimeOffset.UtcNow;
return ValueTask.CompletedTask;
}
protected override ValueTask OnAfterExecuteAsync(
CreateOrderCommand command,
OrderResult result,
IMutationContext context,
CancellationToken ct) {
// Retrieve data from context
var requestId = (Guid)context.Items["RequestId"]!;
var startTime = (DateTimeOffset)context.Items["StartTime"]!;
var duration = DateTimeOffset.UtcNow - startTime;
_logger.LogInformation(
"Request {RequestId} completed in {Duration}ms",
requestId,
duration.TotalMilliseconds);
return ValueTask.CompletedTask;
}
}
MutationContext Implementation¶
The default MutationContext class implements IMutationContext:
MutationContext Implementation
public sealed class MutationContext(CancellationToken cancellationToken) : IMutationContext {
public CancellationToken CancellationToken { get; } = cancellationToken;
public IDictionary<string, object?> Items { get; } = new Dictionary<string, object?>();
}
Complete Example¶
Here is a complete example showing all hooks working together:
Complete Example
// The attribute goes on the command class; the generator emits the
// partial CreateOrderCommandEndpoint (REST) and CreateOrderCommandMutation (GraphQL)
[CommandEndpoint<CreateOrderCommand, OrderResult>(
RestRoute = "/api/orders",
GraphQLMutation = "createOrder")]
public class CreateOrderCommand : ICommand {
public required string CustomerId { get; init; }
}
// Your partial half of the generated endpoint holds the hooks
public partial class CreateOrderCommandEndpoint {
private readonly IValidator<CreateOrderCommand> _validator;
private readonly INotificationService _notifications;
private readonly ILogger<CreateOrderCommandEndpoint> _logger;
protected override async ValueTask OnBeforeExecuteAsync(
CreateOrderCommand command,
IMutationContext context,
CancellationToken ct) {
// Validation
await _validator.ValidateAndThrowAsync(command, ct);
// Store timing info
context.Items["StartTime"] = Stopwatch.GetTimestamp();
_logger.LogInformation(
"Creating order for customer {CustomerId}",
command.CustomerId);
}
protected override async ValueTask OnAfterExecuteAsync(
CreateOrderCommand command,
OrderResult result,
IMutationContext context,
CancellationToken ct) {
// Calculate duration
var startTime = (long)context.Items["StartTime"]!;
var elapsed = Stopwatch.GetElapsedTime(startTime);
_logger.LogInformation(
"Order {OrderId} created in {ElapsedMs}ms",
result.OrderId,
elapsed.TotalMilliseconds);
// Send notification
await _notifications.NotifyAsync(
$"Order {result.OrderId} confirmed",
ct);
}
protected override ValueTask<OrderResult?> OnErrorAsync(
CreateOrderCommand command,
Exception ex,
IMutationContext context,
CancellationToken ct) {
_logger.LogError(ex,
"Failed to create order for customer {CustomerId}",
command.CustomerId);
// Rethrow the exception
return ValueTask.FromResult<OrderResult?>(null);
}
}
Transport-Specific Behavior¶
The hooks defined in MutationEndpointBase<TCommand, TResult> are inherited by transport-specific base classes:
- FastEndpoints:
RestMutationEndpointBase<TCommand, TResult> - HotChocolate:
GraphQLMutationBase<TCommand, TResult>
Both transports call the hooks in the same order, providing consistent behavior across REST and GraphQL endpoints.
See Also¶
- Custom Request DTOs - Map custom DTOs to commands
- Dispatcher - Command dispatch patterns
- Receptors - Command handlers
Version 1.0.0 - Foundation Release