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:
- .NET CLI
- PackageReference
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
<PackageReference Include="UnambitiousFx.Functional" Version="2.0.0" />
<PackageReference Include="UnambitiousFx.Functional.AspNetCore" Version="2.0.0" />
<PackageReference Include="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 (codeERROR).ValidationFailure— one or more validation messages (codeVALIDATION).NotFoundFailure— missing resource (codeNOT_FOUND).ConflictFailure— conflicting operation, e.g. duplicate key (codeCONFLICT).UnauthorizedFailure— access denied (codeUNAUTHORIZED).UnauthenticatedFailure— caller is not authenticated (codeUNAUTHENTICATED).BadRequestFailure— malformed request (codeBAD_REQUEST).TimeoutFailure— operation exceeded its time limit (codeTIMEOUT).ExceptionalFailure— wraps a caughtException(codeEXCEPTION).AggregateFailure— bundles multiple failures (codeAGGREGATE_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, ornet10.0if needed. - Ensure
using UnambitiousFx.Functional.Failures;is present where you build or inspect failures. - Use
IsFailure/IsSuccessandTryGetFailure/TryGetfor state access. - Run your tests and confirm HTTP mapping expectations.