Skip to main content

Migration from v1

This guide helps you migrate existing UnambitiousFx.Functional code from v1 to v2.

The migration is mostly mechanical. The pipeline APIs you already use — Bind, Map, Ensure, Tap, Recover, and Match — keep their familiar shapes, so most composition code carries over unchanged.

1. Update packages

Bump every UnambitiousFx.Functional package to v2:

dotnet add package UnambitiousFx.Functional --version 2.0.0
dotnet add package UnambitiousFx.Functional.AspNetCore --version 2.0.0
dotnet add package UnambitiousFx.Functional.xunit --version 2.0.0

If you pin versions centrally, update them in your Directory.Packages.props.

2. Target framework

v2 supports net8.0, net9.0, and net10.0. If your project targets an older framework, retarget it before upgrading:

<TargetFramework>net8.0</TargetFramework>

3. Failure types and the Failures namespace

Failures live in the UnambitiousFx.Functional.Failures namespace. Make sure your files import it where you construct or inspect errors:

using UnambitiousFx.Functional;
using UnambitiousFx.Functional.Failures;

return Result.Failure<User>(new ValidationFailure("Email is required"));

The built-in failure types are:

  • Failure — generic failure (code ERROR).
  • ValidationFailure — one or more validation messages (code VALIDATION).
  • NotFoundFailure — missing resource (code NOT_FOUND).
  • ConflictFailure — conflicting operation, e.g. duplicate key (code CONFLICT).
  • UnauthorizedFailure — access denied (code UNAUTHORIZED).
  • UnauthenticatedFailure — caller is not authenticated (code UNAUTHENTICATED).
  • BadRequestFailure — malformed request (code BAD_REQUEST).
  • TimeoutFailure — operation exceeded its time limit (code TIMEOUT).
  • ExceptionalFailure — wraps a caught Exception (code EXCEPTION).
  • AggregateFailure — bundles multiple failures (code AGGREGATE_ERROR).

Each failure exposes a machine-readable Code (see FailureCodes) alongside its human-readable Message.

4. Result state access

Check result state with IsSuccess and IsFailure:

if (result.IsFailure)
{
// handle the error
}

To get the error out, prefer TryGetFailure, which returns true and a non-null Failure when the result failed:

if (result.TryGetFailure(out var failure))
{
logger.LogWarning("{Code}: {Message}", failure.Code, failure.Message);
}

For typed results, TryGet extracts the value on success or the error on failure in one call:

if (result.TryGet(out var value, out var error))
{
Use(value);
}
else
{
Handle(error);
}

5. Keep your existing pipeline style

Composition code can stay as-is:

return Validate(input)
.Bind(DoWork)
.Ensure(x => x.IsValid, new ValidationFailure("Invalid state"))
.Tap(_ => logger.LogInformation("done"));

6. ASP.NET Core mapping

If you use Functional.AspNetCore, register your failure-to-status mappings and convert results at the edge:

services.AddResultHttp(options =>
{
options.AddMapper<ValidationFailure>(StatusCodes.Status400BadRequest);
options.AddMapper<NotFoundFailure>(StatusCodes.Status404NotFound);
options.AddMapper<ConflictFailure>(StatusCodes.Status409Conflict);
options.AddMapper<UnauthorizedFailure>(StatusCodes.Status401Unauthorized);
});

The default HTTP mapping follows standard semantics (400 / 401 / 404 / 409 / 500 …).

7. xUnit assertions

The assertion style still starts with ShouldBe(). Confirm your typed assertions reference v2 failure names:

result.ShouldBe()
.Failure()
.WhichIsValidationError()
.And(failure => Assert.Contains("email", failure.Message));

Quick migration checklist

  • Update NuGet package versions to 2.0.0.
  • Retarget to net8.0, net9.0, or net10.0 if needed.
  • Ensure using UnambitiousFx.Functional.Failures; is present where you build or inspect failures.
  • Use IsFailure / IsSuccess and TryGetFailure / TryGet for state access.
  • Run your tests and confirm HTTP mapping expectations.

See also