Migrating from FunctionalDDD to Trellis
This guide takes an application using the released FunctionalDDD 2.x packages directly to the current Trellis APIs. Trellis is the renamed framework and has not had a stable release. There is no released "Trellis v1" or "Trellis v2" to upgrade through.
The migration reference is the source-backed comparison of FunctionalDDD and Trellis. Intermediate alpha changes belong in the changelog, not this guide.
Patterns Index
| Task | See |
|---|---|
| Replace FunctionalDDD packages and namespaces | Package map |
| Rename Result factories and railway operations | Result and Error renames |
| Replace throwing result access and implicit conversions | Result access |
| Port open error subclasses and validation errors | Error model |
| Update generated scalar value objects without changing validation accidentally | Value objects |
| Map MVC and Minimal API responses | ASP.NET Core |
| Update tracing subscriptions | Observability |
| Apply the changes in a practical order | Migration order |
Package map (legacy → current)
| FunctionalDDD package | Trellis package | Notes |
|---|---|---|
FunctionalDdd.RailwayOrientedProgramming |
Trellis.Core |
Result, Maybe, Error, and railway operations |
FunctionalDdd.DomainDrivenDesign |
Trellis.Core |
DDD primitives are in the same package as the railway operations |
FunctionalDdd.CommonValueObjects |
Trellis.Primitives |
Built-in value objects; custom scalar base classes live in Trellis.Core |
FunctionalDdd.CommonValueObjectGenerator |
No separate package | The generator is bundled in Trellis.Core |
FunctionalDdd.Asp |
Trellis.Asp |
MVC and Minimal API response mapping |
FunctionalDdd.FluentValidation |
Trellis.FluentValidation |
Standalone FluentValidation-to-Result adapters |
Update PackageReference items and Directory.Packages.props together. Change
FunctionalDdd namespace imports, including global usings, to Trellis. The old response
extensions lived in that root namespace; add Trellis.Asp imports for their replacements.
Trellis targets .NET 10, so update the consuming project's SDK and target framework before restoring.
Trellis's Mediator, authorization, EF Core, HTTP client, state-machine, and testing packages are optional additions. Do not invent corresponding old FunctionalDDD package references or add all Trellis modules just to complete the migration.
Result and Error renames (Trellis.Core)
| FunctionalDDD API | Trellis API |
|---|---|
Result.Success(value) / Result.Success() |
Result.Ok(value) / Result.Ok() |
Result.Failure<T>(error) / Result.Failure(error) |
Result.Fail<T>(error) / Result.Fail(error) |
TapError / TapErrorAsync |
TapOnFailure / TapOnFailureAsync |
MapError / MapErrorAsync |
MapOnFailure / MapOnFailureAsync |
Compensate / CompensateAsync |
RecoverOnFailure / RecoverOnFailureAsync |
Two-branch Finally / FinallyAsync |
Match / MatchAsync |
IsSuccess and IsFailure retain their names. For a whole-result Finally callback,
call the callback explicitly rather than treating it as a one-callback Match.
Deferred factories now invoke the supplied function explicitly. Replace conditional
factories with a ternary using Result.Ok and Result.Fail<T>, awaiting async predicates
before branching. Replace Result.FromException with Result.Try / Result.TryAsync
around an operation, or map an already-caught exception to an appropriate safe failure.
See the complete factory comparison.
Keep no-payload handlers on Result<Unit>. FunctionalDDD already used that shape;
Trellis's parameterless Result.Ok() and Result.Fail(error) still return it. There is
no intermediate non-generic instance-result migration.
Result access
Implicit value/error conversions to Result<T> are removed. Wrap successful values
with Result.Ok(value) and errors with Result.Fail<T>(error).
Replace nullable value.ToResult(error) with Result.EnsureNotNull(value, error),
or EnsureNotNullAsync(error) on a nullable Task/ValueTask result. Prefer field/detail
guards for ordinary required fields and lazy factories for custom errors.
Maybe<T>.ToResult still converts absence to failure and must not become a nullable guard.
See choosing a Result entry point.
The throwing result.Value getter is removed. Extract a payload with TryGetValue,
Match, or guarded deconstruction; inside a railway chain, use Map or Bind.
The Error getter is now nullable and never throws on success; use a null pattern or
TryGetError. Default-initialized results are failures, not successes.
Trellis also rejects direct JSON serialization of result wrappers. HTTP endpoints must map them to responses; other boundaries should extract the payload explicitly. See current Result behavior.
Error model
FunctionalDDD's open error hierarchy becomes the closed Trellis Error record catalog.
Map errors by domain meaning and preserve application codes and safe details:
| FunctionalDDD shape | Trellis shape | |
|---|---|---|
ValidationError / Error.Validation(...) |
Error.InvalidInput.ForField(code, field, detail: detail) / ForRule(code, detail: detail) |
|
NotFoundError |
Error.NotFound.For<TResource>(id: id, detail: detail) |
|
ConflictError |
Error.Conflict.For<TResource>(code, id: id, detail: detail) / ForReason(code, detail: detail) |
|
UnauthorizedError |
new Error.AuthenticationRequired() { Code = code, Detail = detail } |
|
ForbiddenError |
Error.Forbidden.ForPolicy(code, detail: detail) |
|
UnexpectedError |
new Error.Unexpected(code, faultId) { Detail = safeDetail } |
Do not recreate custom subclasses; choose a case that represents the failure.
Factories are code-first. Validation carries field/rule violations rather than the old
field-name/detail-list shape: keep every violation, its code, and its detail, and build
explicit input pointers for nested paths. Error.Instance is not a domain field in
Trellis; the ASP boundary sets ProblemDetails.Instance, while ResourceRef carries
domain resource identity.
Revisit equality assertions and client error parsing. FunctionalDDD's base error equality compared codes; Trellis errors use typed value equality. Old JSON fields and HTTP status mappings are not assumed unchanged. See the error comparison and current factories.
Value objects
Retain partial declarations and change the scalar bases to the self-typed forms:
RequiredString<TSelf> and RequiredGuid<TSelf>. The generator is bundled with
Trellis.Core; do not duplicate inherited value/equality members or generated factories.
Generated NewUnique() calls become NewUniqueV4() for the same random-GUID choice,
or NewUniqueV7() if the application deliberately wants time-ordered IDs.
Review validation, not just compilation. FunctionalDDD rejected blank required
strings and Guid.Empty. Trellis accepts concrete values by default and rejects only
null. [NotDefault] rejects the sentinel; [Trim] opts into string trimming.
For strings, [NotDefault] alone still accepts whitespace-only text.
[Trim, NotDefault] rejects it but also trims valid text, unlike FunctionalDDD's
untrimmed string behavior. Use ValidateAdditional to preserve nonblank validation
without changing stored text. See defaults and opt-ins.
ASP.NET Core (Trellis.Asp)
Register AddTrellisAsp and replace ToHttpResult / ToActionResult and their async
forms with ToHttpResponse / ToHttpResponseAsync. For typed MVC signatures, chain
AsActionResult<T> / AsActionResultAsync<T>.
Created responses and failure mappings are configured through the current response options, not a sequence of intermediate Trellis mapping APIs. Review status codes, locations, and Problem Details bodies in endpoint tests. See the ASP reference.
Observability
| FunctionalDDD subscription | Trellis subscription |
|---|---|
"Functional DDD ROP" |
"Trellis.Results" |
"Functional DDD CVO" |
"Trellis.Primitives" |
AddFunctionalDddRopInstrumentation |
AddTrellisResultsInstrumentation |
AddFunctionalDddCvoInstrumentation |
AddTrellisPrimitivesInstrumentation |
Update collectors, activity listeners, source filters, and tracing tests. The sources are renamed from FunctionalDDD; they are not unchanged subscriptions to older Trellis packages. See observability setup.
Practical guidance
- Keep a passing FunctionalDDD build/test baseline.
- Update the SDK, target framework, package references, and namespace imports.
- Rename factories and railway operations; make implicit Result construction explicit.
- Port the Error model and scalar value-object declarations, preserving intended validation.
- Replace throwing result access and update HTTP mapping and telemetry subscriptions.
- Build and test business outcomes, value-object edge cases, failure bodies, and tracing.
- Adopt additional Trellis integration modules separately if the application needs them.