Wiaoj.Results.AspNetCore 0.3.0-alpha.1

This is a prerelease version of Wiaoj.Results.AspNetCore.
dotnet add package Wiaoj.Results.AspNetCore --version 0.3.0-alpha.1
                    
NuGet\Install-Package Wiaoj.Results.AspNetCore -Version 0.3.0-alpha.1
                    
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="Wiaoj.Results.AspNetCore" Version="0.3.0-alpha.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Wiaoj.Results.AspNetCore" Version="0.3.0-alpha.1" />
                    
Directory.Packages.props
<PackageReference Include="Wiaoj.Results.AspNetCore" />
                    
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 Wiaoj.Results.AspNetCore --version 0.3.0-alpha.1
                    
#r "nuget: Wiaoj.Results.AspNetCore, 0.3.0-alpha.1"
                    
#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 Wiaoj.Results.AspNetCore@0.3.0-alpha.1
                    
#: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=Wiaoj.Results.AspNetCore&version=0.3.0-alpha.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Wiaoj.Results.AspNetCore&version=0.3.0-alpha.1&prerelease
                    
Install as a Cake Tool

Wiaoj.Results

NuGet License: MIT

Wiaoj.Results is a high-performance, zero-dependency library that implements the Result Pattern for .NET. It allows you to write robust, expressive, and type-safe code by replacing exception-based control flow with a functional approach known as Railway Oriented Programming (ROP).

Designed with Domain-Driven Design (DDD) and Clean Architecture in mind, it helps you manage application flow elegantly while minimizing performance overhead.

Features

  • Zero Dependencies. Built with pure C#, no external packages required.
  • High Performance. Uses lightweight readonly record struct types (Error, ErrorType) to minimize heap allocations and garbage collection pressure.
  • Rich Error Model. Structured, immutable errors featuring Code, Description, Type, and extensible Metadata.
  • Advanced Async Support. First-class extension methods for both Task and ValueTask to keep asynchronous pipelines allocation-conscious.
  • Elegant Control Flow. Eliminate nested if checks using fluent combinators like .Then(), .Map(), .Ensure(), .Do(), and .Match().
  • Collection Power. Combine, partition, or filter IEnumerable<Result<T>> and IAsyncEnumerable<Result<T>> collections in a single pass.
  • Safe Resource Management. Handle IDisposable and IAsyncDisposable payloads using .Consume() and .ConsumeAsync().
  • Exception Safety. Wrap throwing code and third-party libraries using Result.Try and Result.TryAsync, with automatic mapping of common exception types to ErrorType.

Installation

Install via the .NET CLI:

dotnet add package Wiaoj.Results

Or via the Package Manager Console:

Install-Package Wiaoj.Results

Core Concepts

1. Returning Results (Success or Failure)

Instead of throwing exceptions or returning null, return a Result<T>. The library supports implicit conversions from both raw values and Error objects, keeping your code clean.

using Wiaoj.Results;

public class UserService
{
    public Result<User> GetUser(int id)
    {
        if (id <= 0)
            return Error.Validation("User.InvalidId", "ID must be positive.");

        var user = _repository.Find(id);

        if (user is null)
            return Error.NotFound("User.NotFound", $"User with id {id} was not found.");

        // Implicitly converts the User object to a successful Result<User>
        return user;
    }
}

2. Void Operations (Result<Success>)

For operations that do not return a specific value, use Result<Success> (or simply return Result.Success()). Success is a zero-allocation, 1-byte struct optimized for this case.

public Result<Success> DeleteUser(int id)
{
    if (!_repo.Exists(id))
        return Error.NotFound();

    _repo.Delete(id);
    return Result.Success();
}

3. Handling Results (Pattern Matching)

Extract values or handle errors gracefully at the edges of your application (UI, API controllers) using .Match() or .Switch().

var result = service.GetUser(1);

// Match: returns a value based on the outcome
string response = result.Match(
    user => $"Welcome back, {user.Name}!",
    errors => $"Failed: {errors[0].Description}"
);

// Switch: executes an action based on the outcome (returns void)
result.Switch(
    user => Console.WriteLine($"Success: {user.Email}"),
    errors => Console.WriteLine($"Error Code: {errors[0].Code}")
);

Railway Oriented Programming (Chaining)

Instead of writing nested if (!result.IsSuccess) checks, chain your operations. If any step fails, the pipeline short-circuits and bypasses subsequent steps, propagating the error down the chain.

public async Task<Result<Guid>> RegisterUserAsync(UserDto dto)
{
    return await ValidateDtoAsync(dto)                                   // 1. Returns Result<UserDto>
        .EnsureAsync(IsEmailUniqueAsync, Error.Conflict("Email.InUse"))  // 2. Fails if email exists
        .ThenAsync(validDto => CreateUserInDbAsync(validDto))            // 3. Executes next step returning Result<User>
        .DoAsync(user => SendWelcomeEmailAsync(user))                    // 4. Side-effect: runs only on success
        .MapAsync(user => user.Id);                                      // 5. Transforms Result<User> to Result<Guid>
}

Rich Error Model

The library uses an extensible value-type approach (ErrorType) instead of a plain enum to categorize errors, making it straightforward to map them to HTTP status codes or log severity levels. You can declare your own domain-specific ErrorType values alongside the built-in ones — see Custom Errors & Metadata below.

Built-in Error Types

Factory Method Suggested HTTP Code Use Case
Error.Failure() 500 General failures or default unhandled states.
Error.Unexpected() 500 Unexpected system errors.
Error.Validation() 400 Invalid input formats or schema violations.
Error.NotFound() 404 The requested resource does not exist.
Error.Conflict() 409 Duplicate resource or business logic conflict.
Error.Unauthorized() 401 Authentication is required but missing or invalid.
Error.Forbidden() 403 Authenticated, but lacks required permissions.
Error.UnprocessableEntity() 422 Syntactically valid request, but a semantic rule was violated.
Error.RateLimitExceeded() 429 Caller has sent too many requests.
Error.ServiceUnavailable() 503 A downstream dependency is temporarily unreachable.
Error.Timeout() 408 / 504 The operation did not complete within the allowed time.
Error.Gone() 410 The resource has been permanently removed.

Custom Errors & Metadata

Define domain-specific error types and attach contextual metadata to your errors.

public static class AppErrorTypes
{
    public static readonly ErrorType Maintenance = new("Maintenance");
}

// Emitting a custom error with metadata
return Error.Custom(AppErrorTypes.Maintenance, "System.Offline", "System is under maintenance.")
            .WithMetadata("RetryAfter", DateTime.UtcNow.AddHours(1))
            .WithMetadata("TicketId", 12345);

Multiple Errors

Useful for returning aggregated validation errors instead of failing at the first bad input. A List<Error> implicitly converts to a failed Result<T> carrying every error in the list.

List<Error> errors = [
    Error.Validation("Name.Required", "Name cannot be empty."),
    Error.Validation("Age.Min", "Age must be at least 18.")
];

return errors; // Implicitly converts to a failed Result<Success> containing all errors

Advanced Features

Safely Wrapping Exceptions (Try / TryAsync)

Convert exceptions from third-party libraries (or the BCL) into Result objects. Error.FromException maps common exception types (TimeoutException, UnauthorizedAccessException, ArgumentException) to the matching ErrorType automatically; anything else becomes ErrorType.Unexpected.

// Automatically maps TimeoutException, UnauthorizedAccessException, etc. to the correct ErrorTypes
Result<string> content = await Result.TryAsync(ct => File.ReadAllTextAsync("data.txt", ct));

// Or provide a custom exception mapper:
Result<int> parsed = Result.Try(
    () => int.Parse("bad_input"),
    ex => Error.Validation("ParseError", ex.Message)
);

Nullability Bridges (ToResult / EnsureNotNull)

Convert null reference returns from existing APIs (like Entity Framework) into strict Result types.

var user = await dbContext.Users.FindAsync(id);

// If user is null, returns the provided NotFound error.
return user.ToResult(Error.NotFound("User.NotFound", "User not found."));

// Or chaining on an existing Result<T?>:
Result<User> strictResult = nullableResult.EnsureNotNull(Error.NotFound());

Collection Combinators (Combine, Partition)

Process a batch of results in a single pass.

IEnumerable<Result<User>> userResults = userIds.Select(id => GetUser(id));

// Returns Result<IReadOnlyList<User>>.
// If any GetUser call failed, `combined` becomes a failure holding all collected errors.
Result<IReadOnlyList<User>> combined = userResults.Combine();

// Or split successes and failures without short-circuiting:
var (users, errors) = userResults.Partition();

The same operations are available for Task<Result<T>> collections (CombineAsync, PartitionAsync, evaluated in parallel via Task.WhenAll) and for IAsyncEnumerable<Result<T>> streams.

Resource Management (Consume / DisposeValue)

When your Result<T> holds an IDisposable or IAsyncDisposable resource (e.g., a Stream or HttpResponseMessage), .Consume() / .ConsumeAsync() ensure it gets disposed right after execution.

await Result.TryAsync(ct => httpClient.GetAsync(url, ct))
    .ConsumeAsync(async (response, ct) =>
    {
        var data = await response.Content.ReadAsStringAsync(ct);
        Console.WriteLine(data);
        // The response is automatically disposed at the end of this block.
    });

ValueTask Optimization

For high-performance scenarios (like cache lookups that are usually synchronous), the library provides dedicated .ThenAsync, .MapAsync, .EnsureAsync, .DoAsync/.TapAsync, and .MatchAsync overloads for ValueTask<Result<T>> to avoid the heap allocation a Task-based pipeline would otherwise incur on the synchronous-completion path.

Web API / Minimal API Integration

Mapping a Result<T> to an HTTP response is straightforward using .Match(). Here is a common pattern for ASP.NET Core Minimal APIs or controllers:

[HttpGet("{id}")]
public async Task<IResult> GetUser(int id)
{
    var result = await _userService.GetUserAsync(id);

    return result.Match(
        user => Results.Ok(user),
        errors => Results.Problem(
            statusCode: GetStatusCode(errors[0].Type),
            title: errors[0].Description,
            extensions: errors[0].Metadata?.ToDictionary(k => k.Key, v => v.Value)
        )
    );
}

// Helper to map your ErrorTypes to standard HTTP status codes
private static int GetStatusCode(ErrorType type) => type.Name switch
{
    nameof(ErrorType.Validation)   => StatusCodes.Status400BadRequest,
    nameof(ErrorType.NotFound)     => StatusCodes.Status404NotFound,
    nameof(ErrorType.Conflict)     => StatusCodes.Status409Conflict,
    nameof(ErrorType.Unauthorized) => StatusCodes.Status401Unauthorized,
    nameof(ErrorType.Forbidden)    => StatusCodes.Status403Forbidden,
    nameof(ErrorType.RateLimit)    => StatusCodes.Status429TooManyRequests,
    nameof(ErrorType.Unavailable)  => StatusCodes.Status503ServiceUnavailable,
    _                               => StatusCodes.Status500InternalServerError
};

Contributing

Contributions, bug reports, and feature requests are welcome. Feel free to open an issue or submit a pull request on the GitHub repository.

License

This project is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0-alpha.1 44 10/5/2026
0.2.0-alpha.3 58 9/24/2026
0.2.0-alpha.2 54 9/24/2026
0.2.0-alpha.1 55 9/24/2026
0.1.0-alpha.9 60 9/21/2026
0.1.0-alpha.8 53 9/21/2026
0.1.0-alpha.7 57 9/18/2026
0.1.0-alpha.6 57 9/16/2026
0.1.0-alpha.5 59 9/16/2026
0.1.0-alpha.4 58 9/16/2026
0.1.0-alpha.3 54 9/15/2026
0.1.0-alpha.2 64 9/15/2026
0.1.0-alpha.1 108 9/14/2026
0.0.1-alpha.112-preview 66 9/13/2026
0.0.1-alpha.111-preview 55 9/13/2026
0.0.1-alpha.110-preview 56 9/12/2026
0.0.1-alpha.109-preview 83 9/11/2026
0.0.1-alpha.108-preview 75 9/8/2026
0.0.1-alpha.107-preview 82 9/8/2026
0.0.1-alpha.106-preview 68 9/8/2026
Loading failed