A modern .NET 10 library implementing the Result Pattern with Railway-Oriented Programming and Flurl HTTP integration.
- Overview
- Installation
- Quick Start
- Core API
- Railway-Oriented Programming
- Pattern Matching
- LINQ Query Syntax
- Error Model
- Multi-Error Support
- JSON Serialization
- HTTP Extensions (Flurl)
- Async Support
- Null Safety
- Benchmarking
Responses provides:
- Immutable
readonly structtypes for zero-allocation hot paths - Railway-Oriented Programming with Map, Bind, Tap, and Ensure
- Pattern Matching with Match and Else
- LINQ Query Syntax via SelectMany
- Typed Errors with ErrorType enum and metadata support
- Multi-Error Collections for validation scenarios
- HTTP Integration via Flurl with ProblemDetails parsing
- JSON Serialization via System.Text.Json with DTO pattern
dotnet add package Responses --version 2.0.0
dotnet add package Responses.Http --version 2.0.0Requirements: .NET 10.0+
using Responses;
// Success
var result = Result.Ok();
var resultWithValue = Result.Ok(42);
// Failure
var fail = Result.Fail("ERR001", "Something went wrong");
var validationFail = Result.Fail<int>(Error.Validation("VAL", "Invalid input"));
// Check outcome
if (result.IsSuccess) { /* ... */ }
if (result.IsFailed) { /* ... */ }| Type | Description |
|---|---|
Result |
Void success/failure |
Result<T> |
Success with value |
Result<TValue, TError> |
Success with typed error |
All types are immutable readonly struct with [StructLayout(LayoutKind.Auto)] for optimal memory layout.
// Basic
Result.Ok() // → Result
Result.Ok(42) // → Result<int>
Result.Fail("ERR001", "message") // → Result
Result.Fail<int>("ERR001", "message") // → Result<int>
// Conditional
Result.OkIf(age >= 18, age, "ERR", "Must be 18+")
Result.FailIf(string.IsNullOrEmpty(email), email, "ERR", "Required")
// With Error object
var error = Error.Validation("VAL", "Invalid email");
Result.Fail(error)
Result.Fail<int>(error)var result = Result.Ok(42);
// Value — throws InvalidOperationException when failed
int value = result.Value;
// ValueOrDefault — safe access, returns default(T) when failed
int safeValue = result.ValueOrDefault;
// Error — throws InvalidOperationException when success
Error error = result.Error;
// Errors — collection (safe, never throws)
IError[] allErrors = result.Errors;Transforms the value on success, propagates error on failure:
var result = Result.Ok("hello")
.Map(s => s.ToUpper())
.Map(s => s.Length);
// → Result<int> with value 5
var failed = Result.Fail<int>("ERR", "msg")
.Map(x => x * 2);
// → Still failed, func was NOT calledChains fallible operations — stops at first failure:
Result<int> ParseAndValidate(string input) =>
int.TryParse(input, out var n) && n > 0
? Result.Ok(n)
: Result.Fail<int>("PARSE", "Invalid number");
var result = Result.Ok("42")
.Bind(ParseAndValidate)
.Bind(x => x > 10 ? Result.Ok(x) : Result.Fail<int>("RANGE", "Too small"));Executes a side-effect without modifying the Result:
var result = Result.Ok(42)
.Tap(x => Console.WriteLine($"Value: {x}"))
.Tap(x => _logger.LogInfo($"Processed: {x}"));
// → Result<int> unchangedValidates a condition, returns failure if false:
var result = Result.Ok("user@example.com")
.Ensure(e => e.Contains("@"), Error.Validation("FMT", "Invalid email"))
.Ensure(e => e.Length >= 5, Error.Validation("LEN", "Too short"));var message = result.Match(
v => $"Success: {v} items processed",
e => $"Error {e.Code}: {e.Message}"
);
// Void Match
result.Match(
v => Console.WriteLine($"Got: {v}"),
e => Console.WriteLine($"Failed: {e.Code}")
);int value = result.Else(0); // Fallback value
int computed = result.Else(e => e.Code == "NOT_FOUND" ? -1 : 0); // Fallback functionvar sum = from x in Result.Ok(5)
from y in Result.Ok(10)
from z in Result.Ok(3)
select x + y + z;
// → Result<int> with value 18
// Short-circuits on first failure
var fail = from x in Result.Fail<int>("ERR", "msg")
from y in Result.Ok(10) // NOT executed
select x + y;
// → Failed Resultpublic enum ErrorType
{
Unknown = 0,
Validation = 1,
NotFound = 2,
Conflict = 3,
Unauthorized = 4,
Forbidden = 5,
ServerError = 6,
Timeout = 7,
Cancelled = 8,
InternalError = 9,
}var metadata = new Dictionary<string, string>
{
{ "field", "email" },
{ "value", "invalid-input" }
};
var error = new Error("VAL001", "Invalid email", ErrorType.Validation, metadata);
string field = error.Metadata["field"]; // "email"Error.Validation("VAL", "Invalid input")
Error.NotFound("NF", "Resource not found")
Error.Conflict("CON", "Duplicate resource")
Error.Unauthorized("UA", "Authentication required")
Error.Forbidden("FB", "Access denied")
Error.Server("SVR", "Internal server error")
Error.Timeout("TO", "Request timed out")
Error.Cancelled("CAN", "Operation cancelled")var errors = new IError[]
{
Error.Validation("NAME", "Name is required"),
Error.Validation("EMAIL", "Invalid email format"),
Error.Validation("AGE", "Must be 18 or older")
};
var result = Result.Fail<int>(errors);
// Access all errors
foreach (var error in result.Errors)
Console.WriteLine($"[{error.Type}] {error.Code}: {error.Message}");
// Or via LINQ
var validationErrors = result.Errors
.Where(e => e.Type == ErrorType.Validation)
.ToList();Responses uses the DTO pattern for reliable System.Text.Json serialization:
using Responses.Serialization;
// Serialize
var result = Result.Ok(42);
var dto = ResultDto<int>.FromResult(result);
string json = JsonSerializer.Serialize(dto);
// Deserialize
var dtoBack = JsonSerializer.Deserialize<ResultDto<int>>(json);
var resultBack = dtoBack.ToResult();JSON format:
{
"isSuccessful": true,
"value": 42,
"errors": []
}{
"isSuccessful": false,
"value": null,
"errors": [
{
"code": "VAL001",
"message": "Invalid email",
"type": "Validation",
"layer": "Responses",
"applicationName": "MyApp",
"metadata": { "field": "email" }
}
]
}using Responses.Http;
using Flurl.Http;
// GET with typed result
var result = await "https://api.example.com/users/1"
.GetAsync()
.ReceiveResult<User>();
if (result.IsSuccess)
Console.WriteLine(result.Value.Name);
// POST
var created = await "https://api.example.com/users"
.PostJsonAsync(newUser)
.ReceiveResult<User>();| Status Code | ErrorType |
|---|---|
| 400 | Validation |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | NotFound |
| 409 | Conflict |
| 5xx | ServerError |
When the server returns application/problem+json:
{
"type": "https://example.com/errors/not-found",
"title": "User Not Found",
"status": 404,
"detail": "The requested user does not exist",
"instance": "/api/users/999"
}Responses automatically parses it:
var result = await "https://api.example.com/users/999"
.GetAsync()
.ReceiveResult<User>();
// Error.Message = "The requested user does not exist"
// Error.Type = ErrorType.NotFound// Serialization error — doesn't throw
var result = await "https://api.example.com/broken"
.GetAsync()
.ReceiveResult<User>();
// Returns Result with error containing raw body
if (result.IsFailed)
Console.WriteLine(result.Errors[0].Message); // Raw response bodyAll composition methods have async variants:
var result = await Result.Ok("user@example.com")
.MapAsync(async email => await ValidateEmailAsync(email))
.BindAsync(async id => await FetchUserAsync(id))
.TapAsync(async user => await LogAsync(user));All methods throw ArgumentNullException for null arguments:
result.Map(null!); // ArgumentNullException
result.Bind(null!); // ArgumentNullException
result.Tap(null!); // ArgumentNullException
result.Ensure(null!, e); // ArgumentNullException
result.Match(null!, f); // ArgumentNullExceptionRun the BenchmarkDotNet suite to verify zero-allocation claims:
dotnet run -c Release --project benchmarks/Responses.BenchmarksBenchmarks cover:
Result.Ok()/Result.Ok(42)— allocation verificationMap/Bind— success and failure pathsValueOrDefault— success and failure paths- Error creation — with and without metadata
| Version | Changes |
|---|---|
| 2.0.0 | .NET 10, readonly struct, railway-oriented programming, STJ, Flurl 4.x, multi-error, ProblemDetails |
| 1.2.0 | Legacy Newtonsoft.Json-based Result pattern with Flurl 3.x extensions |
MIT