Table of Contents

Trellis.ServiceDefaults API Reference

Package: Trellis.ServiceDefaults
Namespace: Trellis.ServiceDefaults
Purpose: Opinionated composition builder for API/composition-root projects that want Trellis integration modules applied in the canonical order.

See also: trellis-api-cookbook.md — composition-root recipe.

Use this file when

  • You are wiring a composition root and want Trellis modules applied in the canonical order.
  • You want one fluent builder for ASP, Mediator, FluentValidation, resource authorization, actor provider, and EF unit-of-work registration.
  • You need to know what AddTrellis(...) deliberately does not register.

Patterns Index

Goal Canonical API / pattern See
Enable ASP Result-to-HTTP mapping services.AddTrellis(o => o.UseAsp()) TrellisServiceBuilder, ASP
Enable scalar-value JSON / model-binding validation services.AddTrellis(o => o.UseScalarValueValidation()) (compose with .UseAsp() for the controller-host default) TrellisServiceBuilder, ASP
Enable Trellis ProblemDetails customization .UseProblemDetails() TrellisServiceBuilder, ASP AddTrellisProblemDetails
Enable the IETF Idempotency-Key middleware for opted-in POST / PATCH endpoints .UseIdempotency(opt => ...) plus services.AddInMemoryIdempotencyStore() (or an EF-backed store) and app.UseTrellisIdempotency() TrellisServiceBuilder, ASP Trellis.Asp.Idempotency, Cookbook Recipe 29
Add standard mediator behaviors .UseMediator() TrellisServiceBuilder, Mediator
Add FluentValidation adapter/scanning .UseFluentValidation(typeof(Program).Assembly) or .UseFluentValidation() TrellisServiceBuilder, FluentValidation
Add resource authorization .UseResourceAuthorization(...) TrellisServiceBuilder, Mediator resource authorization
Authorize an identified resource using a shared loader without scanning .UseSharedResourceAuthorization<TMessage,TResource,TId,TResponse>() plus a separately registered shared-loader implementation TrellisServiceBuilder
Register an actor provider .UseClaimsActorProvider(), .UseNestedJsonPathClaimsActorProvider(), .UseEntraActorProvider(), or .UseDevelopmentActorProvider(). For microservices consuming gateway-minted internal JWTs, install Trellis.Microservices.AspNetCore and call services.AddTrellisInternalJwtActorProvider(...) directly (no TrellisServiceBuilder slot — the UseTrellisInternalJwtActor slot was removed in v3 cleanup when the implementation moved). TrellisServiceBuilder, ASP actor providers
Add EF unit-of-work behavior .UseEntityFrameworkUnitOfWork<TContext>() TrellisServiceBuilder, Mediator UoW
Relay domain events durably via a transactional outbox .UseOutbox<TContext>(configure?) TrellisServiceBuilder, Outbox reference
Dispatch domain events from successful commands .UseDomainEvents(typeof(MyHandler).Assembly) or .UseDomainEvents() TrellisServiceBuilder, Mediator domain events
Publish a translated external contract (integration events) via the outbox .UseIntegrationEvents(typeof(MyHandler).Assembly) or .UseIntegrationEvents() TrellisServiceBuilder, Outbox integration events
Consume integration events idempotently (deduplicate transport redeliveries) .UseInbox<TContext>(o => o.ConsumerId = "orders") TrellisServiceBuilder, Inbox reference
Auto-dispatch domain events from every tracked aggregate (outcome-DTO commands) .UseTrackedAggregateDomainEvents(typeof(MyHandler).Assembly) or .UseTrackedAggregateDomainEvents() TrellisServiceBuilder, Mediator tracked dispatch

Common traps

  • AddTrellis(...) does not register DbContext, Mediator handlers, or route constraints — those are always application-owned. Assembly-scanning overloads discover validators, resource loaders, and domain-event handlers; parameterless overloads leave per-type registrations to you. Typed overloads register the specified components without scanning. In particular, UseSharedResourceAuthorization<TMessage,TResource,TId,TResponse>() registers the per-message adapter but leaves the SharedResourceLoaderById<TResource,TId> implementation application-owned.
  • UseEntityFrameworkUnitOfWork<TContext>() is applied last so transaction commit remains innermost in the mediator pipeline.
  • Calling UseEntityFrameworkUnitOfWork<TContext>() more than once (with the same or a different TContext) throws InvalidOperationException. The Trellis pipeline supports exactly one transactional IUnitOfWork per composition; chaining two calls (e.g. for a read/write context split) is always misconfiguration. Use a separate composition root or a single multi-tenant DbContext instead.
  • UseOutbox<TContext>() registers only the relay hosted service; you must also call AddTrellisOutboxInterceptor() on the context options and AddTrellisOutbox() in OnModelCreating. Calling UseOutbox more than once throws InvalidOperationException (one outbox relay per composition). It is order-independent versus UseEntityFrameworkUnitOfWork<TContext>() because the relay is a hosted service, not a pipeline behavior.
  • UseInbox<TContext>() registers only the inbound dispatcher, the EF dedup store, and InboxOptions; you must also call AddTrellisInbox() in OnModelCreating to map the TrellisInboxMessages table, and register the IIntegrationEventHandler<TEvent> implementations that consume the messages. InboxOptions.ConsumerId is required, so the configure callback that sets it is mandatory. Calling UseInbox more than once throws InvalidOperationException (one inbox per composition). It is order-independent versus UseEntityFrameworkUnitOfWork<TContext>() because the dispatcher is an inbound seam, not a pipeline behavior.
  • If you only need one module, direct package-specific registration remains valid; the builder is for composition-root clarity.
  • Shared-loader adapters are fallbacks: when UseResourceAuthorization(assemblies) discovers a custom per-message loader, it replaces an adapter installed by an earlier direct AddSharedResourceAuthorization call. Scanning never replaces an application-provided implementation, factory, or instance registration, and leaves keyed registrations untouched.

AOT compatibility

Trellis.ServiceDefaults is AOT- and trim-compatible (<IsAotCompatible>true</IsAotCompatible>, <IsTrimmable>true</IsTrimmable>, <EnableAotAnalyzer>true</EnableAotAnalyzer>, <EnableTrimAnalyzer>true</EnableTrimAnalyzer>). The compatibility surface is split across two overload shapes per assembly-scanning slot:

Slot AOT-safe overload Scanning overload ([RequiresUnreferencedCode] + [RequiresDynamicCode])
FluentValidation o.UseFluentValidation() (adapter only) plus o.UseFluentValidation<TValidator, TMessage>() per validator o.UseFluentValidation(asm)
Resource authorization (direct) o.UseResourceAuthorization() (pipeline only) plus o.UseResourceAuthorization<TMessage, TResource, TResponse>() per command o.UseResourceAuthorization(asm)
Resource authorization (shared loader) o.UseSharedResourceAuthorization<TMessage, TResource, TId, TResponse>() per message; register SharedResourceLoaderById<TResource,TId> separately o.UseResourceAuthorization(asm)
Resource authorization (indirect / via) o.UseResourceAuthorization() (pipeline only) plus o.UseRelatedResourceAuthorization<TMessage, TLeaf, TLeafId, TOwner, TOwnerId, TResponse>(extractOwnerId) per command, or the ResolvedAuthorizationPath overload for multi-hop / fan-out o.UseResourceAuthorization(asm) (the scanner discovers IAuthorizeResourceVia<TOwner> commands too)
Domain events (response-shape) o.UseDomainEvents() (publisher + behavior only) plus o.UseDomainEvents<TEvent, THandler>() per handler o.UseDomainEvents(asm)
Domain events (tracked-aggregate) o.UseTrackedAggregateDomainEvents() (publisher + behavior only) plus o.UseTrackedAggregateDomainEvents<TEvent, THandler>() per handler o.UseTrackedAggregateDomainEvents(asm)

The AOT-safe overloads use only open-generic DI registrations and explicit closed-type method calls — no reflection over assemblies. The scanning overloads remain available for fast iteration in non-AOT consumers and surface the IL2026 / IL3050 warnings at the consumer's call site so the choice between AOT and convenience is explicit, never silent.

Native AOT with struct Result<T>. Use Mediator's literal closed-generator pipeline and direct typed resource registrations. Native DI cannot close open behaviors over value-type responses. Do not call UseMediator or slots that imply it in this configuration.

Direct per-package registrations (services.AddTrellisFluentValidation() from Trellis.Mediator.FluentValidation, services.AddResourceAuthorization<TMessage, TResource, TResponse>(), services.AddDomainEventHandler<TEvent, THandler>()) remain valid as an escape hatch — call them outside the builder when you need to register a type the builder does not yet model.

Types

TrellisServiceCollectionExtensions

public static class TrellisServiceCollectionExtensions
Signature Returns Description
public static IServiceCollection AddTrellis(this IServiceCollection services, Action<TrellisServiceBuilder> configure) IServiceCollection Creates a TrellisServiceBuilder, lets the caller select modules, then applies the selected modules in canonical order. Does not register DbContext or Mediator handlers.

TrellisServiceBuilder

public sealed class TrellisServiceBuilder
Signature Returns Description
public TrellisServiceBuilder UseAsp(Action<TrellisAspOptions>? configure = null) TrellisServiceBuilder Registers Trellis.Asp integration via AddTrellisAsp(...) (error-to-status mapping + ResourceCollectionNameRegistry). Does NOT register scalar-value validation — compose with UseScalarValueValidation() when the host binds value-object DTOs from JSON / route / query. Repeated calls compose the configure delegates rather than overwriting.
public TrellisServiceBuilder UseScalarValueValidation() TrellisServiceBuilder Registers scalar-value validation via AddScalarValueValidation(): configures both MVC and Minimal API JSON pipelines (model binders, JSON converters, SuppressModelStateInvalidFilter toggle) so IScalarValue<TSelf, TPrimitive> / Maybe<T> validation surfaces as RFC 9457 ProblemDetails. Mutates global MvcOptions / JsonOptions. Independent of UseAsp(). Does NOT register the Minimal API endpoint filter or middleware — Minimal API hosts must additionally call app.UseScalarValueValidation() (middleware) and chain .WithScalarValueValidation() per endpoint. Idempotent.
public TrellisServiceBuilder UseProblemDetails() TrellisServiceBuilder Registers Trellis ProblemDetails customization (traceId on every error, 405 Allow header projected as extensions.allow, 500 detail rewrite) via AddTrellisProblemDetails(). Independent of UseAsp() — does not pull in Trellis MVC/result-mapping infrastructure. Idempotent across direct + builder composition: a consumer that calls both services.AddTrellisProblemDetails() directly and options.UseProblemDetails() ends up with exactly one Trellis post-configure layer.
public TrellisServiceBuilder UseIdempotency(Action<IdempotencyOptions>? configure = null) TrellisServiceBuilder Registers AddTrellisIdempotency(configure): startup-validated IdempotencyOptions, the default IIdempotencyScopeResolver (per-actor, falling back to anonymous), and an internal marker used by app.UseTrellisIdempotency() for startup validation. Does not register a store — composition is explicit; pair with services.AddInMemoryIdempotencyStore() (dev / tests) or an EF-backed store (production). Mount the middleware with app.UseTrellisIdempotency() in the request pipeline. Independent of UseAsp(). Repeated calls compose the configure delegates rather than overwriting, mirroring UseAsp / UseMediator.
public TrellisServiceBuilder UseMediator(Action<TrellisMediatorTelemetryOptions>? configureTelemetry = null) TrellisServiceBuilder Registers Trellis Mediator behaviors via AddTrellisBehaviors(...). Repeated calls compose the configure delegates rather than overwriting.
public TrellisServiceBuilder UseFluentValidation() TrellisServiceBuilder AOT-safe. Registers the FluentValidation adapter only; pair with UseFluentValidation<TValidator, TMessage>() or explicit per-validator DI registrations. Implies UseMediator().
public TrellisServiceBuilder UseFluentValidation<TValidator, TMessage>() where TValidator : class, IValidator<TMessage> TrellisServiceBuilder AOT-safe. Registers TValidator as the IValidator<TMessage> and wires the FluentValidation adapter. Implies UseMediator().
[RequiresUnreferencedCode] [RequiresDynamicCode] public TrellisServiceBuilder UseFluentValidation(params Assembly[] assemblies) TrellisServiceBuilder Registers the FluentValidation adapter and scans the supplied assemblies for IValidator<T> implementations (non-AOT). Implies UseMediator().
public TrellisServiceBuilder UseResourceAuthorization() TrellisServiceBuilder AOT-safe. Enables the resource-authorization pipeline without scanning. Implies UseMediator().
public TrellisServiceBuilder UseSharedResourceAuthorization<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TMessage, TResource, TId, TResponse>() where TMessage : IAuthorizeResource<TResource>, IIdentifyResource<TResource, TId>, IMessage where TResource : class where TResponse : IResult, IFailureFactory<TResponse> TrellisServiceBuilder AOT-safe. Registers the scoped resource-authorization behavior, authorized-resource accessor, and shared-loader adapter via AddSharedResourceAuthorization<TMessage,TResource,TId,TResponse>(). The shared-loader implementation and actor provider remain application-owned; existing per-message loaders are preserved. Implies UseMediator() without requiring a separate UseResourceAuthorization() call. Idempotent across direct, low-level, scanned, and builder registration. Applied in the existing typed resource-authorization stage before validation and the unit of work, regardless of fluent call order. Dual-mode messages are rejected when the builder applies registrations.
public TrellisServiceBuilder UseResourceAuthorization<TMessage, TResource, TResponse>() where TMessage : IAuthorizeResource<TResource>, IMessage where TResource : class where TResponse : IResult, IFailureFactory<TResponse> TrellisServiceBuilder AOT-safe. Registers the closed-generic ResourceAuthorizationBehavior<TMessage, TResource, TResponse> for the named command and registers IAuthorizedResource<TMessage, TResource> (AuthorizedResourceHolder<TMessage, TResource>) so handlers can avoid a duplicate load — see Recipe 31. Idempotent across direct + builder composition: a consumer that calls both services.AddResourceAuthorization<TMessage, TResource, TResponse>() directly and options.UseResourceAuthorization<TMessage, TResource, TResponse>() ends up with exactly one behavior descriptor and a registered accessor. The where TResource : class constraint matches the underlying behavior — value-type resources are rejected at compile time. Implies UseMediator().
[RequiresUnreferencedCode] [RequiresDynamicCode] public TrellisServiceBuilder UseResourceAuthorization(params Assembly[] assemblies) TrellisServiceBuilder Scans assemblies for IAuthorizeResource<TResource> commands and registers ResourceAuthorizationBehavior<TMessage, TResource, TResponse> for each (non-AOT). Implies UseMediator().
public TrellisServiceBuilder UseResourceAuthorization(Action<Trellis.Mediator.ResourceAuthorizationOptions> configure) TrellisServiceBuilder Configures ResourceAuthorizationOptions: HideExistence<TResource>() or HideExistence<TAuthorizationResource,TPublicResource>() gives missing, removed, and withheld resources the same public NotFound; both forms accept optional fixed code/detail. Propagate<TResource>() opts out. Repeated callbacks compose in registration order. Implies UseMediator(), but needs a typed / scan / explicit registration to wire resource behaviors. Throws ArgumentNullException for null configure. See Recipe 32.
public TrellisServiceBuilder UseRelatedResourceAuthorization<TMessage, TLeaf, TLeafId, TOwner, TOwnerId, TResponse>(Func<TLeaf, TOwnerId?> extractOwnerId) where TMessage : IAuthorizeResourceVia<TOwner>, IIdentifyResource<TLeaf, TLeafId>, IMessage where TLeaf : class where TOwner : class where TOwnerId : notnull where TResponse : IResult, IFailureFactory<TResponse> TrellisServiceBuilder AOT-safe. The via-command counterpart to UseResourceAuthorization<TMessage, TResource, TResponse>(). Registers the closed-generic ResourceAuthorizationViaBehavior<TMessage, TLeaf, TOwner, TResponse> for a command that authorizes against an owner reached by a single hop from its leaf, and registers IAuthorizedResource<TMessage, TLeaf> so handlers inject the leaf (the mutation target), not the owner. Returning null from extractOwnerId short-circuits to Error.Forbidden. Loaders are consumer-owned: register SharedResourceLoaderById<TLeaf, TLeafId>, SharedResourceLoaderById<TOwner, TOwnerId>, and an IResourceLoader<TMessage, TLeaf>; a missing loader throws at request time rather than masking as a 403. Idempotent and order-independent relative to UseEntityFrameworkUnitOfWork<TContext>(). Implies UseMediator(). Throws ArgumentNullException when extractOwnerId is null.
public TrellisServiceBuilder UseRelatedResourceAuthorization<TMessage, TLeaf, TOwner, TResponse>(ResolvedAuthorizationPath path) where TMessage : IAuthorizeResourceVia<TOwner>, IMessage where TLeaf : class where TResponse : IResult, IFailureFactory<TResponse> TrellisServiceBuilder AOT-safe. Same as above but takes a hand-built ResolvedAuthorizationPath for multi-hop chains, plural-terminal fan-out, or composite shapes the single-hop overload cannot express. The path's MessageType / LeafType / OwnerType must agree with the generic arguments; the behavior's constructor validates this and fails fast. Implies UseMediator(). Throws ArgumentNullException when path is null.
public TrellisServiceBuilder UseClaimsActorProvider(Action<ClaimsActorOptions>? configure = null) TrellisServiceBuilder Registers ClaimsActorProvider as IActorProvider. Mutually exclusive with the other actor-provider selectors; a second actor-provider selector throws InvalidOperationException.
public TrellisServiceBuilder UseNestedJsonPathClaimsActorProvider(Action<NestedJsonPathClaimsActorOptions>? configure = null) TrellisServiceBuilder Registers NestedJsonPathClaimsActorProvider as IActorProvider with startup validation for the container-claim invariant. Mutually exclusive with the other actor-provider selectors; a second actor-provider selector throws InvalidOperationException. When configure is omitted, the default empty JSON paths make the provider behave like ClaimsActorProvider.
public TrellisServiceBuilder UseEntraActorProvider(Action<EntraActorOptions>? configure = null) TrellisServiceBuilder Registers EntraActorProvider as IActorProvider. Mutually exclusive with the other actor-provider selectors; a second actor-provider selector throws InvalidOperationException.
public TrellisServiceBuilder UseEasyAuthActorProvider(Action<ClaimsActorOptions>? configure = null) TrellisServiceBuilder Registers EasyAuthClaimsActorProvider as IActorProvider for Azure App Service / Container Apps "Easy Auth". Actor mapping only — pair with AddAuthentication(...).AddEasyAuth() so the X-MS-CLIENT-PRINCIPAL header is decoded onto HttpContext.User first. Mutually exclusive with the other actor-provider selectors; a second actor-provider selector throws InvalidOperationException.
public TrellisServiceBuilder UseDevelopmentActorProvider(Action<DevelopmentActorOptions>? configure = null) TrellisServiceBuilder Registers DevelopmentActorProvider as IActorProvider. Mutually exclusive with the other actor-provider selectors; a second actor-provider selector throws InvalidOperationException. Use only in development/testing hosts.
public TrellisServiceBuilder UseCachingActorProvider<T>() where T : class, IActorProvider TrellisServiceBuilder Wraps the inner IActorProvider registration with a per-request caching decorator via AddCachingActorProvider<T>(). Chain after the matching UseXxxActorProvider(...) call so the inner provider's IOptions<TOptions> is configured first. Throws InvalidOperationException on repeated call.
public TrellisServiceBuilder UseWorkerActor(Actor systemActor) TrellisServiceBuilder Wraps the existing unkeyed IActorProvider via AddTrellisWorkerActor(systemActor). A builder actor-provider selector is not required: a compatible registration added to IServiceCollection beforehand also works. The wrapper returns systemActor when IHttpContextAccessor.HttpContext is null and delegates otherwise. Apply() runs after actor-provider selection and caching, regardless of fluent call order. Exactly one unkeyed inner registration is required. Throws InvalidOperationException on repeated calls, missing/multiple registrations, singleton implementation type/factory, or transient lifetime. Scoped registrations and singleton instances are supported; keyed registrations are ignored.
[RequiresUnreferencedCode] [RequiresDynamicCode] public TrellisServiceBuilder UseEntityFrameworkUnitOfWork<TContext>() where TContext : DbContext TrellisServiceBuilder Registers EfUnitOfWork<TContext> and TransactionalCommandBehavior<,> via AddTrellisUnitOfWork<TContext>(). Implies UseMediator() and is applied last. Annotated non-AOT because the underlying Trellis.EntityFrameworkCore package opts out of AOT/trim. Throws InvalidOperationException on repeated call — see "Common traps".
public TrellisServiceBuilder UseDomainEvents() TrellisServiceBuilder AOT-safe. Registers DomainEventDispatchBehavior<,> and the default IDomainEventPublisher without scanning. Implies UseMediator(). Mutually exclusive with UseTrackedAggregateDomainEvents(...).
public TrellisServiceBuilder UseDomainEvents<TEvent, THandler>() where TEvent : IDomainEvent where THandler : class, IDomainEventHandler<TEvent> TrellisServiceBuilder AOT-safe. Registers THandler for TEvent and wires the dispatch behavior. Implies UseMediator(). Mutually exclusive with UseTrackedAggregateDomainEvents(...).
[RequiresUnreferencedCode] [RequiresDynamicCode] public TrellisServiceBuilder UseDomainEvents(params Assembly[] assemblies) TrellisServiceBuilder Scans assemblies for IDomainEventHandler<TEvent> implementations (non-AOT). Implies UseMediator(). Mutually exclusive with UseTrackedAggregateDomainEvents(...).
public TrellisServiceBuilder UseTrackedAggregateDomainEvents() TrellisServiceBuilder AOT-safe. Registers TrackedAggregateDomainEventDispatchBehavior<,> and the default IDomainEventPublisher without scanning. Implies UseMediator(). Mutually exclusive with UseDomainEvents(...).
public TrellisServiceBuilder UseTrackedAggregateDomainEvents<TEvent, THandler>() where TEvent : IDomainEvent where THandler : class, IDomainEventHandler<TEvent> TrellisServiceBuilder AOT-safe. Registers THandler for TEvent and wires the tracked dispatch behavior. Implies UseMediator(). Mutually exclusive with UseDomainEvents(...).
[RequiresUnreferencedCode] [RequiresDynamicCode] public TrellisServiceBuilder UseTrackedAggregateDomainEvents(params Assembly[] assemblies) TrellisServiceBuilder Scans assemblies for IDomainEventHandler<TEvent> implementations (non-AOT). Implies UseMediator(). Mutually exclusive with UseDomainEvents(...).
[RequiresUnreferencedCode] [RequiresDynamicCode] public TrellisServiceBuilder UseOutbox<TContext>(Action<OutboxOptions>? configure = null) where TContext : DbContext TrellisServiceBuilder Registers the transactional-outbox relay via AddTrellisOutbox<TContext>(). Order-independent versus UseEntityFrameworkUnitOfWork (the relay is a hosted service, not a pipeline behavior). Throws InvalidOperationException if called more than once. The capture interceptor and table mapping are wired separately on the DbContext. Annotated non-AOT. See Trellis.EntityFrameworkCore.Outbox.
[RequiresUnreferencedCode] [RequiresDynamicCode] public TrellisServiceBuilder UseInbox<TContext>(Action<InboxOptions> configure) where TContext : DbContext TrellisServiceBuilder Registers the transactional inbox via AddTrellisInbox<TContext>(): the IInboxDispatcher, the EF IInboxStore, and InboxOptions (whose ConsumerId is required, so configure is mandatory). Order-independent versus UseEntityFrameworkUnitOfWork (the dispatcher is an inbound seam, not a pipeline behavior). Throws InvalidOperationException if called more than once. The TrellisInboxMessages table mapping is wired separately with modelBuilder.AddTrellisInbox(). Annotated non-AOT. See Trellis.EntityFrameworkCore.Inbox.
public TrellisServiceBuilder UseIntegrationEvents() TrellisServiceBuilder AOT-safe. Registers the default in-process IIntegrationEventPublisher and the scoped IIntegrationEventCollector (no scanning). Pair with UseDomainEvents() (for translators) and UseOutbox<TContext>() (for delivery).
public TrellisServiceBuilder UseIntegrationEvents<TEvent, THandler>() where TEvent : IIntegrationEvent where THandler : class, IIntegrationEventHandler<TEvent> TrellisServiceBuilder AOT-safe. Registers THandler for TEvent and wires the default publisher and collector.
[RequiresUnreferencedCode] [RequiresDynamicCode] public TrellisServiceBuilder UseIntegrationEvents(params Assembly[] assemblies) TrellisServiceBuilder Scans assemblies for IIntegrationEventHandler<TEvent> implementations (non-AOT) and registers the default publisher and collector. See Outbox integration events.

Behavior

Domain-event dispatch uses IUnitOfWorkScope.IsOwner to distinguish a real outer commit from a deferred inner success. Both dispatch slots suppress inner publication/clearing; response-shape mode retains nested aggregate responses even when the outer response is DTO/Unit. Failure/throw discards the batch without clearing events, while outbox capture still runs on a successful FailAfterCommit save. Manually owned external unit-of-work scopes require explicit post-commit dispatch instead of the automatic behaviors.

UseOutbox<TContext>() validates IReportingDomainEventPublisher during host startup before the relay starts. When integration services are registered, it also validates IIntegrationEventPublisher; domain-only hosts do not need one. UseIntegrationEvents() registers a translator-only collector: Add throws outside the relay's active BeginTranslation() lease.

AddTrellis(...) records selected modules first and then applies them in this order:

  1. ASP integration.
  2. Scalar-value validation (when UseScalarValueValidation() is selected).
  3. ProblemDetails customization (when UseProblemDetails() is selected).
  4. Idempotency-Key middleware DI (when UseIdempotency(...) is selected).
  5. Actor provider (the optional caching wrap that chains after it, then the optional worker-actor wrap that chains after caching).
  6. Mediator behaviors.
  7. Resource authorization (assembly scanning when selected, then typed registrations including UseSharedResourceAuthorization, then failure-exposure options).
  8. FluentValidation adapter/scanning when selected.
  9. Domain event dispatch (registers DomainEventDispatchBehavior<,>, the default IDomainEventPublisher, and any scanned handlers), plus tracked-aggregate domain events when selected.
  10. Integration event dispatch (when UseIntegrationEvents(...) is selected).
  11. EF Core Unit of Work.
  12. Transactional outbox relay (when UseOutbox<TContext>() is selected).
  13. Transactional inbox dispatch registration (when UseInbox<TContext>() is selected).

That order preserves the important pipeline invariant: TransactionalCommandBehavior<,> is the innermost behavior, closest to the handler, so commit failures remain visible to outer logging/tracing/exception behaviors. The lower-level registration helpers are also order-independent: if a transaction behavior is present before AddTrellisBehaviors() or domain-event dispatch runs, it is rehomed to the innermost slot.

Mediator's order is Exception -> Tracing -> Logging -> AuthorizationContext -> static authorization -> direct/via resource authorization -> Validation -> selected event dispatch -> TransactionalCommand -> handler. UseMediator and the resource slots supply the integral AuthorizationContextBehavior automatically: there is no separate actor handler toggle. Known closed/open context descriptors normalize to one applicable frame. Options before or after a two-assembly application/persistence scan compose normally; repeat callbacks run in registration order and scans do not duplicate execution. See actor-aware bases and migration.

Repeated configuration callbacks

Optional version-aware pagination uses the existing ASP callback: UseAsp(asp => asp.UseVersionedPageUrls()), with the extension supplied by Trellis.Asp.ApiVersioning. Unversioned applications keep UseAsp() and the same HttpContext.PageUrl endpoint expression. There is no new registration helper or builder slot: neither this package nor Trellis.Asp references the optional versioning SDK. The host-local TrellisAspOptions.PageUrlRouteResolver policy composes with other ASP configuration.

Repeated calls to UseAsp, UseIdempotency, and UseMediator invoke every configure callback in registration order on the same options instance. Contravariant callbacks such as Action<object> can be mixed with options-specific callbacks in either order. Calls with no callback leave the existing callbacks intact.

Order-independence for explicit resource-authorization registrations

Explicit services.AddResourceAuthorization<TMessage, TResource, TResponse>() calls made BEFORE AddTrellis(...) are now order-independent. AddTrellisBehaviors() (called by UseMediator()) detects any pre-existing closed-generic ResourceAuthorizationBehavior<TMessage, TResource, TResponse> descriptors and re-positions them to sit immediately before ValidationBehavior<,>, so they end up in the canonical pipeline envelope regardless of registration order. This mirrors the symmetry between AddTrellisUnitOfWork<TContext> and AddDomainEventDispatch.

AddTrellis(...) deliberately does not register:

  • AddDbContext<TContext>(...) — provider, connection string, pooling, migrations, and interceptors are application-owned.
  • AddMediator(...) — handler discovery/source-generator configuration is application-owned.
  • route constraints — route parameter names are application-owned.

Examples

// Error-to-status mapping only (no scalar-value JSON / model-binding wiring).
services.AddTrellis(options => options.UseAsp());
// Controller-host default: error mapping + scalar-value validation.
services.AddTrellis(options => options
    .UseAsp()
    .UseScalarValueValidation()
    .UseMediator()
    .UseFluentValidation(typeof(Program).Assembly));
// Adapter only; validators are registered explicitly elsewhere.
services.AddTrellis(options => options
    .UseMediator()
    .UseFluentValidation());
// No assembly scanning; resource authorization registrations are explicit elsewhere.
services.AddTrellis(options => options
    .UseMediator()
    .UseResourceAuthorization());
services.AddTrellis(options => options
    .UseAsp()
    .UseScalarValueValidation()
    .UseMediator()
    .UseClaimsActorProvider()
    .UseResourceAuthorization(typeof(Program).Assembly)
    .UseEntityFrameworkUnitOfWork<AppDbContext>());
// Domain event dispatch with assembly scanning. Handlers fire after the transaction
// commits because UseDomainEvents is applied before UseEntityFrameworkUnitOfWork.
services.AddTrellis(options => options
    .UseMediator()
    .UseDomainEvents(typeof(Program).Assembly)
    .UseEntityFrameworkUnitOfWork<AppDbContext>());

Registrations without a builder slot

Not every services.AddXxx() Trellis ships has a matching UseXxx() slot, and the omissions are deliberate.

The complete, authoritative classification lives in Trellis.ServiceDefaults/tests/RegistrationSurfaceTests.cs, which reflects over every registration helper reachable from Trellis.ServiceDefaults and fails the build if one is unclassified, if a classified helper no longer exists, if a named slot is missing from TrellisServiceBuilder, or if a helper classified as slotted is never actually referenced by Trellis.ServiceDefaults. The table below is the narrative summary of the cases worth explaining; treat the test as the source of truth if the two ever disagree.

Registration Package Why no slot
AddInMemoryIdempotencyStore Trellis.Asp The default leaf store. UseIdempotency() registers the middleware and options but deliberately no store, so the application picks one explicitly alongside it.
AddTrellisRouteConstraint / AddTrellisRouteConstraints Trellis.Asp Registers per-value-object route constraints — application content, not a feature toggle. The application names the types (or the assembly to scan), so there is nothing for a slot to decide.
AddTransactionalCommandBehavior Trellis.Mediator Provider-neutral; invoked by AddTrellisUnitOfWork<TContext>(), which is surfaced as UseEntityFrameworkUnitOfWork<TContext>().
AddTrellisUnitOfWorkWithoutBehavior Trellis.EntityFrameworkCore An escape hatch for hosts that take over pipeline ordering; a slot would contradict its purpose.
AddCosmosIdempotencyStore Trellis.Asp.Idempotency.Cosmos Vendor SDK.
AddAzureServiceBusIntegrationEventPublisher / AddAzureServiceBusIntegrationEventConsumer Trellis.Messaging.AzureServiceBus Vendor SDK.

The vendor-SDK rule is the important one: Trellis.ServiceDefaults references no cloud SDK, and a builder slot is a compile-time reference. Adding one would make every consumer of the meta-package carry the Azure SDK in order to use features that have nothing to do with Azure. Call these registrations directly on IServiceCollection alongside AddTrellis(...). That same absence from the reference graph is what keeps the vendor packages outside the test's scan, so their "no slot" status needs no allowlist entry.

AddAzureServiceBusIntegrationEventPublisher in particular is order-independent by construction: it replaces any existing IIntegrationEventPublisher registration rather than appending to it, so it can be called before or after AddTrellis(options => options.UseIntegrationEvents(...)) with the same result.