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:
- Problem statement — what the consumer is trying to accomplish.
- Solution code — copy-pasteable C# that compiles against the documented public surface only. No invented APIs.
- What it shows — the cross-cutting concept being demonstrated.
- Anti-pattern → fix (when applicable) — the wrong way and which Trellis analyzer catches it.
Conventions used throughout:
- All Trellis types live in the
Trellisnamespace 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.Failare the construction APIs.default(Result<T>)is a typed failure; do not rely on it as success.- Every async pipeline uses
*Asyncextensions; mixing sync chain methods withTask<Result<T>>triggersTRLS009. - Examples reference an
OrderId : RequiredGuid<OrderId>value object and anOrderaggregate. Substitute your own types without changing the structure. - A command without a payload returns
Result<Trellis.Unit>— qualified because a file that imports bothTrellisandMediatorhas twoUnittypes in scope (Trellis.UnitandMediator.Unit). Addglobal using TrellisUnit = Trellis.Unit;(orglobal using Unit = Trellis.Unit;, since Trellis code never referencesMediator.Unit) to yourGlobalUsings.csto 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, usevalue.EnsureNotNullOrWhiteSpace(fieldName, detail); it emitsValueNotNullfor null andValueNotEmptyfor empty/whitespace, only on failure, and does not trim valid text. Use itsFunc<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, () => ...), ormaybe.ToResult(() => ...). Value-threadedEnsureusesvalue => 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
staticcallbacks 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.Ensurefor a boolean guard without a payload,EnsureNotNullfor required nullable values, andResult.Ok(value)for deliberate success wrapping. Do not substitute aResult<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.