Resulto.AspNetCore 1.0.0

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

Resulto

A lightweight, dependency-free Result pattern for .NET. Model success and failure explicitly instead of throwing exceptions for expected error cases — then chain, transform, and map your results with a small, predictable API.

CI NuGet License: MIT

Result<User> result = await GetUserAsync(id)
    .Ensure(u => u.IsActive, ResultError.Forbidden("Account is disabled"))
    .Map(u => u.ToDto());

return result.Match(
    onSuccess: dto => Ok(dto),
    onFailure: err => Problem(err.Message, statusCode: err.Code));

Why

Exceptions are for the exceptional — not for "user not found" or "email already taken". Those are ordinary outcomes your callers should handle. Resulto makes the failure path part of the type signature, so the compiler reminds you it exists.

  • Zero dependencies in the core package — works anywhere (netstandard2.0 + net8.0).
  • Extensible error categories — add your own error types (e.g. HTTP 429) without forking.
  • Structured validation errors — per-field details that bind cleanly to API clients.
  • Optional ASP.NET Core integration — map results to RFC 7807 ProblemDetails in one call.
  • Localized default messages — English, Russian, and Tajik ship in the box.

Packages

Package Target Purpose
Resulto netstandard2.0, net8.0 Core Result/BaseResult pattern. No dependencies.
Resulto.AspNetCore net8.0 Converts results to HTTP responses (ProblemDetails).
dotnet add package Resulto
dotnet add package Resulto.AspNetCore   # only if you build web APIs

Core concepts

Result<T> — an operation that returns a value

Result<int> Parse(string s) =>
    int.TryParse(s, out int n)
        ? n                                  // implicit success
        : ResultError.BadRequest("Not a number");   // implicit failure

Result<int> r = Parse("42");
if (r.IsSuccess) Console.WriteLine(r.Value);   // 42

Value throws if the result is a failure, and Error throws if it is a success — so you always go through IsSuccess/IsFailure or Match.

BaseResult — an operation that returns nothing

BaseResult Delete(Guid id)
{
    if (!Exists(id)) return ResultError.NotFound();
    Remove(id);
    return BaseResult.Success();   // cached singleton, zero allocation
}

Chaining

Method Runs when Does
Map success transforms the value
Bind success chains another result-producing call
Ensure success fails if a predicate is not met
Tap success side effect, returns the same result
TapError failure side effect on the error
Match both collapses to a single value, exhaustively

Every method has an async sibling (MapAsync, BindAsync, EnsureAsync, …).

Result<OrderDto> result = await GetOrderAsync(id)
    .EnsureAsync(o => o.OwnerId == userId, ResultError.Forbidden())
    .Bind(o => o.Confirm())          // returns Result<Order>
    .Map(o => o.ToDto());

Errors

Built-in factories cover the common HTTP-shaped cases:

ResultError.BadRequest();            // 400
ResultError.Unauthorized();          // 401
ResultError.Forbidden();             // 403
ResultError.NotFound();              // 404
ResultError.Conflict();              // 409
ResultError.UnsupportedMediaType();  // 415
ResultError.Validation();            // 422
ResultError.InternalServerError();   // 500

Each takes an optional message; when omitted, a localized default is used.

Equality note: two ResultErrors are equal when they share the same ErrorType, regardless of message. Equality answers "is this the same kind of error?" — handy for result.Error == ResultError.NotFound() style checks. Keep it in mind when comparing errors.

Adding your own error types (extensibility)

ErrorType is open for extension — it is a readonly record struct, not a closed enum. Declare your own categories and use them anywhere a built-in type is accepted:

public static class AppErrors
{
    public static readonly ErrorType RateLimited = new("RateLimited", 429);
    public static readonly ErrorType PaymentRequired = new("PaymentRequired", 402);
}

return ResultError.Custom(AppErrors.RateLimited, "Too many requests, slow down.");

The HTTP status code travels inside the ErrorType, so the ASP.NET mapping (below) handles your custom categories automatically — no extra wiring.

Structured validation errors

List<ValidationError> errors =
[
    new("Email", ValidationCodes.Required, "Email is required."),
    new("Age",   ValidationCodes.OutOfRange, "Age must be 18 or older."),
];

return ResultError.Validation(errors);   // 422 with per-field details

ValidationCodes provides machine-readable constants (required, invalid_format, out_of_range, …) so clients can branch on a stable code rather than a message.

ASP.NET Core integration

Add Resulto.AspNetCore and turn any result into an HTTP response:

using Resulto.AspNetCore;

app.MapGet("/users/{id}", async (Guid id) =>
    (await userService.GetAsync(id)).ToHttpResult());        // 200 or ProblemDetails

app.MapPost("/users", async (CreateUser cmd) =>
{
    Result<User> result = await userService.CreateAsync(cmd);
    return result.ToHttpCreated(uri: result.IsSuccess ? $"/users/{result.Value.Id}" : null);  // 201 or ProblemDetails
});

app.MapDelete("/users/{id}", async (Guid id) =>
    (await userService.DeleteAsync(id)).ToHttpResult());     // 204 or ProblemDetails

By default, failures become RFC 7807 ProblemDetails; validation failures become ValidationProblem with per-field errors. The status code is taken straight from the error, so custom error types map correctly with no extra code.

Customizing how errors are rendered

The error → HTTP mapping is an extension point — you are never locked into the built-in behavior. Customize it at three levels (see ResultHttp):

Per call — override for a single endpoint:

return result.ToHttpResult(onError: err => Results.StatusCode(err.Code));

Globally — set the default once at startup, composing with the built-in mapper for the rest:

// Program.cs
ResultHttp.DefaultErrorMapper = error => error.ErrorType == AppErrors.RateLimited
    ? Results.Json(new { error = "rate_limited" }, statusCode: 429)
    : ResultHttp.ProblemDetails(error);   // built-in behavior for everything else

Fully — write your own ResultErrorMapper from scratch:

ResultErrorMapper myMapper = error => Results.Json(
    new { code = error.ErrorType.Name, message = error.Message },
    statusCode: error.Code);

ResultHttp.DefaultErrorMapper = myMapper;     // or pass per call: result.ToHttpResult(myMapper)

Localization

Default error messages are resolved from CultureInfo.CurrentCulture. English (neutral), Russian (ru), and Tajik (tg) ship in the package. Set a custom message on any factory to override the default.

Performance

Returning a failure with Result is ~300–380× faster than throwing and catching an exception for the same logic, and allocates ~4× less — because throwing captures a stack trace while a Result is just a method return.

Method (Depth=1) Mean Ratio Allocated
Result (return failure) 15.2 ns 1.00 80 B
Exception (throw + catch) 5,779.3 ns ≈381× 344 B

See BENCHMARKS.md for the full numbers and how to run them.

Samples

A runnable minimal API showing ToHttpResult, validation errors, and a custom error type lives in samples/Resulto.Sample.Api:

dotnet run --project samples/Resulto.Sample.Api

Building from source

dotnet build
dotnet test
dotnet pack -c Release   # produces .nupkg + .snupkg

Contributing

Issues and pull requests are welcome. Please make sure dotnet build (warnings are errors) and dotnet test pass, and keep the core Resulto package dependency-free.

License

MIT © Qurbonali Nazarov

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.

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
1.0.0 85 5/26/2026