ResultFlow.Core
0.1.0
dotnet add package ResultFlow.Core --version 0.1.0
NuGet\Install-Package ResultFlow.Core -Version 0.1.0
<PackageReference Include="ResultFlow.Core" Version="0.1.0" />
<PackageVersion Include="ResultFlow.Core" Version="0.1.0" />
<PackageReference Include="ResultFlow.Core" />
paket add ResultFlow.Core --version 0.1.0
#r "nuget: ResultFlow.Core, 0.1.0"
#:package ResultFlow.Core@0.1.0
#addin nuget:?package=ResultFlow.Core&version=0.1.0
#tool nuget:?package=ResultFlow.Core&version=0.1.0
ResultFlow
Opinionated, allocation-aware Result pattern for .NET 8+
Eliminate exception-driven control flow. Build composable, testable, railway-oriented pipelines.
Why ResultFlow?
Exception-based error handling has fundamental problems in modern .NET services:
| Problem | Impact |
|---|---|
| Exceptions are expensive | ~10× slower than return values under GC pressure |
| Invisible control flow | Callers can't know what a method can fail with |
| Untestable branching | You need to throw to test error paths |
| Lost context | Stack unwinding discards domain context |
ResultFlow gives you a first-class Result<T> type with a fluent pipeline API — the Railway-Oriented Programming pattern — that is idiomatic in C#, plays well with async/await, and maps cleanly to HTTP response codes.
Packages
Quick Start
dotnet add package ResultFlow.Core
dotnet add package ResultFlow.AspNetCore # optional
dotnet add package ResultFlow.FluentValidation # optional
Core Concepts
Error
An immutable, typed error value:
// Semantic factory methods
var notFound = Error.NotFound("User.NotFound", "User with id 42 does not exist.");
var validation = Error.Validation("User.Email.Invalid", "Email is not a valid address.");
var conflict = Error.Conflict("User.Email.Duplicate", "This email is already registered.");
var unauthorized = Error.Unauthorized("Auth.TokenExpired", "Access token has expired.");
// With structured metadata (e.g., field-level errors)
var withMeta = Error.Validation("Validation.Failed", "Multiple fields failed.", metadata: new Dictionary<string, object>
{
["Email"] = new[] { "Must be a valid email." },
["Username"] = new[] { "Too short.", "Reserved word." },
});
Result<T> and Result
// Success
Result<User> success = Result.Success(user);
Result<User> implicit = user; // implicit cast
// Failure
Result<User> failure = Result.Failure<User>(Error.NotFound("User.NotFound", "..."));
Result<User> implicit2 = Error.NotFound("User.NotFound", "..."); // implicit cast
// Void result
Result voidSuccess = Result.Success();
Result voidFailure = Result.Failure(Error.Unexpected("System.Error", "Database unavailable."));
Pipeline API
Then — bind / flatMap
Chain operations that can themselves fail:
Result<OrderDto> result = await GetUserAsync(userId) // Task<Result<User>>
.ThenAsync(user => GetOrderAsync(user, orderId)) // Task<Result<Order>>
.ThenAsync(order => ApplyDiscountAsync(order, promoCode)) // Task<Result<Order>>
.MapAsync(order => mapper.ToDto(order)); // Task<Result<OrderDto>>
Ensure — inline validation
Result<Order> result = await GetOrderAsync(orderId)
.EnsureAsync(
order => order.Status == OrderStatus.Pending,
Error.Conflict("Order.NotPending", "Only pending orders can be cancelled."));
OnSuccess / OnFailure — side effects (tap)
var result = await GetUserAsync(id)
.OnSuccessAsync(user => logger.LogInformation("Fetched user {Id}", user.Id))
.OnFailureAsync(err => logger.LogWarning("Failed: {Code}", err.Code));
Recover — fallback
var result = await GetFromCacheAsync(key)
.RecoverAsync(err => GetFromDatabaseAsync(key));
Match — consume the result
// The recommended consumption pattern — forces both branches to be handled
var dto = result.Match(
onSuccess: user => UserDto.From(user),
onFailure: error => throw new UnreachableException(error.ToString())
);
ASP.NET Core Integration
Controller
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(Guid id)
{
return await _userService.GetAsync(id).ToActionResultAsync();
// Success → 200 OK { ... }
// NotFound error → 404 ProblemDetails
// Validation error → 422 ProblemDetails with field metadata
}
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest request)
{
return await _userService.CreateAsync(request)
.ToCreatedResult(nameof(GetUser), new { id = "will-be-set" });
}
Minimal API
app.MapGet("/users/{id}", async (Guid id, IUserService svc) =>
await svc.GetAsync(id).ToIResultAsync());
app.MapPost("/users", async (CreateUserRequest req, IUserService svc) =>
await svc.CreateAsync(req).ToCreatedIResult($"/users/{req.Email}"));
Error → HTTP Status Mapping
ErrorType |
HTTP Status |
|---|---|
Failure |
400 Bad Request |
Validation |
422 Unprocessable Entity |
NotFound |
404 Not Found |
Conflict |
409 Conflict |
Unauthorized |
401 Unauthorized |
Forbidden |
403 Forbidden |
Unexpected |
500 Internal Server Error |
FluentValidation Integration
// Standalone validation
Result<CreateUserRequest> validated = await validator.ValidateToResultAsync(request, ct);
// Inline in pipeline
Result<User> result = await validator
.ValidateToResultAsync(request, ct)
.ThenAsync(req => _userService.CreateAsync(req));
// Validate an already-in-flight result
Result<User> result2 = await someResult
.ThenValidateAsync(validator, ct)
.ThenAsync(user => _repo.SaveAsync(user, ct));
Service Layer Pattern
public sealed class UserService : IUserService
{
public async Task<Result<UserDto>> GetAsync(Guid id, CancellationToken ct = default)
{
var user = await _repo.FindAsync(id, ct);
if (user is null)
return Error.NotFound("User.NotFound", $"User {id} does not exist.");
return mapper.ToDto(user); // implicit cast to Result<UserDto>
}
public async Task<Result<UserDto>> CreateAsync(CreateUserRequest request, CancellationToken ct = default)
{
return await _validator
.ValidateToResultAsync(request, ct)
.ThenAsync(async req =>
{
if (await _repo.EmailExistsAsync(req.Email, ct))
return Error.Conflict("User.Email.Duplicate", "Email already registered.");
var user = User.Create(req.Email, req.Name);
await _repo.AddAsync(user, ct);
return mapper.ToDto(user);
});
}
}
Performance Notes
- Zero allocation on the success path for void
Result(singleton instance). ConfigureAwait(false)on all async extension methods — safe for library consumption.Error.Metadatais lazily allocated — noDictionarycreated unless needed.- No reflection, no code generation — all pipelines are statically compiled.
Contributing
See CONTRIBUTING.md. All PRs must include unit tests and pass the CI pipeline.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on ResultFlow.Core:
| Package | Downloads |
|---|---|
|
ResultFlow.Validation
FluentValidation bridge for ResultFlow. Provides ValidateToResult(), ValidateToResultAsync() and ThenValidate() pipeline integration — converts FluentValidation results into Result<T> with per-field error metadata, without coupling domain logic to HTTP. Requires ResultFlow.Core and FluentValidation 12+. |
|
|
ResultFlow.Http
ASP.NET Core integration for ResultFlow. Provides ToActionResult() and ToIResult() extension methods that map Result<T> to RFC 7807 ProblemDetails HTTP responses for both controller-based and Minimal API endpoints. Requires ResultFlow.Core. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 127 | 5/7/2026 |