Trellis.Mediator — API Reference
Package: Trellis.Mediator
Namespace: Trellis.Mediator
Purpose: Provides Trellis result-aware Mediator pipeline behaviors plus DI helpers for validation, authorization, tracing, logging, and optional resource authorization.
See also: trellis-api-cookbook.md — recipes using this package.
Use this file when
- You are wiring Trellis result-aware behaviors into the
Mediatorpipeline. - You need exact command/query interfaces, validation behavior, static authorization, resource authorization, tracing/logging, or EF unit-of-work behavior.
- You need to know which DI helper registers a behavior versus which helper registers resource loaders.
Patterns Index
| Goal | Canonical API / pattern | See |
|---|---|---|
| Add the standard Trellis mediator behaviors | services.AddTrellisBehaviors() |
ServiceCollectionExtensions |
| Add validation to a message | Implement IValidate and register IMessageValidator<TMessage> or FluentValidation adapter |
ValidationBehavior<TMessage,TResponse> |
| Add static permission authorization | Message implements IAuthorize; register AddTrellisBehaviors() |
AuthorizationBehavior<TMessage,TResponse> |
| Add resource authorization with assembly scanning | services.AddResourceAuthorization(typeof(SomeType).Assembly) |
ServiceCollectionExtensions |
| Add resource authorization explicitly | services.AddResourceAuthorization<TMessage,TResource,TResponse>() plus loader registration |
ResourceAuthorizationBehavior<TMessage,TResource,TResponse> |
Bridge IIdentifyResource<TResource,TId> to a shared loader |
services.AddSharedResourceLoader<TMessage,TResource,TId>() |
ServiceCollectionExtensions |
| Register EF unit-of-work behavior | services.AddTrellisUnitOfWork<TContext>() |
Canonical pipeline order |
| Keep commits inside the pipeline | Repositories stage changes; TransactionalCommandBehavior commits on success |
Behavioral notes |
Common traps
- Explicit
AddResourceAuthorization<TMessage,TResource,TResponse>()inserts the behavior only; it does not automatically register the shared-loader bridge. AddTrellisUnitOfWork<TContext>()should be registered after other behavior registrations so the transaction behavior is innermost.- Handlers should return Trellis
Result/Result<T>failures, not throw for expected business outcomes.
Types
AuthorizationBehavior<TMessage, TResponse>
Declaration
public sealed class AuthorizationBehavior<TMessage, TResponse>(IActorProvider actorProvider) : IPipelineBehavior<TMessage, TResponse> where TMessage : IAuthorize, global::Mediator.IMessage where TResponse : IResult, IFailureFactory<TResponse>
Constructors
| Signature | Description |
|---|---|
public AuthorizationBehavior(IActorProvider actorProvider) |
Builds the static-permission behavior. |
Properties
| Name | Type | Description |
|---|---|---|
— |
— |
None. |
Methods
| Signature | Returns | Description |
|---|---|---|
public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken) |
ValueTask<TResponse> |
Resolves the current actor, checks RequiredPermissions with HasAllPermissions, and returns TResponse.CreateFailure(new Error.Forbidden("authorization.insufficient.permissions") { Detail = "Insufficient permissions." }) when authorization fails. Throws InvalidOperationException when no actor can be resolved. |
ExceptionBehavior<TMessage, TResponse>
Declaration
public sealed partial class ExceptionBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse> where TMessage : global::Mediator.IMessage where TResponse : IResult, IFailureFactory<TResponse>
Constructors
| Signature | Description |
|---|---|
public ExceptionBehavior(ILogger<ExceptionBehavior<TMessage, TResponse>> logger) |
Builds the exception-to-failure behavior. |
Properties
| Name | Type | Description |
|---|---|---|
— |
— |
None. |
Methods
| Signature | Returns | Description |
|---|---|---|
public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken) |
ValueTask<TResponse> |
Catches unhandled exceptions except OperationCanceledException, logs them, and returns TResponse.CreateFailure(new Error.InternalServerError(Guid.NewGuid().ToString("N")) { Detail = "An unexpected error occurred while processing the request." }). |
IValidate
Declaration
public interface IValidate
Constructors
No public constructors.
Properties
| Name | Type | Description |
|---|---|---|
— |
— |
None. |
Methods
| Signature | Returns | Description |
|---|---|---|
IResult Validate() |
IResult |
Returns success to continue or any failure result to short-circuit the pipeline. |
LoggingBehavior<TMessage, TResponse>
Declaration
public sealed partial class LoggingBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse> where TMessage : global::Mediator.IMessage where TResponse : IResult
Constructors
| Signature | Description |
|---|---|
public LoggingBehavior(ILogger<LoggingBehavior<TMessage, TResponse>> logger, TrellisMediatorTelemetryOptions? options = null) |
Builds the logging behavior. options is resolved from DI; when null (i.e. not registered) the safe-by-default options are used and Error.Detail is redacted. |
Properties
| Name | Type | Description |
|---|---|---|
— |
— |
None. |
Methods
| Signature | Returns | Description |
|---|---|---|
public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken) |
ValueTask<TResponse> |
Logs start (Information), end with elapsed milliseconds (Information on success, Warning on failure). On failure emits error.Code only by default; the free-text Error.Detail is included only when TrellisMediatorTelemetryOptions.IncludeErrorDetail is true. |
ResourceAuthorizationBehavior<TMessage, TResource, TResponse>
Declaration
public sealed class ResourceAuthorizationBehavior<TMessage, TResource, TResponse>(IActorProvider actorProvider, IServiceProvider serviceProvider) : IPipelineBehavior<TMessage, TResponse> where TMessage : IAuthorizeResource<TResource>, global::Mediator.IMessage where TResponse : IResult, IFailureFactory<TResponse>
Constructors
| Signature | Description |
|---|---|
public ResourceAuthorizationBehavior(IActorProvider actorProvider, IServiceProvider serviceProvider) |
Builds the resource-loading authorization behavior. |
Properties
| Name | Type | Description |
|---|---|---|
— |
— |
None. |
Methods
| Signature | Returns | Description |
|---|---|---|
public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken) |
ValueTask<TResponse> |
Resolves the actor from IActorProvider first (throws InvalidOperationException when null — fail fast before doing any I/O). Then resolves IResourceLoader<TMessage, TResource> from the current scope, returns loader failures directly, and finally calls message.Authorize(actor, loadResult.Unwrap()) before invoking the handler. This behavior is only active when registered explicitly or via AddResourceAuthorization(...); it is not included in AddTrellisBehaviors() or PipelineBehaviors. |
ServiceCollectionExtensions
Declaration
public static class ServiceCollectionExtensions
Constructors
No public constructors.
Properties
| Name | Type | Description |
|---|---|---|
PipelineBehaviors |
IReadOnlyList<Type> |
Ordered pipeline behavior types (outermost → innermost): ExceptionBehavior<,>, TracingBehavior<,>, LoggingBehavior<,>, AuthorizationBehavior<,>, ValidationBehavior<,>. Resource authorization and the EFCore TransactionalCommandBehavior are opt-in and not part of this list. |
Methods
| Signature | Returns | Description |
|---|---|---|
public static IServiceCollection AddTrellisBehaviors(this IServiceCollection services) |
IServiceCollection |
Registers the five open generic behaviors listed in PipelineBehaviors and a default TrellisMediatorTelemetryOptions singleton (Detail redacted). Idempotent — uses TryAddEnumerable/TryAddSingleton so repeat calls (e.g. from plug-in extensions like AddTrellisFluentValidation, AddTrellisAsp) do not duplicate registrations. |
public static IServiceCollection AddTrellisBehaviors(this IServiceCollection services, Action<TrellisMediatorTelemetryOptions> configure) |
IServiceCollection |
Same as the parameterless overload, but applies configure to the registered TrellisMediatorTelemetryOptions singleton. Replaces any prior options registration so this call wins regardless of ordering. |
public static IServiceCollection AddResourceAuthorization<TMessage, TResource, TResponse>(this IServiceCollection services) where TMessage : IAuthorizeResource<TResource>, global::Mediator.IMessage where TResponse : IResult, IFailureFactory<TResponse> |
IServiceCollection |
Registers ResourceAuthorizationBehavior<TMessage, TResource, TResponse> and inserts it immediately before ValidationBehavior<,> when validation is already registered. |
[RequiresUnreferencedCode("Assembly scanning requires unreferenced types. Use explicit registration for AOT/trimming scenarios.")] [RequiresDynamicCode("Constructs closed generic types at runtime. Use explicit registration for AOT scenarios.")] public static IServiceCollection AddResourceAuthorization(this IServiceCollection services, params Assembly[] assemblies) |
IServiceCollection |
Scans assemblies for IAuthorizeResource<TResource> implementations, resolves TResponse from ICommand<T>, IQuery<T>, or IRequest<T>, registers closed ResourceAuthorizationBehavior<,,> instances, registers discovered IResourceLoader<,> implementations, registers discovered SharedResourceLoaderById<,> implementations, and bridges IIdentifyResource<TResource, TId> messages to shared loaders when no explicit loader is registered. |
[RequiresUnreferencedCode("Assembly scanning requires unreferenced types. Use explicit registration for AOT/trimming scenarios.")] public static IServiceCollection AddResourceLoaders(this IServiceCollection services, Assembly assembly) |
IServiceCollection |
Registers discovered IResourceLoader<,> implementations with TryAddScoped. |
public static IServiceCollection AddSharedResourceLoader<TMessage, TResource, TId>(this IServiceCollection services) where TMessage : IAuthorizeResource<TResource>, IIdentifyResource<TResource, TId> |
IServiceCollection |
Registers SharedResourceLoaderAdapter<TMessage, TResource, TId> as IResourceLoader<TMessage, TResource>. |
TracingBehavior<TMessage, TResponse>
Declaration
public sealed class TracingBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse> where TMessage : global::Mediator.IMessage where TResponse : IResult
Constructors
| Signature | Description |
|---|---|
public TracingBehavior(TrellisMediatorTelemetryOptions? options = null) |
Builds the tracing behavior. options is resolved from DI; when null (i.e. not registered) the safe-by-default options are used and Error.Detail is redacted from Activity.StatusDescription. |
Fields
| Name | Type | Description |
|---|---|---|
ActivitySourceName |
string |
Public constant activity source name. Value: "Trellis.Mediator". |
Properties
| Name | Type | Description |
|---|---|---|
— |
— |
None. |
Methods
| Signature | Returns | Description |
|---|---|---|
public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken) |
ValueTask<TResponse> |
Starts an activity named after TMessage. On failure tags the activity with error.code (the stable Error.Code) and error.type (the stable error class name); sets ActivityStatusCode.Error. The StatusDescription is left empty by default — the free-text Error.Detail is included only when TrellisMediatorTelemetryOptions.IncludeErrorDetail is true. On success sets ActivityStatusCode.Ok. Rethrows thrown exceptions after marking the activity as error and tagging error.type; the exception message is not copied into telemetry. |
TrellisMediatorTelemetryOptions
Declaration
public sealed class TrellisMediatorTelemetryOptions
Operator-tunable redaction settings consumed by LoggingBehavior and TracingBehavior. Resolved from DI; when not registered the behaviors fall back to a default-constructed instance (Detail redacted).
Properties
| Name | Type | Description |
|---|---|---|
IncludeErrorDetail |
bool |
When true, the logging and tracing behaviors include Error.Detail in their emitted message and activity status description. Defaults to false (Detail is redacted; only the stable Error.Code and type name are emitted). |
IMessageValidator
Declaration
public interface IMessageValidator<in TMessage>
where TMessage : global::Mediator.IMessage
Extensibility hook for the unified validation stage. Implementations are resolved from DI as IEnumerable<IMessageValidator<TMessage>> by ValidationBehavior<TMessage, TResponse>; every registered validator runs before the handler. External packages (e.g., Trellis.FluentValidation) plug additional validation sources into the pipeline through this interface without taking a dependency on a specific validation library or message-side interface from Trellis.Mediator.
Methods
| Signature | Returns | Description |
|---|---|---|
ValueTask<IResult> ValidateAsync(TMessage message, CancellationToken cancellationToken) |
ValueTask<IResult> |
Returns Result.Ok() on success, or Result.Fail(new Error.UnprocessableContent(...)) with field/rule violations on failure. Error.UnprocessableContent failures from every validator (and IValidate.Validate() if implemented) are aggregated into a single response failure by ValidationBehavior. Returning a non-Error.UnprocessableContent failure (e.g., Error.Conflict, Error.Forbidden) is allowed but short-circuits the stage immediately and is propagated as-is. |
ValidationBehavior<TMessage, TResponse>
Declaration
public sealed class ValidationBehavior<TMessage, TResponse>(IEnumerable<IMessageValidator<TMessage>> validators) : IPipelineBehavior<TMessage, TResponse> where TMessage : global::Mediator.IMessage where TResponse : IResult, IFailureFactory<TResponse>
Unified validation stage. Runs IValidate.Validate() (when the message implements IValidate) and every IMessageValidator<TMessage> registered in DI for the message, then aggregates Error.UnprocessableContent failures into a single response. The behavior is registered for all messages — when the message does not implement IValidate and no validators are registered it is a no-op pass-through.
Constructors
| Signature | Description |
|---|---|
public ValidationBehavior(IEnumerable<IMessageValidator<TMessage>> validators) |
Receives every IMessageValidator<TMessage> registered in DI. The collection is iterated once per request. |
Properties
| Name | Type | Description |
|---|---|---|
— |
— |
None. |
Methods
| Signature | Returns | Description |
|---|---|---|
public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken) |
ValueTask<TResponse> |
Aggregation rules: (1) Multiple Error.UnprocessableContent failures from IValidate and validators are merged into a single Error.UnprocessableContent whose Fields and Rules collect every reported violation. (2) An Error.UnprocessableContent with empty Fields AND empty Rules still short-circuits the handler — original failure semantics are preserved. (3) A non-Error.UnprocessableContent failure returned by any source short-circuits the stage immediately and is propagated as-is. |
Extension methods
Trellis.Mediator.ServiceCollectionExtensions
public static IServiceCollection AddTrellisBehaviors(this IServiceCollection services)
public static IServiceCollection AddTrellisBehaviors(this IServiceCollection services, Action<TrellisMediatorTelemetryOptions> configure)
public static IServiceCollection AddResourceAuthorization<TMessage, TResource, TResponse>(this IServiceCollection services) where TMessage : IAuthorizeResource<TResource>, global::Mediator.IMessage where TResponse : IResult, IFailureFactory<TResponse>
[RequiresUnreferencedCode("Assembly scanning requires unreferenced types. Use explicit registration for AOT/trimming scenarios.")]
[RequiresDynamicCode("Constructs closed generic types at runtime. Use explicit registration for AOT scenarios.")]
public static IServiceCollection AddResourceAuthorization(this IServiceCollection services, params Assembly[] assemblies)
[RequiresUnreferencedCode("Assembly scanning requires unreferenced types. Use explicit registration for AOT/trimming scenarios.")]
public static IServiceCollection AddResourceLoaders(this IServiceCollection services, Assembly assembly)
public static IServiceCollection AddSharedResourceLoader<TMessage, TResource, TId>(this IServiceCollection services) where TMessage : IAuthorizeResource<TResource>, IIdentifyResource<TResource, TId>
Interfaces
public interface IValidate
public interface IMessageValidator<in TMessage> where TMessage : global::Mediator.IMessage
Behavioral notes
Canonical pipeline order
The Trellis pipeline executes outermost → innermost in this order. The first five are registered by AddTrellisBehaviors(); the last two are opt-in.
ExceptionBehavior<,>— catches unhandled exceptions (exceptOperationCanceledException), logs them, and converts them to a typedTResponse.CreateFailure(new Error.InternalServerError(...)). Sits outermost so every other layer is wrapped.TracingBehavior<,>— opens an OpenTelemetryActivityper message under the"Trellis.Mediator"activity source. On failure tagserror.code/error.typeand setsActivityStatusCode.Error.Error.Detailis redacted fromStatusDescriptionunlessTrellisMediatorTelemetryOptions.IncludeErrorDetailistrue.LoggingBehavior<,>— structured logging with start/end and elapsed-ms entries; emits the stableError.Codeon failure. Inherits the same correlation context propagated by the surroundingActivity.Error.Detailis redacted unlessIncludeErrorDetailistrue.AuthorizationBehavior<,>— runs forIAuthorizemessages; resolves the actor and rejects withnew Error.Forbidden("authorization.insufficient.permissions")whenRequiredPermissionsare not satisfied. No I/O.ResourceAuthorizationBehavior<,,>(opt-in viaAddResourceAuthorization(...)) — runs forIAuthorizeResource<TResource>messages. Inserted immediately beforeValidationBehavior<,>so a 403 short-circuits before a 422 is computed. Resolves the actor first (fail-fast, no I/O when null), then loads the resource viaIResourceLoader<TMessage, TResource>and callsmessage.Authorize(actor, resource).ValidationBehavior<,>— unified validation stage. RunsIValidate.Validate()if implemented, then everyIMessageValidator<TMessage>resolved from DI; aggregates allError.UnprocessableContentfailures into a single response. External validation sources (e.g., theTrellis.FluentValidationadapter) participate here without occupying their own pipeline slot.TransactionalCommandBehavior<,>(opt-in, lives inTrellis.EntityFrameworkCore, not registered byAddTrellisBehaviors()) — wraps the handler forICommand<TResponse>messages and callsIUnitOfWork.CommitAsyncon success. Register viaAddTrellisUnitOfWork<TContext>()from the EFCore package afterAddTrellisBehaviors()so it lands innermost (closest to the handler) and commit failures remain visible to outer logging/tracing. Queries are skipped.
Code examples
Registering behaviors and shared resource authorization
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Mediator;
using Microsoft.Extensions.DependencyInjection;
using Trellis;
using Trellis.Authorization;
using Trellis.Mediator;
var services = new ServiceCollection();
services.AddScoped<IActorProvider, StaticActorProvider>();
services.AddScoped<SharedResourceLoaderById<Order, OrderId>, OrderResourceLoader>();
services.AddTrellisBehaviors();
services.AddResourceAuthorization<GetOrderQuery, Order, Result<Order>>();
services.AddSharedResourceLoader<GetOrderQuery, Order, OrderId>();
var behaviorOrder = Trellis.Mediator.ServiceCollectionExtensions.PipelineBehaviors;
Console.WriteLine(string.Join(", ", behaviorOrder.Select(type => type.Name)));
public sealed partial class OrderId : RequiredGuid<OrderId>;
public sealed partial class ActorId : RequiredString<ActorId>;
public sealed record Order(OrderId Id, ActorId OwnerId);
public sealed record GetOrderQuery(OrderId Id)
: IQuery<Result<Order>>, IAuthorize, IAuthorizeResource<Order>, IIdentifyResource<Order, OrderId>, IValidate
{
public IReadOnlyList<string> RequiredPermissions => ["orders:read"];
public OrderId GetResourceId() => Id;
public IResult Validate() => Result.Ok();
public IResult Authorize(Actor actor, Order resource) =>
resource.OwnerId.Value == actor.Id
? Result.Ok()
: Result.Fail(new Error.Forbidden("orders.read") { Detail = "Only the owner can view the order." });
}
public sealed class OrderResourceLoader : SharedResourceLoaderById<Order, OrderId>
{
public override Task<Result<Order>> GetByIdAsync(OrderId id, CancellationToken cancellationToken) =>
Task.FromResult(ActorId.TryCreate("user-1").Map(ownerId => new Order(id, ownerId)));
}
// Escape hatch: prefer IIdentifyResource<TResource, TId> + SharedResourceLoaderById<TResource, TId> in generated services.
// services.AddScoped<IResourceLoader<GetOrderQuery, Order>, GetOrderResourceLoader>();
public sealed class StaticActorProvider : IActorProvider
{
public Task<Actor> GetCurrentActorAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(Actor.Create("user-1", new HashSet<string> { "orders:read" }));
}
Assembly scanning registration
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Trellis.Mediator;
var services = new ServiceCollection();
Assembly[] assemblies = [typeof(SomeMessageInApplicationAssembly).Assembly];
services.AddTrellisBehaviors();
services.AddResourceAuthorization(assemblies);
public sealed class SomeMessageInApplicationAssembly { }