Baryo.Dev: JSON serialization support for Verdict. Includes System.Text.Json converters for Result types.
$ dotnet add package Verdict.Json"FluentResults' features with 189x better performance. Best of both worlds."
Problem: Exception-based error handling kills performance (20,000x slower). FluentResults is feature-rich but allocates 176-368KB per 1000 operations.
Solution: Verdict delivers zero-allocation error handling with 72-189x better performance than FluentResults, while providing the same enterprise features through opt-in packages.
ROI: In a 100k req/sec API, Verdict eliminates ~25GB/sec of GC pressure. That's real cost savings in cloud infrastructure.
Risk: Zero. Drop-in replacement. Start with core (zero allocation), add features as needed. No vendor lock-in (MPL-2.0).
dotnet add package Verdict
# Multi-error support, validation, combine operations
dotnet add package Verdict.Extensions
# Async/await fluent API with CancellationToken & timeout support
dotnet add package Verdict.Async
# Success/error metadata, global factories
dotnet add package Verdict.Rich
# Auto-logging integration
dotnet add package Verdict.Logging
# ASP.NET Core integration with ProblemDetails
dotnet add package Verdict.AspNetCore
# JSON serialization (System.Text.Json)
dotnet add package Verdict.Json
# Original fluent extensions
dotnet add package Verdict.Fluent
| Package | Purpose | Dependencies | Allocation |
|---|---|---|---|
| Verdict | Core Result types | Zero | 0 bytes |
| Verdict.Extensions | Multi-error, validation | System.Memory | ~200 bytes (pooled) |
| Verdict.Async | Async API, cancellation | Zero | Task only |
| Verdict.Rich | Success/error metadata | Zero | ~160-350 bytes |
| Verdict.Logging | Auto-logging | MS.Extensions.Logging | Logging overhead |
| Verdict.AspNetCore | Web integration | ASP.NET Core | HTTP overhead |
| Verdict.Json | JSON serialization | System.Text.Json | JSON overhead |
| Verdict.Fluent | Original fluent API | Zero | 0 bytes |
Design Philosophy: Start with zero-allocation core. Scale to enterprise features through opt-in packages. Never compromise on speed.
using Verdict;
// Success case
Result<int> Divide(int numerator, int denominator)
{
if (denominator == 0)
return Result<int>.Failure("DIVIDE_BY_ZERO", "Cannot divide by zero");
return Result<int>.Success(numerator / denominator);
}
// Using the result
var result = Divide(10, 2);
if (result.IsSuccess)
{
Console.WriteLine($"Result: {result.Value}");
}
else
{
Console.WriteLine($"Error: [{result.Error.Code}] {result.Error.Message}");
}
using Verdict;
Result<int> GetValue()
{
// Implicit conversion from T to Result<T>
return 42;
}
Result<string> GetError()
{
// Implicit conversion from Error to Result<T>
return new Error("NOT_FOUND", "Value not found");
}
using Verdict;
using Verdict.Fluent;
var result = Divide(10, 2)
.Map(x => x * 2) // Transform success value
.OnSuccess(x => Console.WriteLine($"Success: {x}"))
.OnFailure(e => Console.WriteLine($"Error: {e.Message}"));
// Pattern matching
var message = result.Match(
onSuccess: value => $"Result is {value}",
onFailure: error => $"Error: {error.Message}"
);
using Verdict;
using Verdict.Async;
// CancellationToken support throughout async chains
var result = await GetUserAsync()
.MapAsync(async (user, ct) => await FetchOrdersAsync(user.Id, ct), cancellationToken)
.BindAsync(async (orders, ct) => await ProcessOrdersAsync(orders, ct), cancellationToken);
// Timeout support
var timedResult = await LongRunningOperationAsync()
.WithTimeout(TimeSpan.FromSeconds(30), "TIMEOUT", "Operation timed out");
using Verdict;
using Verdict.Json;
// Serialize Result to JSON
var result = Result<int>.Success(42);
var json = result.ToJson(); // {"isSuccess":true,"value":42}
// Deserialize JSON to Result
var restored = VerdictJsonExtensions.FromJson<int>(json);
// Configure for ASP.NET Core
services.AddControllers()
.AddJsonOptions(opts => opts.JsonSerializerOptions.AddVerdictConverters());
// ASP.NET Core ProblemDetails with environment-aware defaults
builder.Services.AddVerdictProblemDetails(builder.Environment);
using Verdict;
using Verdict.AspNetCore;
// Minimal API - returns RFC 7807 ProblemDetails on failure
app.MapGet("/users/{id}", async (int id) =>
{
var result = await userService.GetUserAsync(id);
return result.ToHttpResult();
});
// MVC Controller - with location URI for 201 Created
[HttpPost]
public ActionResult<User> Create(CreateUserRequest request)
{
var result = userService.CreateUser(request);
return result.ToActionResult(
successStatusCode: 201,
locationUri: $"/api/users/{result.ValueOrDefault?.Id}");
}
Error.FromException(ex, sanitize: true) to avoid leaking sensitive details.IncludeExceptionDetails/IncludeStackTrace off by default; enable only in development via AddVerdictProblemDetails(environment).application/problem+json content type.Error.CreateValidated / Error.ValidateErrorCode enforce alphanumeric + underscore codes (safe for logs/headers).dotnet run -c Release --project benchmarks/Verdict.Benchmarks -- --json
using Verdict;
// Sanitize exception messages for production (prevent info leakage)
var prodError = Error.FromException(ex, sanitize: true);
var customError = Error.FromException(ex, sanitize: true,
sanitizedMessage: "A database error occurred");
// Validate error codes (alphanumeric + underscore only)
var error = Error.CreateValidated("VALID_CODE", "Message");
bool isValid = Error.IsValidErrorCode("NOT_FOUND"); // true
bool isInvalid = Error.IsValidErrorCode("invalid-code"); // false
using Verdict;
using Verdict.Extensions;
// Include value information in error messages
var result = Result<int>.Success(15)
.Ensure(
age => age >= 18,
age => new Error("AGE_RESTRICTION", $"User is {age} years old, must be at least 18"));
// Error: "User is 15 years old, must be at least 18"
"We're replacing Exceptions for logic flow and FluentResults for object wrappers."
If you're building a generic business app, use FluentResults.
But if you're building a High-Performance System (like a Headless CMS, API Gateway, or microservice) where every millisecond and every byte of memory counts, you use Verdict.
Verdict replaces three categories of "Standard Practice" that are either Too Slow, Too Heavy, or Too Complex for modern, high-performance microservices.
What it is: The default C# way to handle errors (throw new UserNotFoundException()).
Why we replace it: Performance.
The Verdict Win: Verdict returns a struct. It's just a value return. It's ~50,000x faster than throwing an exception.
What it is: The most popular Result pattern library on NuGet (millions of downloads).
Why we replace it: Memory Allocation (GC Pressure).
Result.Ok(), it allocates memory on the heap.The Verdict Win: Verdict uses a readonly struct.
What it is: A massive library that tries to turn C# into Haskell. It has Either<L, R>, Option<T>, etc.
Why we replace it: Cognitive Load.
The Verdict Win: Verdict is C# idiomatic.
.IsSuccess and .Error.Comprehensive benchmarks comparing Verdict against Exceptions, FluentResults, and LanguageExt on Apple M1:
| Library | Mean | Allocated | vs Verdict |
|---|---|---|---|
| Verdict | 335 ns | 0 B | 1.00x (baseline) |
| Exceptions | 336 ns | 0 B | 1.00x |
| LanguageExt | 1,326 ns | 0 B | 3.96x slower |
| FluentResults | 63,303 ns | 176,000 B | 189x slower ⚠️ |
Key Finding: Verdict is 189x faster than FluentResults with zero allocations vs 176KB per 1000 operations.
| Library | Mean | Allocated | vs Verdict |
|---|---|---|---|
| Verdict | 626 ns | 0 B | 1.00x (baseline) |
| LanguageExt | 2,160 ns | 96 B | 3.45x slower |
| FluentResults | 91,343 ns | 368,000 B | 146x slower ⚠️ |
| Exceptions | 16,836,328 ns | 344,023 B | 26,890x slower ⚠️ |
Key Finding: Verdict is 146x faster than FluentResults and 26,890x faster than exceptions with zero allocations.
| Library | Mean | Allocated | vs Verdict |
|---|---|---|---|
| Verdict | 1,276 ns | 0 B | 1.00x (baseline) |
| LanguageExt | 1,975 ns | 0 B | 1.55x slower |
| FluentResults | 92,422 ns | 245,600 B | 72x slower ⚠️ |
| Exceptions | 1,626,148 ns | 22,401 B | 1,274x slower ⚠️ |
Key Finding: Verdict is 72x faster than FluentResults in realistic workloads with zero allocations vs 245KB.
✅ Verdict vs FluentResults:
✅ Verdict vs Exceptions:
| Feature | Verdict (Baryo.Dev) | FluentResults | Exceptions | LanguageExt |
|---|---|---|---|---|
| Philosophy | Digital Essentialism | Feature Rich | Native | Functional Purity |
| Memory | Stack (Struct) | Heap (Class) | Expensive | Heap/Mixed |
| GC Pressure | Zero (on success) | Low/Medium | High | Medium |
| Speed | Instant | Fast | Slow | Fast |
| Learning Curve | Low | Low | Low | High |
| Dependencies | 0 | 0 | 0 | Many |
| Success Allocation | 0 B | 176 KB | 0 B | 0 B |
| Failure Allocation | 0 B | 368 KB | 344 KB | 96 B |
Run the benchmarks yourself:
dotnet run -c Release --project benchmarks/Verdict.Benchmarks
Verdict follows a clean separation of concerns:
Verdict)Pure data structures with zero dependencies:
Result<T>: The core result typeError: Lightweight error representationVerdict.Fluent)Optional functional extensions:
Match<T, TOut>: Pattern matchingMap<T, K>: Functor mappingOnSuccess: Side-effect on successOnFailure: Side-effect on failureVerdict.Benchmarks)Performance validation using BenchmarkDotNet.
readonly struct?This project is licensed under the Mozilla Public License 2.0 (MPL-2.0).
Contributions are welcome! Please feel free to submit a Pull Request.
Created by: Baryo.Dev
Lead Developer: Arnel Isiderio Robles
Built with ❤️ for high-performance .NET applications.
The Verdict: FluentResults' features with 189x better performance. Best of both worlds.