BbQ.Outcome
1.2.0
dotnet add package BbQ.Outcome --version 1.2.0
NuGet\Install-Package BbQ.Outcome -Version 1.2.0
<PackageReference Include="BbQ.Outcome" Version="1.2.0" />
<PackageVersion Include="BbQ.Outcome" Version="1.2.0" />
<PackageReference Include="BbQ.Outcome" />
paket add BbQ.Outcome --version 1.2.0
#r "nuget: BbQ.Outcome, 1.2.0"
#:package BbQ.Outcome@1.2.0
#addin nuget:?package=BbQ.Outcome&version=1.2.0
#tool nuget:?package=BbQ.Outcome&version=1.2.0
BbQ.Outcome
A modern C# functional result type inspired by ErrorOr.
It builds on the excellent foundation of ErrorOr by adding async workflows, LINQ query syntax, deconstruction, Source Link, and multi-targeting — making error-aware programming feel like a first-class citizen in modern .NET.
❓ Why BbQ.Outcome?
ErrorOr pioneered the idea of replacing exceptions with a discriminated union of either a value or errors.
BbQ.Outcome takes this idea further:
- Structured errors: Rich
Errorrecord withCode,Description, andSeverity. - Async composition:
BindAsync,MapAsync,CombineAsyncfor natural async pipelines. - Explicit retries: Retry selected Outcome failures with cancellation-aware delays and no resilience-framework dependency.
- LINQ integration: Native
Select/SelectManysupport for sync + async queries. - IAsyncEnumerable streaming:
Select,Bind,Map,Where,Values,ErrorsoverIAsyncEnumerable<Outcome<T>>streams. - Deconstruction: Tuple-style unpacking
(isSuccess, value, errors)for ergonomic handling. - Friendly ToString: Human-readable logging like
Success: 42orErrors: [DIV_ZERO: Division by zero]. - Multi-targeting: Works across
net8.0,net9.0, andnet10.0. - Source Link enabled: Step directly into source when debugging NuGet packages.
- Source generator support: Auto-generate
Error<T>helper properties from enums with the[QbqOutcome]attribute.
💡 Example
var query =
from x in ParseAsync("10")
from y in DivideAsync(x, 2)
select y * 2;
var (ok, value, errors) = await query;
Console.WriteLine(ok
? $"Result: {value}"
: $"Errors: {string.Join("; ", errors.Select(e => e.Description))}");
Output:
Result: 10
💾 Installation
dotnet add package BbQ.Outcome
🧩 Source Generator: Error Helper Properties
The [QbqOutcome] attribute enables automatic generation of Error<TCode> helper properties for enums. This eliminates boilerplate and keeps error definitions DRY.
Usage
Mark your error enum with [QbqOutcome]:
[QbqOutcome]
public enum ApiErrorCode
{
/// <summary>
/// The requested resource was not found.
/// </summary>
NotFound,
/// <summary>
/// The user does not have permission to access this resource.
/// </summary>
Unauthorized,
/// <summary>
/// An internal server error occurred.
/// </summary>
InternalError
}
The source generator automatically creates a static class ApiErrorCodeErrors with helper properties:
// Generated code (do not edit)
public static class ApiErrorCodeErrors
{
public static Error<ApiErrorCode> NotFoundError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.NotFound,
Description: "The requested resource was not found.",
Severity: ErrorSeverity.Error
);
public static Error<ApiErrorCode> UnauthorizedError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.Unauthorized,
Description: "The user does not have permission to access this resource.",
Severity: ErrorSeverity.Error
);
public static Error<ApiErrorCode> InternalErrorError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.InternalError,
Description: "An internal server error occurred.",
Severity: ErrorSeverity.Error
);
}
Custom Descriptions
You can specify error descriptions in two ways:
1. Using XML Documentation (Recommended)
[QbqOutcome]
public enum ApiErrorCode
{
/// <summary>
/// The requested resource was not found.
/// </summary>
NotFound
}
2. Using [Description] Attribute
[QbqOutcome]
public enum ApiErrorCode
{
[System.ComponentModel.Description("Resource not found")]
NotFound
}
The generator prioritizes [Description] attributes over XML comments. If neither is provided, it uses the enum member name as a fallback.
Custom Severity Levels
By default, all generated errors use ErrorSeverity.Error. You can customize the severity for individual enum members using the [ErrorSeverity(...)] attribute:
[QbqOutcome]
public enum ApiErrorCode
{
/// <summary>
/// Validation failed.
/// </summary>
[ErrorSeverity(ErrorSeverity.Validation)]
ValidationFailed,
/// <summary>
/// The requested resource was not found.
/// </summary>
NotFound, // Uses default ErrorSeverity.Error
/// <summary>
/// An internal server error occurred.
/// </summary>
[ErrorSeverity(ErrorSeverity.Critical)]
InternalError
}
Generated code respects the specified severity levels:
// Generated code (do not edit)
public static class ApiErrorCodeErrors
{
public static Error<ApiErrorCode> ValidationFailedError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.ValidationFailed,
Description: "Validation failed.",
Severity: ErrorSeverity.Validation // Custom severity
);
public static Error<ApiErrorCode> NotFoundError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.NotFound,
Description: "The requested resource was not found.",
Severity: ErrorSeverity.Error // Default severity
);
public static Error<ApiErrorCode> InternalErrorError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.InternalError,
Description: "An internal server error occurred.",
Severity: ErrorSeverity.Critical // Custom severity
);
}
Available Severity Levels
The ErrorSeverity enum provides the following levels:
Info: Informational message; does not indicate a failure.Validation: Validation failure; the operation did not meet required conditions.Warning: Warning; the operation may have succeeded but with unexpected side effects.Error: Standard error; the operation failed and the error should be handled. (default)Critical: Critical error; the system may be in an inconsistent state.
Benefits
- Zero boilerplate: No manual
Error<T>construction. - Documentation-driven: Descriptions are extracted from XML doc comments (
<summary>tags) or[Description]attributes. - Flexibility: Choose between explicit
[Description]attributes or self-documenting XML comments. - Severity control: Customize error severity per enum member with
[ErrorSeverity(...)].
⚡ Performance
Both Outcome<T> (untyped errors) and Outcome<T, TError> (typed errors) are zero-allocation readonly struct types. Hot-path operations use internal unchecked accessors and [AggressiveInlining] to ensure the JIT can fully inline composition chains.
Outcome<T> wraps Outcome<T, object?> internally. Despite this extra layer of indirection, Map, Bind, Match, and Pipeline operations perform at near-identical speed to the direct Outcome<T, TError> type across .NET 8, 9, and 10:
| Operation | .NET 8 (ns) | .NET 9 (ns) | .NET 10 (ns) | Allocations |
|---|---|---|---|---|
| Create (success) | ~1.7 | ~2.0 | ~1.6 | 0 B |
| Map (success) | ~1.9 | ~1.7 | ~2.0 | 0 B |
| Bind (success) | ~2.0–7.2 | ~2.2 | ~2.3 | 0 B |
| Match (success) | <1 | <1 | <1 | 0 B |
| Pipeline (Map+Bind chain) | ~16 | ~15 | ~13 | 32 B |
Full benchmark results: tests/BbQ.Outcome.Benchmarks/README.md
- Consistent naming: Property names follow the pattern
{EnumMember}Error. - Type-safe: Full compile-time type checking with
Error<YourEnumType>.
How It Works
- The generator scans for enums decorated with
[QbqOutcome]. - For each enum member, it extracts:
- Description (in priority order):
[Description("...")]attribute if present<summary>from XML documentation comments- Enum member name as fallback
- Severity from
[ErrorSeverity(...)]attribute (defaults toErrorSeverity.Error)
- Description (in priority order):
- It generates a static helper class with pre-constructed
Error<T>properties using named parameters. - All special characters in descriptions are properly escaped for C# string literals.
🔁 Common Patterns
Async Pipelines with Bind
public async Task<Outcome<User>> GetAndValidateUserAsync(Guid userId)
{
return await GetUserAsync(userId)
.BindAsync(user => ValidateUserAsync(user))
.BindAsync(user => EnrichUserAsync(user));
}
Explicit retries
Retries operate on an operation factory so each attempt actually re-executes the underlying work:
var result = await Outcome.RetryAsync(
ct => repository.LoadAsync(userId, ct),
errors => errors.All(error =>
error.Code is StorageErrorCode.TemporarilyUnavailable
or StorageErrorCode.RateLimited),
new RetryOptions
{
MaxAttempts = 4,
DelayGenerator = retry =>
TimeSpan.FromMilliseconds(100 * Math.Pow(2, retry - 1))
},
cancellationToken);
MaxAttempts includes the initial operation call. The retry predicate receives the complete error list, so the application decides whether mixed failures are safe to retry. On exhaustion the final failure is returned; previous failures are not accumulated.
Exceptions are deliberately outside RetryAsync. Compose with Outcome.TryAsync when an exception-based dependency needs to participate in the same retry policy:
var result = await Outcome.RetryAsync(
ct => Outcome.TryAsync<User, StorageError>(
innerCt => storageClient.LoadAsync(userId, innerCt),
MapStorageException,
ct),
errors => errors.All(IsTransient),
new RetryOptions { MaxAttempts = 3 },
cancellationToken);
Unrecognized exceptions and cancellation still propagate. There is no retry extension on an already-started Task<Outcome<T>>, because awaiting the same task cannot re-run its producer.
Pattern Matching with Match
var outcome = await CreateUserAsync(email, name);
string message = outcome.Match(
onSuccess: user => $"Created user {user.Name}",
onError: errors => $"Failed: {string.Join(", ", errors.Select(e => e.Description))}"
);
LINQ Queries
var results = from user in GetUsersAsync()
from validated in ValidateAsync(user)
select validated;
var (ok, users, errors) = await results;
Deconstruction
var (success, value, errors) = outcome;
if (success)
{
Console.WriteLine($"Success: {value}");
}
else
{
foreach (var error in errors)
{
Console.WriteLine($"[{error.Severity}] {error.Code}: {error.Description}");
}
}
IAsyncEnumerable Streaming
Process async streams of outcomes with railway-oriented composition — useful when consuming IAsyncEnumerable<Outcome<T>> from CQRS streaming queries or event subscriptions:
IAsyncEnumerable<Outcome<Order>> orderStream = mediator.Stream(query, ct);
// Transform, filter, and extract values from the stream
await foreach (var name in orderStream
.Where(o => o.Total > 100)
.Map(o => o.CustomerName)
.Values())
{
Console.WriteLine(name);
}
Available Stream Operations
| Method | Signature | Description |
|---|---|---|
Select |
IAsyncEnumerable<Outcome<TResult>> |
LINQ-style map over success values; errors propagate |
Map |
IAsyncEnumerable<Outcome<TResult>> |
Functor map (alias for Select, for API consistency) |
Bind |
IAsyncEnumerable<Outcome<TResult>> |
Monadic flatMap producing new outcomes |
SelectMany |
IAsyncEnumerable<Outcome<TResult>> |
LINQ flatMap with projection |
Where |
IAsyncEnumerable<Outcome<T>> |
Predicate filter (failures become validation errors) |
Values |
IAsyncEnumerable<T> |
Extract only success values, discard errors |
Errors |
IAsyncEnumerable<IReadOnlyList<object?>> |
Extract only error lists, discard successes |
All methods support CancellationToken and use ConfigureAwait(false).
Bind over a Stream
// Validate each item in the stream; invalid items become error outcomes
var validated = orderStream.Bind(order =>
order.Total > 0
? Outcome<Order>.From(order)
: Outcome<Order>.Validation("INVALID_TOTAL", "Order total must be positive"));
await foreach (var outcome in validated)
{
outcome.Switch(
onSuccess: order => Console.WriteLine($"Valid order: {order.Id}"),
onError: errors => Console.WriteLine($"Invalid: {errors[0]}")
);
}
Collecting Errors from a Stream
// Log only errors from the stream
await foreach (var errorList in orderStream.Errors())
{
foreach (var error in errorList)
{
logger.LogWarning("Stream error: {Error}", error);
}
}
🔗 Integration with CQRS
When using BbQ.Cqrs, combine Outcome with commands and queries for comprehensive error handling:
// Error codes
[QbqOutcome]
public enum UserErrors
{
[Description("Email already in use")]
[ErrorSeverity(ErrorSeverity.Validation)]
EmailAlreadyExists
}
// Command returns Outcome<T>
public class CreateUserCommand : ICommand<Outcome<User>>
{
public string Email { get; set; }
}
// Handler uses source-generated errors
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Outcome<User>>
{
public async Task<Outcome<User>> Handle(CreateUserCommand request, CancellationToken ct)
{
if (await UserExists(request.Email))
{
return UserErrorsErrors.EmailAlreadyExistsError.ToOutcome<User>();
}
return Outcome<User>.From(await CreateUser(request.Email));
}
}
📚 Learn More
- Outcome Error Helper Properties - Strongly typed error patterns with source generators
- BbQ.Cqrs Documentation - Using Outcome with CQRS
| Product | Versions 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 is compatible. 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 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. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (5)
Showing the top 5 NuGet packages that depend on BbQ.Outcome:
| Package | Downloads |
|---|---|
|
BbQ.ChatWidgets
Framework-agnostic widgets for AI chat UIs, powered by Microsoft.Extensions.AI. |
|
|
BbQ.Outcome.AspNetCore
Explicit HTTP and Problem Details mapping for BbQ.Outcome. |
|
|
BbQ.Outcome.Diagnostics
Opt-in structured logging and Activity instrumentation for BbQ.Outcome. |
|
|
BbQ.Outcome.SystemTextJson
Explicit, Native AOT-compatible System.Text.Json contracts for BbQ.Outcome. |
|
|
BbQ.Cqrs.Outcome
Optional type-safe validation behaviors and response factories for BbQ.Cqrs and BbQ.Outcome. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.2.0 | 52 | 10/16/2026 |
| 1.2.0-preview.4 | 75 | 10/4/2026 |
| 1.2.0-preview.3 | 92 | 9/19/2026 |
| 1.2.0-preview.2 | 84 | 9/13/2026 |
| 1.2.0-preview.1 | 89 | 9/13/2026 |
| 1.1.2 | 482 | 5/21/2026 |
| 1.1.1 | 2,015 | 4/17/2026 |
| 1.1.0 | 154 | 4/16/2026 |
| 1.0.17 | 159 | 4/3/2026 |
| 1.0.16 | 514 | 3/12/2026 |
| 1.0.15 | 137 | 3/11/2026 |
| 1.0.14 | 132 | 3/6/2026 |
| 1.0.13 | 129 | 3/4/2026 |
| 1.0.12 | 146 | 1/27/2026 |
| 1.0.11 | 140 | 1/26/2026 |
| 1.0.10 | 1,074 | 11/28/2025 |
| 1.0.9 | 191 | 11/28/2025 |
| 1.0.8 | 231 | 11/27/2025 |
| 1.0.7 | 237 | 11/27/2025 |
| 1.0.6 | 235 | 11/26/2025 |
General availability: typed and heterogeneous results, async composition, dependency-free retries, generated error helpers and optional HTTP, JSON and diagnostics integrations. See https://github.com/JeanMarcMbouma/Outcome/blob/master/docs/release-outcome-1.2.0.md