Table of Contents

Trellis — start here

You are looking at versioned Trellis API reference files, installed from the Trellis.Core package by the optional Trellis.AgentDocs local tool (agentdocs; approve Trellis.Core in .agentdocs/policy.json). This file is the routing head for the whole set: it tells you which reference to open for a task and which recipe to read. It is self-contained on purpose, so it is the only file you must read before writing Trellis code. The other files sit beside it, so every link below is relative.

Read the references yourself — do not delegate them to a sub-agent

A sub-agent hands back a summary, so the exact signatures never reach your context and you end up writing code against a paraphrase. That is how invented APIs and wrong overloads get produced, and it is the specific failure these references exist to prevent.

Sub-agents are fine for work whose output is a verdict rather than knowledge you must write code against: running builds and tests, searching for a file, auditing something for accuracy. The rule is narrow — if the answer determines the code you are about to write, read it yourself.

Do not guess

Every Trellis API you write should be traceable to a line in one of these files. If you cannot find the method, overload, or attribute you are about to use, stop and read the reference that owns it rather than reconstructing the signature from memory. Trellis makes heavy use of source generators, so a base type such as RequiredGuid<TSelf> or Aggregate<TId> already supplies members that are a compile error to redeclare — the cookbook's Recipe 1 lists that inherited surface explicitly.

How to read these recipes

Hold this file resident; read recipe bodies on demand. This file is the routing head for the whole set and routes every task. The 37 recipe bodies in trellis-api-cookbook.md are on demand, and a typical task needs one to three of them — so open a body when the task lookup table sends you to one, rather than loading all of them up front. Every live recipe is reachable from that table (enforced by the repository's TRLDOC007 lint gate), so if a task is not listed there, no recipe covers it. Never write code from a recipe's title alone.

Every recipe follows the same shape:

  1. Problem statement — what the consumer is trying to accomplish.
  2. Solution code — copy-pasteable C# that compiles against the documented public surface only. No invented APIs.
  3. What it shows — the cross-cutting concept being demonstrated.
  4. Anti-pattern → fix (when applicable) — the wrong way and which Trellis analyzer catches it.

Conventions used throughout:

  • All Trellis types live in the Trellis namespace except where called out (Trellis.Asp, Trellis.Asp.Authorization, Trellis.EntityFrameworkCore, Trellis.Analyzers).
  • Snippets use C# 12+ features (file-scoped namespaces, primary constructors, collection expressions) — Trellis targets net10.0.
  • Result.Ok / Result.Fail are the construction APIs. default(Result<T>) is a typed failure; do not rely on it as success.
  • Every async pipeline uses *Async extensions; mixing sync chain methods with Task<Result<T>> triggers TRLS009.
  • Examples reference an OrderId : RequiredGuid<OrderId> value object and an Order aggregate. Substitute your own types without changing the structure.
  • A command without a payload returns Result<Trellis.Unit> — qualified because a file that imports both Trellis and Mediator has two Unit types in scope (Trellis.Unit and Mediator.Unit). Add global using TrellisUnit = Trellis.Unit; (or global using Unit = Trellis.Unit;, since Trellis code never references Mediator.Unit) to your GlobalUsings.cs to drop the qualification.

Preferred patterns, not just valid overloads

Primary recipe examples are the recommended defaults. Use an alternative only when its documented trade-off fits the task, and state why:

  • For required nullables, prefer Result.EnsureNotNull(value, fieldName, detail) (or the async form): standard required-field errors are already lazy. These guards check null only, not blank strings or other domain rules. For nonblank strings, use value.EnsureNotNullOrWhiteSpace(fieldName, detail); it emits ValueNotNull for null and ValueNotEmpty for empty/whitespace, only on failure, and does not trim valid text. Use its Func<Error> overload for a custom failure.
  • For custom guard/conversion errors, construct the error inside the factory whenever that overload exists: Result.Ensure(condition, () => new Error.Forbidden(...)), Result.EnsureNotNull(value, () => ...), or maybe.ToResult(() => ...). Value-threaded Ensure uses value => error, not a parameterless factory. Eager error construction wastes work and can record validation violations even when a guard passes or an earlier failure skips it.
  • Prefer static callbacks when they need no captured state. Capturing factories can still allocate a closure; lazy construction avoids unused errors, not every allocation. Returning an already-created error from a factory does not undo its allocation. Eager overloads remain appropriate for existing/reused errors and APIs without a factory overload; do not invent missing overloads.
  • Use Result.Ensure for a boolean guard without a payload, EnsureNotNull for required nullable values, and Result.Ok(value) for deliberate success wrapping. Do not substitute a Result<Unit> guard for a payload-bearing success.
  • Prefer expression-based EF mappings such as builder.OwnsMany(order => order.LineItems, ...) when convention can bind the backing field. String-based field mappings are a fallback, not the default; value-object collections need no explicit ownership mapping unless overriding a convention.

LLM preflight: load the smallest correct reference set

Before writing Trellis code, choose the task in the lookup table below, then open the required package references at their Patterns Index. Read the complete selected API section, its constraints and linked behavioral/preflight sections; do not load an unrelated full catalog. Expand when the task uses another surface, inherited members, or cross-package ordering. Section-first reading does not remove any required package or safety contract. The cookbook supplies end-to-end composition; a direct API route is enough for a small guard or conversion task.

If you are changing... Load these references before coding Why
Result, Maybe, errors, value-object bases, aggregates, specifications, pagination Core index; selected recipe for end-to-end work Core owns the ROP primitives and DDD base types used by every package.
ASP.NET endpoints, controllers, response mapping, ETags, Prefer, ranges, actor providers ASP index, Core index; add Mediator preflight when endpoints send messages ToHttpResponse and scalar validation are ASP-owned, while handlers and result shapes come from Core/Mediator.
Mediator handlers, pipeline behaviors, validation, authorization, domain events Mediator preflight, Core index, selected recipe; EF unit-of-work and authorization index for composed stages Preserve cross-package pipeline ordering and opt-in behavior contracts.
EF Core persistence, repositories, unit of work, Maybe<T> queries, [OwnedEntity] EF index, Core index, selected recipe; Mediator preflight when commits happen through handlers EF owns mapping/interceptors; Mediator owns when command commits run.
FluentValidation integration FluentValidation index, Mediator preflight, selected recipe FluentValidation plugs into ValidationBehavior through IMessageValidator<TMessage>; it is not a separate pipeline behavior.
Composition-root helpers (AddTrellis, UseXxx) ServiceDefaults index, selected recipe, plus the selected modules' API and ordering sections TrellisServiceBuilder preserves canonical order but does not register app-owned services like DbContext or Mediator handlers.
HTTP client adapters HTTP index, Core index, selected recipe The HTTP package maps upstream responses into Core Result<T> / Maybe<T> shapes.
Tests Testing index; add ASP testing index for WebApplicationFactory or .http replay Unit/helper assertions and ASP integration helpers live in separate test packages.
Analyzer diagnostics Matching WRONG/FIX section, then the diagnostic index and its detail link, then the named package API section Adapt the canonical control-flow shape; check the exact API and code-fix limitations.

Measurable completion check for generated code: every Trellis method call should be traceable to a loaded package reference, every selected integration module should be wired in the documented order, and every public API or behavior change should update the matching package reference plus the matching recipe in trellis-api-cookbook.md (and this router's lookup row) when it affects a cross-package recipe.

Known non-APIs and corrected assumptions:

Do not write Correct source-backed statement
WithDocumentPerVersion() No Trellis API with this name exists.
MapScalarApiReference() Sample-app helper only; not a Trellis framework API.
Place UseScalarValueValidation() anywhere Add it before routing/endpoints that deserialize request bodies.
Mutate IAuthorize.RequiredPermissions RequiredPermissions is an IReadOnlyList<string>.
IValidate.Validate() returns Result The declared return type is IResult.

Patterns Index

Task -> recipe lookup

Use this table before writing code. If a task matches a row, read that recipe first.

Task Start here
Create or load an aggregate with value objects Recipe 1
Write a command handler that validates and persists Recipe 2, then Recipe 16
Load multiple independent resources in one handler (HTTP + DB, two upstream services, factory-created DbContexts) Recipe 21
Multi-aggregate orchestration: side effect per element of a related-aggregate set Recipe 22
Apply an operation to every element of a related-aggregate set where per-element validation can fail (reserve stock per line item, etc.) — avoid partial mutation Recipe 25
Concurrency control on mutating endpoints — require or optionally honor If-Match before mutation Recipe 23
Save bandwidth on reads — return 304 Not Modified when the client's If-None-Match still matches Recipe 6
Add a paginated list query Recipe 3
Generate the same named-route pagination links with or without API versioning HttpContext.PageUrl, then optional versioning policy for versioned hosts
Parse cursor/limit query input in MVC or Minimal APIs without treating ?cursor= as missing HttpRequestPaginationExtensions, then Recipe 3
Paginate a translated spherical distance or an application-computed score with validated continuation state bound to query context Recipe 40
Validate geographic coordinates, calculate in-memory distance, build conservative bounds, or compose an EF Core radius query GeoCoordinate and GeoBounds, then GeoCoordinateExpressions in the EF Core reference for database queries; use Recipe 40 for distance pagination
Model weekly availability, overnight periods, or time-zone-aware membership WeeklySchedule and WeeklyPeriod in the Primitives reference; use Recipe 13's DTO seam for JSON
Add Minimal API or MVC endpoints Recipe 4, Recipe 5
Return 201 for a PUT upsert without an application-layer URL, or let the endpoint supply a Created Location Recipe 4, then write outcomes
Generate versioned Location links to a named route or MVC action, including cross-route segment pins Recipe 4, then target-aware API versioning
Map primitive DTO fields to value objects Recipe 18
Require nullable values without !, or guard a nullable-returning query Result.EnsureNotNull / EnsureNotNullAsync; add Recipe 18 for command-factory composition
Require a nonblank string without trimming or constructing unused errors EnsureNotNullOrWhiteSpace; no composite recipe is needed
Migrate universal or nullable ToResult calls without losing payloads or treating absence as success Choosing a Result entry point, then Recipe 18
Add resource authorization Recipe 7
Obtain checked actor/resource parameters without provider/accessor constructor dependencies Recipe 7, Recipe 24, Recipe 31, then actor-aware handler bases
Authorize against a related resource one or more navigation hops away (cricket-style fan-out, owner chains) Recipe 24
Enforce tenant isolation on a command (per-command scope check, no base type) Recipe 38
Map Maybe<T> or composite value objects with EF Core Recipe 8, Recipe 13
Choose JSON transport for a composite value object with nested/optional fields Recipe 13's JSON boundary and DTO seam
Map an aggregate's read-only collection with a private backing field Recipe 13's collection mapping
Add optional request/response fields Recipe 14
Read optional HTTP resources where 404 means absent Recipe 19
Choose between fail-fast and accumulating-error collection ops, including indexed validation Recipe 20
Return synchronous Result chains from Task/ValueTask APIs Recipe 2, then AsTask() / AsValueTask() in trellis-api-core.md
Create HTTP-oriented resource errors Use ResourceRef.For<TResource>(id) from trellis-api-core.md
Point ProblemDetails.Instance at the resource that failed, instead of leaving it null or hand-formatting a URI Recipe 28
Add a state transition Recipe 9
Write handler/domain tests Recipe 10
Replay service .http examples in CI, generate fresh GUIDs, or diagnose unresolved placeholders .http replay helpers, then replay variables and failure handling
Write integration tests for a BackgroundService worker Recipe 26
Insert a row idempotently on a unique constraint (de-duplicated worker outbox, "save unless exists") Recipe 27
Match duplicate-key, foreign-key, or retry abort/exhaustion conflicts without copying wire strings FaultCodes vocabulary, then EF save helpers or Recipe 27
Make POST / PATCH safe under client retries with an IETF Idempotency-Key header Recipe 29
Render ASP.NET Core rate-limit rejections as Trellis 429 Problem Details with optional Retry-After RateLimiterOptionsExtensions in the ASP reference
Define domain events Recipe 17
Make domain events survive a crash (transactional outbox) Recipe 35
Publish a stable external contract (integration events) translated from domain events Recipe 36
Connect an inbound message, domain outbox row, translated integration event, and W3C trace Recipe 36, then IntegrationMessageContext and outbox lineage
Show a validation failure in the user's language, or render your own message from a 422 instead of showing the server's English Recipe 39
Fix analyzer warnings Recipe 11
Wire the composition root Recipe 12
Rehydrate an entity from a database row (fail-loud vs Result-track) Recipe 30
Reconstitute an aggregate in a non-EF repository without re-running its factory (no re-validation, no events) Recipe 37
Migrate a FunctionalDDD 2.x application to Trellis Migration guide; not needed for current-API work
Avoid the pipeline-then-handler duplicate load when a command both authorizes and mutates the same resource Recipe 31
Hide existence of sensitive resources — give missing, removed, and withheld resources the same public NotFound, optionally with fixed code/detail Recipe 32
Configure the strict AddJwtBearer validation profile + key-rotation runbook for a gateway-minted internal JWT Moved: xavierjohn/Trellis.Microservices (Recipe 1 in the microservices cookbook)
Stand up the gateway side of the Path B microservices pattern (YARP transform that mints the internal JWT) Moved: xavierjohn/Trellis.Microservices (Recipe 2 in the microservices cookbook)

Mistake-regression routing

These rows route recurring LLM lab mistakes to the most relevant reference before code is written.

If the task involves... Read first Why
Loading independent resources before creating a command result Recipe 21 Sequential awaits over genuinely independent loads (HTTP + DB, two upstream services) serialise latency. Result.ParallelAsync(...).WhenAllAsync() is the framework idiom. Do NOT use against repositories sharing a scoped DbContext — that races EF Core and throws; sequential is correct there.
Overdue/date-filter queries over Maybe<DateTime> Recipe 8, then trellis-api-efcore.md Keep a typed specification and use MaybeQueryableExtensions in EF queries.
State transitions on an aggregate Recipe 9, then trellis-api-statemachine.md Keep transition methods consistent and put domain mutation after FireResult succeeds.
Cross-aggregate mutation such as cancel/return releasing stock Recipe 1, Recipe 2, and trellis-api-core.md The application handler orchestrates multiple aggregates; an aggregate mutates only itself.
Single-loop mutate-as-you-validate over a collection of related aggregates (reserve stock per line item, etc.) Recipe 25 A later element's validation failure leaves earlier elements partially mutated in memory. Validate every fallible domain check across every participating aggregate before the first state-changing call.
Result-returning ASP endpoints Recipe 4, Recipe 5, then trellis-api-asp.md AddTrellisAsp() is required for Result-to-HTTP mapping; exception middleware is not the mapper.
Pagination endpoints with bound string? cursor / int? limit parameters HttpRequestPaginationExtensions, then Recipe 3 MVC can normalize a present empty cursor to null, and host integer binding can return 400 before Trellis emits a coded 422. Parse Request.Query through TryCreatePageRequest(), declare query input origin for downstream cursor decoding, and declare OpenAPI parameters separately.
Failure-code OpenAPI metadata or .http examples trellis-api-asp.md, trellis-api-testing-aspnetcore.md Generated APIs need failure paths, not happy-path-only docs/tests.
Reading operands out of a failure's detail text — splitting "Valid values: ", parsing bounds out of a sentence Recipe 39 Prose belongs to whichever producer noticed the failure, and one failure can have several producers wording it differently. Code is stable and Args carries the operands as typed values.
Sorting by non-unique/computed values, reusing a cursor with different filters, or seeking after Take Recipe 3, Recipe 40 Ordering and seek must share a unique tie-breaker; validate query context and apply the boundary before limiting the candidates.
Resource authorization guards Recipe 7, then trellis-api-authorization.md Use Result.Ensure for owner/admin boolean guards.

The rest of the set

File What it covers
trellis-api-cookbook.md End-to-end recipes spanning packages. Open a recipe body when the lookup above sends you to one.
trellis-api-core.md Result<T>, Maybe<T>, Error, aggregates, entities, specifications, pagination.
trellis-api-migration.md FunctionalDDD-to-Trellis package, namespace and API changes; open only when migrating a FunctionalDDD application.
trellis-api-analyzers.md The analyzer and generator diagnostics, TRLS001-TRLS066, and TrellisDiagnosticIds.
trellis-api-anti-patterns.md Ready-to-apply WRONG/FIX shapes for the analyzer diagnostics (TRLSxxx).
trellis-value-object-taxonomy.md Choosing a value-object category: scalar, symbolic, structured, optional.

The remaining trellis-api-*.md files cover the optional packages — trellis-api-efcore.md for Trellis.EntityFrameworkCore, and so on. The lookup tables above name the right one per task, so route through them rather than opening files speculatively.

A file being present here does not mean the project you are editing references that package. The complete first-party set ships with Trellis.Core, so references for packages you have not installed are present by design — that is how you discover a module worth adopting. The agentdocs command installs the references of the packages you approved in .agentdocs/policy.json and removes them when the package is no longer approved or no longer publishes guidance. Packages published from other repositories (for example Trellis.ServiceLevelIndicators) ship their own reference alongside themselves, so those appear only once installed.

So before writing code against one of these files, confirm the package is actually referenced by the project you are editing — check its .csproj or Directory.Packages.props. If it is not, the reference still tells you what adopting the package would buy; say that, rather than emitting code that cannot compile.