ResultFlow.Core 0.1.0

dotnet add package ResultFlow.Core --version 0.1.0
                    
NuGet\Install-Package ResultFlow.Core -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ResultFlow.Core" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ResultFlow.Core" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="ResultFlow.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ResultFlow.Core --version 0.1.0
                    
#r "nuget: ResultFlow.Core, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ResultFlow.Core@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ResultFlow.Core&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=ResultFlow.Core&version=0.1.0
                    
Install as a Cake Tool

ResultFlow

NuGet License: MIT .NET 8

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

Package Description NuGet
ResultFlow.Core Core types: Result<T>, Error, pipeline extensions NuGet
ResultFlow.AspNetCore ToActionResult(), ToIResult() for controllers & Minimal APIs NuGet
ResultFlow.FluentValidation Bridge between FluentValidation and Result<T> NuGet

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.Metadata is lazily allocated — no Dictionary created 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

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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