BbQ.Outcome 1.2.0

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

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 Error record with Code, Description, and Severity.
  • Async composition: BindAsync, MapAsync, CombineAsync for natural async pipelines.
  • Explicit retries: Retry selected Outcome failures with cancellation-aware delays and no resilience-framework dependency.
  • LINQ integration: Native Select/SelectMany support for sync + async queries.
  • IAsyncEnumerable streaming: Select, Bind, Map, Where, Values, Errors over IAsyncEnumerable<Outcome<T>> streams.
  • Deconstruction: Tuple-style unpacking (isSuccess, value, errors) for ergonomic handling.
  • Friendly ToString: Human-readable logging like Success: 42 or Errors: [DIV_ZERO: Division by zero].
  • Multi-targeting: Works across net8.0, net9.0, and net10.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:

[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

  1. The generator scans for enums decorated with [QbqOutcome].
  2. For each enum member, it extracts:
    • Description (in priority order):
      1. [Description("...")] attribute if present
      2. <summary> from XML documentation comments
      3. Enum member name as fallback
    • Severity from [ErrorSeverity(...)] attribute (defaults to ErrorSeverity.Error)
  3. It generates a static helper class with pre-constructed Error<T> properties using named parameters.
  4. 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

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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
Loading failed

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