Table of Contents

Migrating from FunctionalDDD to Trellis

Use this file when

  • You are replacing the released FunctionalDdd.* packages with Trellis.*.
  • You need the net API changes from FunctionalDDD 2.x to the current Trellis surface.

FunctionalDDD is the released predecessor; Trellis has not had a stable release. This guide compares FunctionalDDD with current Trellis APIs, not successive Trellis alpha builds or internally labelled development versions. Alpha-to-alpha changes belong in the changelog.

The source baseline was checked against the published FunctionalDDD RailwayOrientedProgramming 2.1.10 package and its source commit. Read the destination package reference for complete current signatures. The developer guide gives the recommended migration order. For new Trellis code, use the router.

Core and package migration

Packages and namespaces

FunctionalDDD package Trellis replacement
<PackageReference Include="FunctionalDdd.RailwayOrientedProgramming" /> Trellis.Core
<PackageReference Include="FunctionalDdd.DomainDrivenDesign" /> Trellis.Core
<PackageReference Include="FunctionalDdd.CommonValueObjects" /> Trellis.Primitives; custom scalar base classes now live in Trellis.Core
<PackageReference Include="FunctionalDdd.CommonValueObjectGenerator" /> Remove the separate reference; the generator is bundled in Trellis.Core
<PackageReference Include="FunctionalDdd.Asp" /> Trellis.Asp
<PackageReference Include="FunctionalDdd.FluentValidation" /> Trellis.FluentValidation

Update package references and centrally managed versions together. Replace using FunctionalDdd; and global imports with using Trellis;. FunctionalDDD's response extensions also lived in the root namespace; add using Trellis.Asp; where the new response-mapping APIs are used. Trellis targets .NET 10, so update the consuming project's target framework and SDK. Packages such as Trellis.Mediator, Trellis.Authorization, Trellis.EntityFrameworkCore, Trellis.Http, and Trellis.StateMachine are optional additions, not renamed FunctionalDDD packages.

Result factories and access

FunctionalDDD API / behavior Current Trellis API / migration
Result.Success(value) / Result.Success() Result.Ok(value) / Result.Ok()
Result.Failure<T>(error) / Result.Failure(error) Result.Fail<T>(error) / Result.Fail(error)
Deferred Result.Success(Func<T>) / Result.Failure<T>(Func<Error>) Invoke the factory explicitly: Result.Ok(funcOk()) / Result.Fail<T>(errorFactory())
Result.SuccessIf(...) / Result.FailureIf(...), including tuple and async forms Use a ternary with Result.Ok / Result.Fail<T>; await an async predicate before branching
Result.FromException / Result.FromException<T> Use Result.Try / Result.TryAsync around the operation, or log an already-caught exception and return an appropriate failure; never copy its message into public error detail
Implicit value/error conversion to Result<T> Wrap explicitly with Result.Ok(value) / Result.Fail<T>(error)
value.ToResult(error) on a nullable reference or struct Result.EnsureNotNull(value, error); Task/ValueTask receivers use EnsureNotNullAsync(error)
Throwing result.Value getter Use TryGetValue, Match, guarded deconstruction, or Map / Bind inside a chain
Throwing result.Error getter on success Error is nullable and never throws; use a null pattern or TryGetError
default(Result<T>) reports success Default is a failure; construct every intended success explicitly

No-payload results keep their shape. FunctionalDDD already returned Result<Unit> from its parameterless success and failure factories. In Trellis, Result.Ok() and Result.Fail(error) also return Result<Unit>; there is no non-generic instance-result migration to perform. IsSuccess and IsFailure retain their names.

For ordinary required fields, prefer the lazy field/detail null guard; construct custom errors inside its factory overload. Maybe<T>.ToResult remains an absence-to-failure conversion; do not replace it with a nullable guard. See choosing a Result entry point.

Railway operations

FunctionalDDD operation Current Trellis operation
TapError(...) / TapErrorAsync(...) TapOnFailure / TapOnFailureAsync
MapError(...) / MapErrorAsync(...) MapOnFailure / MapOnFailureAsync
Compensate(...) / CompensateAsync(...) RecoverOnFailure / RecoverOnFailureAsync
Two-branch Finally(...) / FinallyAsync(...) Match / MatchAsync

For a Finally callback that accepts the whole result rather than separate success/error callbacks, invoke that callback explicitly; do not invent a one-callback Match overload. See current Result extension families.

Error model

FunctionalDDD's open Error class, public constructors, subclasses, and base factories become Trellis's closed Error record catalog. Choose a case by its domain meaning rather than recreating a custom subclass.

FunctionalDDD shape Current Trellis shape
ValidationError / Error.Validation(...) Error.InvalidInput.ForField(code, field, detail: detail) or ForRule(code, detail: detail); preserve all violations
NotFoundError / Error.NotFound(...) Error.NotFound.For<TResource>(id: id, detail: detail); preserve an application code with the named code: argument
new ConflictError(...) Error.Conflict.For<TResource>(code, id: id, detail: detail) or ForReason(code, detail: detail)
new UnauthorizedError(...) new Error.AuthenticationRequired() { Code = code, Detail = detail }
new ForbiddenError(...) Error.Forbidden.ForPolicy(code, detail: detail) or a resource-qualified For factory
UnexpectedError new Error.Unexpected(code, faultId) { Detail = safeDetail }; keep exception details in logs
validationError.FieldErrors with field names and detail lists Error.InvalidInput.Fields / .Rules with input pointers, reason codes, arguments, and detail
Error.Instance ASP owns ProblemDetails.Instance; use ResourceRef for domain resource identity
Base Error equality compares only Code Trellis errors compare their typed value payload and detail; compare Code explicitly when only the reason matters

Case-scoped factories are code-first. Preserve application codes and details, but reconsider clients/tests that depend on the previous error classes, JSON fields, or HTTP mappings. For a simple property, ForField(code, "email", ...) produces /email; use an explicit InputPointer for nested paths and input locations. See current error cases and factories.

Scalar value objects

Custom partial types deriving from FunctionalDDD's RequiredString / RequiredGuid now derive from RequiredString<TSelf> / RequiredGuid<TSelf>. Keep the partial declaration; reuse inherited value/equality members and generated factories rather than redeclaring them. Replace generated NewUnique() calls with NewUniqueV4() to retain random-GUID generation, or choose NewUniqueV7() deliberately for time-ordered IDs.

FunctionalDDD rejected blank required strings and Guid.Empty. Trellis's generated Required types reject only null by default: [NotDefault] rejects the sentinel, while [Trim] enables string trimming. [NotDefault] alone does not reject whitespace-only strings; combining [Trim, NotDefault] rejects them but also changes stored text by trimming. Use ValidateAdditional when preserving the old nonblank-but-untrimmed string behavior. See Required defaults and opt-ins.

ASP.NET Core and observability

Replace FunctionalDDD's ToHttpResult(...) / ToActionResult(...) and async forms with ToHttpResponse / ToHttpResponseAsync. For typed MVC signatures, chain AsActionResult<T> / AsActionResultAsync<T>. Register AddTrellisAsp and use the ASP reference for Created locations and failure mapping. Do not serialize a raw Result<T>; Trellis requires explicit HTTP mapping or payload extraction.

Telemetry subscriptions change too: "Functional DDD ROP" becomes "Trellis.Results", and "Functional DDD CVO" becomes "Trellis.Primitives". Replace AddFunctionalDddRopInstrumentation() with AddTrellisResultsInstrumentation and AddFunctionalDddCvoInstrumentation() with AddTrellisPrimitivesInstrumentation. Update collectors, filters, and tests that use the old source names.