Skip to main content

Built-in Middleware

Dispatch includes middleware for common cross-cutting concerns. Enable them individually or use presets.

Before You Start

Logging Middleware

Structured logging for all message processing:

services.AddDispatch(dispatch =>
{
dispatch.UseLogging(); // Registers LoggingMiddleware with default options
});

Configuration

services.AddDispatch(dispatch =>
{
dispatch.UseLogging(options =>
{
// Log level for successful requests
options.SuccessLevel = LogLevel.Information;

// Log level for failed requests
options.FailureLevel = LogLevel.Error;

// Include message payload in logs (default: false for security)
options.IncludePayload = false;

// Include timing information
options.IncludeTiming = true;

// Exclude specific message types from logging
options.ExcludeTypes.Add(typeof(HealthCheckQuery));
});
});

Log Output

{
"Timestamp": "2025-01-15T10:30:00Z",
"Level": "Information",
"Message": "Message processed successfully",
"Properties": {
"MessageType": "CreateOrderAction",
"MessageId": "abc-123",
"CorrelationId": "xyz-789",
"DurationMs": 45,
"Success": true
}
}

Validation Middleware

Validates messages using FluentValidation or DataAnnotations:

services.AddDispatch(options =>
{
options.ConfigurePipeline("Default", pipeline =>
{
pipeline.Use<ValidationMiddleware>();
});
});

// Register validators
services.AddValidatorsFromAssembly(typeof(Program).Assembly);

FluentValidation Integration

public class CreateOrderValidator : AbstractValidator<CreateOrderAction>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty()
.WithMessage("Customer ID is required");

RuleFor(x => x.Items)
.NotEmpty()
.WithMessage("Order must have at least one item");

RuleForEach(x => x.Items)
.ChildRules(item =>
{
item.RuleFor(x => x.Quantity)
.GreaterThan(0);
});
}
}

DataAnnotations Support

public record CreateOrderAction(
[Required] string CustomerId,
[MinLength(1)] List<OrderItem> Items,
[Range(0, 1000000)] decimal MaxAmount
) : IDispatchAction;

Validation Results

var result = await dispatcher.DispatchAsync(action, ct);

if (!result.IsSuccess && result.ValidationResult is ValidationResult validationResult)
{
foreach (var error in validationResult.Errors)
{
Console.WriteLine($"{error.PropertyName}: {error.Message}");
}
}

Authorization Middleware

Dispatch provides multiple authorization approaches. Choose the one that fits your scenario.

ASP.NET Core Authorization Bridge

Package: Excalibur.Dispatch.Hosting.AspNetCore

For ASP.NET Core applications, the authorization bridge reads standard [Authorize] attributes from message and handler types and evaluates them via ASP.NET Core's IAuthorizationService. The ClaimsPrincipal is sourced from HttpContext.User.

services.AddDispatch(dispatch =>
{
dispatch.UseAspNetCoreAuthorization(options =>
{
options.RequireAuthenticatedUser = true;
options.DefaultPolicy = "MyPolicy"; // optional
});
});

// Register ASP.NET Core authorization policies as usual
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));

options.AddPolicy("CanCreateOrders", policy =>
policy.RequireClaim("permission", "orders:create"));
});

Attribute-Based Authorization

using Microsoft.AspNetCore.Authorization;

[Authorize("AdminOnly")]
public record DeleteUserAction(Guid UserId) : IDispatchAction;

[Authorize("CanCreateOrders")]
public record CreateOrderAction(...) : IDispatchAction;

// Multiple policies (AND logic -- all must pass)
[Authorize("CanCreateOrders")]
[Authorize("IsActive")]
public record CreatePriorityOrderAction(...) : IDispatchAction;

// Role-based (OR logic within a single attribute)
[Authorize(Roles = "Admin,Manager")]
public record ManageUsersAction(...) : IDispatchAction;

// Allow anonymous bypass
[AllowAnonymous]
public record GetPublicDataQuery(...) : IDispatchQuery<PublicData>;

Custom Authorization Requirements

The bridge passes the IDispatchMessage as a resource to AuthorizeAsync, enabling custom AuthorizationHandler<TRequirement, IDispatchMessage> implementations:

public class OrderOwnerRequirement : IAuthorizationRequirement
{
public string ResourceClaim { get; } = "OrderId";
}

public class OrderOwnerHandler : AuthorizationHandler<OrderOwnerRequirement, IDispatchMessage>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OrderOwnerRequirement requirement,
IDispatchMessage resource)
{
if (resource is IOrderMessage orderMessage)
{
var userId = context.User.FindFirst("sub")?.Value;
if (orderMessage.OwnerId == userId)
{
context.Succeed(requirement);
}
}

return Task.CompletedTask;
}
}

Options

OptionDefaultDescription
EnabledtrueEnable/disable the middleware
RequireAuthenticatedUsertrueReject when HttpContext is unavailable or user is unauthenticated. Set to false for background job scenarios.
DefaultPolicynullFallback policy when [Authorize] specifies no explicit policy

A3 Activity-Based Authorization

For grant-based and activity-driven authorization using [RequirePermission] attributes, see Authorization (A3).

Dispatch Core Authorization

The core Excalibur.Dispatch.Middleware.AuthorizationMiddleware provides config-based authorization using IMessageContext. It does not read [Authorize] attributes.

Co-Existence

All three authorization middlewares can be registered in the same pipeline -- they check different attributes and use different identity sources. See the ASP.NET Core authorization bridge documentation for the co-existence model.

Exception Mapping Middleware

Converts exceptions to structured RFC 7807 Problem Details:

services.AddDispatch(dispatch =>
{
dispatch.UseExceptionMapping(); // Registers ExceptionMappingMiddleware
});

Custom Exception Mappers

Register custom IExceptionMapper implementations to control how exceptions are converted:

public class CustomExceptionMapper : IExceptionMapper
{
public IMessageProblemDetails Map(Exception exception)
{
return exception switch
{
ValidationException ex => new MessageProblemDetails
{
Type = "validation-error",
Title = "Validation Failed",
Status = 400,
Detail = string.Join(", ", ex.Errors)
},
NotFoundException ex => new MessageProblemDetails
{
Type = "not-found",
Title = "Resource Not Found",
Status = 404,
Detail = ex.Message
},
UnauthorizedException => new MessageProblemDetails
{
Type = "unauthorized",
Title = "Unauthorized",
Status = 401
},
_ => new MessageProblemDetails
{
Type = "internal-error",
Title = "Internal Server Error",
Status = 500,
Detail = exception.Message
}
};
}

public Task<IMessageProblemDetails> MapAsync(
Exception exception,
CancellationToken cancellationToken)
{
return Task.FromResult(Map(exception));
}

public bool CanMap(Exception exception) => true; // Handles all exception types
}

// Register in DI
services.AddSingleton<IExceptionMapper, CustomExceptionMapper>();

Note: OperationCanceledException is never mapped and is always re-thrown to allow proper cancellation propagation.

Metrics Middleware

OpenTelemetry metrics for observability:

services.AddDispatch(dispatch =>
{
dispatch.UseMetrics(); // Registers MetricsMiddleware
});

services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddDispatchMetrics(); // Adds Excalibur.Dispatch.Core meter
metrics.AddOtlpExporter();
});

Emitted Metrics

MetricTypeDescription
dispatch.messages.processedCounterTotal messages processed
dispatch.messages.durationHistogramProcessing duration in ms
dispatch.messages.publishedCounterMessages published
dispatch.messages.failedCounterFailed messages
dispatch.sessions.activeGaugeActive sessions

Metric Tags

  • message_type: Message class name
  • handler_type: Handler class name
  • success: Whether processing succeeded
  • error_type: Error category (when failed)
  • destination: Publish destination (when publishing)

Tracing Middleware

Distributed tracing with OpenTelemetry:

services.AddDispatch(dispatch =>
{
dispatch.UseTracing(); // Registers TracingMiddleware
});

services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddSource("Excalibur.Dispatch"); // Dispatch activity source
tracing.AddOtlpExporter();
});

Trace Attributes

AttributeDescription
message.typeMessage class name
message.idUnique message ID
handler.typeHandler class name
dispatch.operationOperation type (handle, publish, middleware)
middleware.typeMiddleware class name (for middleware spans)

Retry Middleware

Automatic retry with configurable policies:

services.AddDispatch(dispatch =>
{
dispatch.UseMiddleware<RetryMiddleware>();
});

services.Configure<RetryOptions>(options =>
{
options.MaxAttempts = 3;
options.BaseDelay = TimeSpan.FromMilliseconds(100);
options.MaxDelay = TimeSpan.FromSeconds(30);
options.BackoffMultiplier = 2.0;
options.BackoffStrategy = BackoffStrategy.Exponential;

// Configure retryable exceptions
options.RetryableExceptions.Add(typeof(TransientException));

// Configure non-retryable exceptions (these are never retried)
options.NonRetryableExceptions.Add(typeof(ValidationException));
});

Backoff Strategies

StrategyDescription
FixedSame delay between each attempt
LinearDelay increases linearly (BaseDelay × attempt)
ExponentialDelay doubles each attempt
ExponentialWithJitterExponential with random jitter to prevent thundering herd

Per-Message Retry Policy

[Retry(MaxAttempts = 5, BaseDelayMs = 500)]
public record ImportDataAction(...) : IDispatchAction;

Caching Middleware

Response caching for dispatch actions using .NET HybridCache:

services.AddDispatch(dispatch =>
{
dispatch.AddCaching(); // Registers CachingMiddleware with HybridCache
});

Cache Configuration

[CacheResult(ExpirationSeconds = 300)] // 5 minutes
public record GetProductQuery(string ProductId) : IDispatchAction<Product>;

[CacheResult(ExpirationSeconds = 60, OnlyIfSuccess = true, IgnoreNullResult = true)]
public record GetUserPreferencesQuery(string UserId) : IDispatchAction<UserPreferences>;

Interface-Based Caching

For more control, implement ICacheable<TResult>:

public record GetProductQuery(string ProductId)
: IDispatchAction<Product>, ICacheable<Product>
{
public int ExpirationSeconds => 300;

public bool ShouldCache(Product? result) => result is not null;

public string[] GetCacheTags() => [$"product:{ProductId}"];
}

Cache Invalidation

Implement ICacheInvalidator on messages that should trigger cache invalidation:

public record UpdateProductAction(string ProductId, string Name)
: IDispatchAction, ICacheInvalidator
{
public IEnumerable<string> GetCacheTagsToInvalidate()
=> [$"product:{ProductId}"];

public IEnumerable<string> GetCacheKeysToInvalidate()
=> []; // Or specific cache keys
}

The CacheInvalidationMiddleware automatically invalidates caches when these messages are processed.

Middleware Presets

Use presets for common configurations:

services.AddDispatch(dispatch =>
{
// Development preset: logging (verbose), validation, exception mapping
dispatch.UseDevelopmentMiddleware();

// Production preset: metrics, tracing, retry, exception mapping
dispatch.UseProductionMiddleware();

// Full preset: all middleware with sensible defaults
dispatch.UseFullMiddleware();
});

Preset Contents

PresetMiddleware Included
DevelopmentLogging (Debug level), Validation, ExceptionMapping
ProductionMetrics, Tracing, Retry, ExceptionMapping
FullLogging, Validation, Metrics, Tracing, Retry, ExceptionMapping

Next Steps

See Also