Voyager.Common.Results 1.10.0

There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Voyager.Common.Results --version 1.10.0
                    
NuGet\Install-Package Voyager.Common.Results -Version 1.10.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="Voyager.Common.Results" Version="1.10.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Voyager.Common.Results" Version="1.10.0" />
                    
Directory.Packages.props
<PackageReference Include="Voyager.Common.Results" />
                    
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 Voyager.Common.Results --version 1.10.0
                    
#r "nuget: Voyager.Common.Results, 1.10.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 Voyager.Common.Results@1.10.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=Voyager.Common.Results&version=1.10.0
                    
Install as a Cake Addin
#tool nuget:?package=Voyager.Common.Results&version=1.10.0
                    
Install as a Cake Tool

Voyager.Common.Results

NuGet NuGet Downloads Build Status License: MIT

A lightweight, functional Result Pattern implementation for .NET that enables Railway Oriented Programming. Replace exceptions with explicit error handling, making your code more predictable and easier to test.

Supports .NET Framework 4.8 and .NET 8 ๐Ÿš€

โœจ Features

  • ๐ŸŽฏ Type-safe error handling without exceptions
  • ๐Ÿš‚ Railway Oriented Programming with method chaining
  • โšก Async/await support with extension methods and instance proxies
  • ๐Ÿงฉ Contextual errors with Ensure/EnsureAsync error factories
  • ๐Ÿงต Deadlock-safe async - library uses ConfigureAwait(false) internally
  • ๐Ÿ“ฆ Zero dependencies (except polyfills for .NET Framework)
  • ๐Ÿ” Source Link enabled for debugging
  • ๐Ÿ“š Comprehensive XML documentation
  • ๐Ÿงช Fully tested with high code coverage
  • ๐ŸŽจ Implicit conversions for ergonomic API
  • ๐Ÿ”ฌ Built-in Roslyn analyzer warns when Result is not consumed
  • ๐Ÿค– Automated publishing via GitHub Actions

๐Ÿ“ฆ Installation

# Core Result pattern library
dotnet add package Voyager.Common.Results

# Advanced resilience patterns (Circuit Breaker)
dotnet add package Voyager.Common.Resilience

๐Ÿš€ Quick Start

using Voyager.Common.Results;

// Define operations that can fail (assumes repository doesn't throw exceptions)
public Result<User> GetUser(int id)
{
    var user = _repository.Find(id);
    return user is not null 
        ? user  // Implicit conversion: User โ†’ Result<User>
        : Error.NotFoundError($"User {id} not found");
}

public Result<Order> GetLatestOrder(User user)
{
    var order = _repository.GetLatestOrder(user.Id);
    return order is not null
        ? order 
        : Error.NotFoundError("No orders found");
}

// Chain operations with Railway Oriented Programming
var result = GetUser(123)
    .Bind(user => GetLatestOrder(user))
    .Map(order => order.TotalAmount)
    .Tap(total => _logger.LogInfo($"Total: {total}"));

// Handle the result
var message = result.Match(
    onSuccess: total => $"Order total: {total:C}",
    onFailure: error => $"Error: {error.Message}"
);

๐Ÿงช Testing

The library includes a comprehensive test suite ensuring correctness across multiple dimensions:

  • Monad Laws - Verifies mathematical properties of Result<T> (identity, composition)
  • Invariants - XOR property, null safety, immutability guarantees
  • Error Propagation - Correct error flow through all operators and chains
  • Composition - Operator chaining and combination behavior in complex scenarios
  • Unit Tests - Core functionality, extension methods, edge cases, and cancellation

All tests validate behavior on both .NET 8.0 and .NET Framework 4.8 to ensure cross-platform compatibility.

๐Ÿ“– Documentation

Core Types

  • Result<T> - Represents an operation that returns a value or an error
  • Result - Represents an operation that returns success or an error (void operations)
  • Error - Represents an error with type and message

Error Types

Error.ValidationError("Invalid email format")
Error.NotFoundError("User not found")
Error.UnauthorizedError("User not logged in")
Error.PermissionError("Access denied")
Error.ConflictError("Email already exists")
Error.DatabaseError("Connection failed")
Error.BusinessError("Cannot cancel paid order")
Error.UnavailableError("Service temporarily unavailable")
Error.TimeoutError("Request timed out")
Error.CancelledError("Operation was cancelled")
Error.TooManyRequestsError("Rate limit exceeded")  // HTTP 429
Error.CircuitBreakerOpenError(lastError) // Circuit breaker open
Error.UnexpectedError("Something went wrong")
Error.FromException(exception) // Auto-maps exception type to ErrorType

Error Classification (ADR-005)

Use extension methods to classify errors for resilience patterns:

using Voyager.Common.Results.Extensions;

error.Type.IsTransient()      // Timeout, Unavailable, TooManyRequests, CircuitBreakerOpen
error.Type.IsBusinessError()  // Validation, NotFound, Permission, etc.
error.Type.ShouldRetry()      // Same as IsTransient
error.Type.ToHttpStatusCode() // Maps to HTTP status code

Error Chaining (ADR-006)

Track error origin across distributed service calls:

// Chain errors (like InnerException)
var error = Error.UnavailableError("Order failed")
    .WithInner(productServiceError);

// Find root cause
var rootCause = error.GetRootCause();

// Check chain for specific error type
if (error.HasInChain(e => e.Type == ErrorType.NotFound)) { }

// Add context when calling external services
var result = await _productService.GetAsync(id)
    .AddErrorContextAsync("ProductService", "GetProduct");

Enhanced FromException (ADR-007)

FromException now preserves full diagnostic info and auto-maps exception types:

var error = Error.FromException(exception);
// Auto-maps: TimeoutException โ†’ Timeout, ArgumentException โ†’ Validation, etc.
// Preserves: StackTrace, ExceptionType, Source, InnerException chain

// Override auto-mapping
var error = Error.FromException(exception, ErrorType.Database);

// Detailed logging output
_logger.LogError(error.ToDetailedString());
// [Database] Exception.SqlException: Connection failed
//   Stack Trace: at Repository.Query() in Repository.cs:line 42
//   Caused by: [Unavailable] Exception.SocketException: Network unreachable

Railway Oriented Programming

GetUser(id)
    .Map(user => user.Email)              // Transform success value
    .Bind(email => SendEmail(email))       // Chain another Result operation
    .Ensure(sent => sent, Error.BusinessError("Email not sent"))
    .Tap(() => _logger.LogInfo("Email sent"))  // Side effect
    .OrElse(() => GetDefaultUser())        // Fallback if failed
    .Match(
        onSuccess: () => "Success",
        onFailure: error => error.Message
    );

Finally - Resource Cleanup

Executes an action regardless of success or failure (like finally block):

// Chain with other operations
var result = GetUser(id)
    .Map(user => user.Email)
    .Tap(email => _logger.LogInfo(email))
    .Finally(() => _metrics.RecordOperation());

When to use Finally:

  • โœ… Resource cleanup (close connections, dispose streams)
  • โœ… Logging/metrics regardless of outcome
  • โœ… Releasing locks or semaphores
  • โœ… Any cleanup that must happen in both success and failure paths

Try - Exception Handling

Safely convert exception-throwing code into Result pattern:

// Basic: wraps exceptions with Error.FromException
var result = Result<int>.Try(() => int.Parse(userInput));

// Custom error mapping
var result = Result<int>.Try(
    () => int.Parse(userInput),
    ex => ex is FormatException 
        ? Error.ValidationError("Invalid number format")
        : Error.FromException(ex));

// Void operations
var result = Result.Try(() => File.Delete(path));

// With custom error handling
var result = Result.Try(
    () => File.Delete(path),
    ex => ex is UnauthorizedAccessException
        ? Error.PermissionError("Access denied")
        : Error.FromException(ex));

// Chain with other operations
var userData = Result<string>.Try(() => File.ReadAllText(path))
    .Bind(json => ParseJson(json))
    .Map(data => data.UserId);

// Robust database operations (handles both exceptions and null)
public Result<User> GetUser(int id)
{
    return Result<User>.Try(() => _repository.Find(id))
        .Ensure(user => user is not null, Error.NotFoundError($"User {id} not found"));
}

When to use Try:

  • โœ… Wrapping third-party APIs that throw exceptions
  • โœ… File I/O, parsing, network calls
  • โœ… Converting legacy exception-based code to Result pattern
  • โœ… Custom exception-to-error mapping

TryAsync - Async Exception Handling

Safely convert async exception-throwing code into Result pattern. Automatically maps OperationCanceledException to ErrorType.Cancelled when using CancellationToken:

// Preferred: Use Result<T>.TryAsync proxy for cleaner syntax
var result = await Result<Config>.TryAsync(async () => 
    await JsonSerializer.DeserializeAsync<Config>(stream));

// With CancellationToken support (auto-maps OperationCanceledException โ†’ ErrorType.Cancelled)
var result = await Result<string>.TryAsync(
    async ct => await httpClient.GetStringAsync(url, ct),
    cancellationToken);

// Custom error mapping
var result = await Result<Config>.TryAsync(
    async () => await JsonSerializer.DeserializeAsync<Config>(stream),
    ex => ex is JsonException 
        ? Error.ValidationError("Invalid JSON")
        : Error.UnexpectedError(ex.Message));

// With CancellationToken and custom error mapping
var result = await Result<string>.TryAsync(
    async ct => await httpClient.GetStringAsync(url, ct),
    cancellationToken,
    ex => ex is HttpRequestException 
        ? Error.UnavailableError("Service unavailable")
        : Error.UnexpectedError(ex.Message));

// Chain with other async operations
var userData = await Result<string>.TryAsync(async () => 
        await File.ReadAllTextAsync(path))
    .BindAsync(json => ParseJsonAsync(json))
    .MapAsync(data => data.UserId);

When to use TryAsync:

  • โœ… Async file I/O, database operations
  • โœ… HTTP/API calls with cancellation support
  • โœ… Async parsing and serialization
  • โœ… Converting async exception-based code to Result pattern
  • โœ… Operations that need proper cancellation handling

Retry - Transient Failure Handling

Handle temporary failures (network issues, service unavailability) with automatic retry logic:

using Voyager.Common.Results.Extensions;

// Basic retry with default policy (3 attempts, exponential backoff)
var result = await GetDatabaseConnection()
    .BindWithRetryAsync(
        conn => ExecuteQuery(conn),
        RetryPolicies.TransientErrors()
    );

// Custom retry configuration
var result = await FetchDataAsync()
    .BindWithRetryAsync(
        data => ProcessData(data),
        RetryPolicies.TransientErrors(maxAttempts: 5, baseDelayMs: 500)
    );

// Custom retry policy for specific errors
var policy = RetryPolicies.Custom(
    maxAttempts: 10,
    shouldRetry: e => e.Type == ErrorType.Unavailable || e.Code == "RATE_LIMIT",
    delayStrategy: attempt => 500 // Fixed 500ms delay
);

var result = await apiCall.BindWithRetryAsync(ProcessResponse, policy);

// Retry automatically handles:
// โœ… ErrorType.Unavailable - Service down, network issues, deadlocks
// โœ… ErrorType.Timeout - Operation exceeded time limit
// โŒ Permanent errors (Validation, NotFound, etc.) - NOT retried

Key features:

  • ๐Ÿ”„ Exponential backoff by default (1s โ†’ 2s โ†’ 4s โ†’ ...)
  • ๐ŸŽฏ Only retries transient errors (Unavailable, Timeout)
  • ๐Ÿ“ Always preserves original error - never generic "max retries exceeded"
  • โšก Zero external dependencies
  • ๐Ÿ”ง Fully customizable via RetryPolicies.Custom()
  • ๐Ÿ”” Retry attempt callbacks for logging and metrics

Retry Attempt Callbacks:

var result = await operation.BindWithRetryAsync(
    async value => await _httpClient.GetAsync(value),
    RetryPolicies.TransientErrors(maxAttempts: 3),
    onRetryAttempt: (attempt, error, delayMs) =>
    {
        _logger.LogWarning(
            "Attempt {Attempt} failed: {Error}. Retrying in {Delay}ms",
            attempt, error.Message, delayMs);

        _metrics.IncrementRetryCounter(error.Type.ToString());
    });

When to use Retry:

  • โœ… Network calls with temporary failures
  • โœ… Database operations during brief unavailability
  • โœ… API calls that may be rate-limited or temporarily down
  • โŒ NOT for permanent errors (Validation, NotFound)
  • ๐Ÿ’ก For cascading failure prevention, use Circuit Breaker from Voyager.Common.Resilience

Circuit Breaker - Cascading Failure Prevention

(Requires Voyager.Common.Resilience package)

Prevent cascading failures by temporarily blocking calls to failing services:

using Voyager.Common.Resilience;

// Create a circuit breaker policy
var policy = new CircuitBreakerPolicy(
    failureThreshold: 5,      // Open after 5 consecutive failures
    openTimeout: TimeSpan.FromSeconds(30),  // Stay open for 30s
    halfOpenMaxAttempts: 3    // Allow 3 test attempts when half-open
);

// Execute operations through the circuit breaker
var result = await GetUser(userId)
    .BindWithCircuitBreakerAsync(
        user => CallExternalServiceAsync(user),
        policy
    );

// Circuit breaker states:
// ๐ŸŸข Closed - Normal operation, requests flow through
// ๐Ÿ”ด Open - Too many failures, requests immediately fail with CircuitBreakerOpenError
// ๐ŸŸก HalfOpen - Testing if service recovered, limited attempts allowed

// Check circuit state
if (policy.State == CircuitBreakerState.Open)
{
    _logger.LogWarning("Circuit breaker is open, service unavailable");
}

// Manual reset if needed
policy.Reset();

Key features:

  • ๐Ÿ›ก๏ธ Prevents cascading failures across distributed systems
  • โšก Fast-fail when service is down (no wasted retries)
  • ๐Ÿ”„ Automatic recovery testing via half-open state
  • ๐Ÿงต Thread-safe with SemaphoreSlim for async operations
  • ๐Ÿ“ Preserves last error context via CircuitBreakerOpenError(lastError)
  • ๐ŸŽฏ Returns ErrorType.CircuitBreakerOpen when circuit is open
  • ๐Ÿ”Ž Only counts infrastructure errors (Unavailable, Timeout, Database, Unexpected)
  • โœ… Ignores business errors (Validation, NotFound, Permission, Business, Conflict)
  • ๐Ÿ”” State change callbacks for logging, alerting, and metrics

State Change Callbacks:

var policy = new CircuitBreakerPolicy(failureThreshold: 5);

// Subscribe to state changes
policy.OnStateChanged = (oldState, newState, failureCount, lastError) =>
{
    _logger.LogWarning(
        "Circuit breaker: {OldState} โ†’ {NewState}, failures: {Count}",
        oldState, newState, failureCount);

    if (newState == CircuitState.Open)
    {
        _alertService.SendAlert($"Circuit OPEN: {lastError?.Message}");
        _metrics.IncrementCounter("circuit_breaker_opened");
    }
};

When to use Circuit Breaker:

  • โœ… External API/service calls that may fail
  • โœ… Database operations during outages
  • โœ… Microservice communication
  • โœ… Any operation where cascading failures must be prevented
  • ๐Ÿ’ก Combine with Retry for comprehensive resilience

When to use Retry:

  • โœ… Network calls with temporary failures
  • โœ… Database operations during brief unavailability
  • โœ… API calls that may be rate-limited or temporarily down
  • โŒ NOT for permanent errors (Validation, NotFound)
  • ๐Ÿ’ก For cascading failure prevention, use Circuit Breaker from Voyager.Common.Resilience

Map - Value Transformations

Transform success values or convert void operations to value operations:

// Transform Result<T> values
var emailResult = GetUser(id)
    .Map(user => user.Email);              // Result<User> โ†’ Result<string>

// Convert Result (void) to Result<T> (value)
var numberResult = ValidateInput()
    .Map(() => 42);                         // Result โ†’ Result<int>

// Chain transformations
var result = GetUser(id)
    .Map(user => user.Email)
    .Map(email => email.ToLower())
    .Map(email => email.Trim());

When to use Map:

  • โœ… Transform success value to another type
  • โœ… Convert void success to value success
  • โœ… Simple, non-failing transformations
  • โŒ Don't use for operations that return Result (use Bind instead)

MapError - Error Transformation

Transform errors without affecting success:

// Add context to errors
var result = Operation()
    .MapError(error => Error.DatabaseError("DB_" + error.Code, error.Message));

// Convert error types
var result = ValidateUser()
    .MapError(error => Error.BusinessError("USER_" + error.Code, error.Message));

// Chain transformations
var result = GetData()
    .MapError(e => Error.UnavailableError("Service unavailable: " + e.Message))
    .TapError(e => _logger.LogError(e.Message));

When to use MapError:

  • โœ… Add prefixes or context to error codes/messages
  • โœ… Convert error types for different layers (API โ†’ Domain โ†’ Infrastructure)
  • โœ… Enrich errors with additional information
  • โœ… Standardize error formats

Bind - Chaining Operations

The Bind method is available on both Result and Result<T> for seamless operation chaining:

// Chain void operations (Result โ†’ Result)
var result = ValidateInput()
    .Bind(() => AuthorizeUser())
    .Bind(() => SaveToDatabase())
    .Bind(() => SendNotification());

// Transform void operation to value operation (Result โ†’ Result<T>)
var userResult = ValidateRequest()
    .Bind(() => GetUser(userId))
    .Map(user => user.Email);

// Mix void and value operations
var orderResult = AuthenticateUser()      // Result
    .Bind(() => GetShoppingCart(userId))  // Result โ†’ Result<Cart>
    .Bind(cart => ProcessOrder(cart))     // Result<Cart> โ†’ Result<Order>
    .Map(order => order.Id);              // Result<Order> โ†’ Result<int>

When to use Bind:

  • โœ… Chain operations that return Result<T>
  • โœ… Transform Result (void) to Result<T> (value)
  • โœ… Maintain railway oriented flow
  • โŒ Don't use for simple value transformations (use Map instead)

OrElse - Fallback Pattern

// Try multiple data sources - returns first success
var user = GetUserFromCache(userId)
    .OrElse(() => GetUserFromDatabase(userId))
    .OrElse(() => GetDefaultUser());

// Async version
var config = await LoadConfigFromFileAsync()
    .OrElseAsync(() => LoadConfigFromDatabaseAsync())
    .OrElseAsync(() => GetDefaultConfigAsync());

// Real-world example: Multi-tier data retrieval
var data = await GetFromPrimaryCacheAsync(key)
    .OrElseAsync(() => GetFromDatabaseAsync(key))
    .OrElseAsync(() => GetFromApiAsync(key))
    .OrElseAsync(Result<Data>.Success(defaultValue));

Common use cases:

  • Cache โ†’ Database โ†’ Default value
  • Primary API โ†’ Fallback API โ†’ Cached data
  • User preferences โ†’ Team defaults โ†’ System defaults

Ensure - Contextual Validation

Validate with error messages that include the actual value:

// Static error (old way)
var result = GetUser(id)
    .Ensure(
        user => user.Age >= 18,
        Error.ValidationError("Must be 18 or older"));

// Contextual error (recommended - provides better error messages)
var result = GetUser(id)
    .Ensure(
        user => user.Age >= 18,
        user => Error.ValidationError($"User {user.Name} is {user.Age} years old, must be 18+"));

EnsureAsync - Async Contextual Validation

Validate with async predicates and contextual errors:

// With sync predicate
var result = await GetUserAsync(id)
    .EnsureAsync(
        user => user.Age >= 18,
        user => Error.ValidationError($"User {user.Name} is {user.Age}, must be 18+"));

// With async predicate
var result = await GetUserAsync(id)
    .EnsureAsync(
        async user => await _repo.IsActiveAsync(user.Id),
        user => Error.ValidationError($"User {user.Name} is inactive"));

Instance Method Proxies

No need to import Extensions namespace - common async methods are available directly on Result<T>:

var result = await GetUser(id)              // Result<User>
    .EnsureAsync(
        async u => await _repo.IsActiveAsync(u.Id),
        u => Error.ValidationError($"User {u.Name} inactive"))
    .TapAsync(async u => await _audit.LogAsync($"Access: {u.Id}"))
    .OrElseAsync(() => GetDefaultUserAsync());

Available instance proxies:

  • EnsureAsync(asyncPredicate, error) - async validation
  • EnsureAsync(asyncPredicate, errorFactory) - async validation with contextual error
  • TapAsync(asyncAction) - async side effects
  • OrElseAsync(asyncAlternativeFunc) - async fallback

Async Operations

await GetUserAsync(id)
    .MapAsync(user => user.Email)
    .BindAsync(email => SendEmailAsync(email))
    .TapAsync(() => _logger.LogInfoAsync("Email sent"));

Collection Operations

var results = new Result<int>[] {
    1,  // Implicit conversion: int โ†’ Result<int>
    2,
    3
};

// Combine all results into one
Result<List<int>> combined = results.Combine();

// Partition into successes and failures
var (successes, failures) = results.Partition();

// Get only successful values
List<int> values = results.GetSuccessValues();

// Combine two Results into a tuple
var name = Result<string>.Success("Alice");
var age = Result<int>.Success(30);
Result<(string, int)> pair = name.Combine(age);

Async Collection Operations (v1.9.0)

// TraverseAsync โ€” sequential async, fail-fast
var result = await operations.TraverseAsync(
    x => OperationUpdateResultAsync(ctx, x.op, x.data));

// TraverseAllAsync โ€” sequential async, collect ALL errors
var result = await items.TraverseAllAsync(
    x => ValidateAndProcessAsync(x));

// CombineAsync โ€” await all tasks, then combine
var tasks = items.Select(x => ProcessAsync(x));
var result = await tasks.CombineAsync();

// PartitionAsync โ€” await all tasks, then partition
var (successes, failures) = await tasks.PartitionAsync();

See Collection Operations for detailed documentation.

Analyzer - Result Must Be Consumed (VCR0010)

The package includes a built-in Roslyn analyzer that warns when a Result or Result<T> return value is silently discarded. This prevents a common mistake where errors are lost because nobody checked the result:

GetUser(id);                  // โš ๏ธ VCR0010: Result of 'GetUser' must be checked
await SendEmailAsync(email);  // โš ๏ธ VCR0010: Result of 'SendEmailAsync' must be checked
Result.Success();             // โš ๏ธ VCR0010: Result of 'Success' must be checked

// โœ… All of these are fine - result is consumed:
var result = GetUser(id);           // Assigned to variable
_ = GetUser(id);                    // Explicitly discarded
if (GetUser(id).IsSuccess) { }     // Used in condition
return GetUser(id);                 // Returned
Log(GetUser(id));                   // Passed as argument
GetUser(id).Match(...);             // Used in method chain

The analyzer is bundled in the NuGet package - no extra installation needed. Two quick-fixes are available:

  • Discard result (_ = ...) - when you intentionally want to ignore the result
  • Assign to variable (var result = ...) - when you want to handle it later

Additional Analyzers

The package includes a full suite of Roslyn analyzers that catch common Result pattern mistakes:

ID Severity Description
VCR0010 Warning Result must be consumed โ€” unconsumed Result / Result<T> return values
VCR0020 Warning Value accessed without success check โ€” result.Value without IsSuccess guard
VCR0030 Warning Nested Result<Result<T>> โ€” use Bind instead of Map
VCR0040 Info GetValueOrThrow defeats Result pattern โ€” prefer Match/Bind/Map
VCR0050 Error Failure(Error.None) โ€” failure without error is always a bug
VCR0060 Disabled Prefer Match/Switch over if (IsSuccess) branching (opt-in style rule)
VCR0070 Warning Success(null) โ€” successful result must carry a value, not null
VCR0071 Disabled Result<T?> โ€” nullable type parameter suggests missing Failure modeling (opt-in)
var result = GetUser(id);
result.Value.Name;                     // โš ๏ธ VCR0020: Access 'Value' without checking 'IsSuccess'

result.Map(x => GetOrder(x.Id));       // โš ๏ธ VCR0030: Nested Result<Result<Order>>, use Bind

GetUser(id).GetValueOrThrow();         // โ„น๏ธ VCR0040: Consider Match/Bind instead

Result.Failure(Error.None);            // โŒ VCR0050: Failure with Error.None is a bug

Result<Order?>.Success(null);          // โš ๏ธ VCR0070: Success(null) defeats Result pattern

All analyzers are configurable via .editorconfig:

dotnet_diagnostic.VCR0040.severity = none      # Disable
dotnet_diagnostic.VCR0060.severity = warning   # Enable opt-in rule

๐Ÿ“š More Examples

See the full documentation for detailed examples and best practices.

๐Ÿ—๏ธ Building and Publishing

See BUILD.md for comprehensive instructions on:

  • ๐Ÿค– Automatic publishing with GitHub Actions (recommended)
  • ๐Ÿ”จ Manual building and local testing
  • ๐Ÿ“ฆ Publishing to GitHub Packages and NuGet.org
  • ๐Ÿงช Running tests with code coverage

New to versioning? See Quick Start - Versioning for a 3-step guide to create your first release.

Quick Build

# Restore dependencies
dotnet restore

# Build the solution
dotnet build -c Release

# Run tests
dotnet test -c Release

# Pack the package
dotnet pack src/Voyager.Common.Results/Voyager.Common.Results.csproj -c Release

Automatic Publishing

Simply push to main branch - GitHub Actions will:

  1. โœ… Automatically bump version
  2. โœ… Build for both .NET 8.0 and .NET Framework 4.8
  3. โœ… Run all tests
  4. โœ… Publish to GitHub Packages
  5. โœ… Publish to NuGet.org (if configured)
git add .
git commit -m "Add new feature"
git push origin main

๐Ÿงช Running Tests

# Run all tests
dotnet test

# Run with code coverage
dotnet test --collect:"XPlat Code Coverage"

# Generate coverage report (requires reportgenerator)
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coverage-report -reporttypes:Html

Development Workflow

  • Push to main triggers automatic version bump and publishing
  • All tests must pass before merging
  • Follow existing code style and conventions
  • Add tests for new features
  • Update documentation as needed

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

Inspired by:

๐Ÿ“ Changelog

See CHANGELOG.md for a list of changes.

๐Ÿ“š Additional Resources


Made with โค๏ธ by Voyager Poland

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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. 
.NET Framework net48 is compatible.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETFramework 4.8

    • No dependencies.
  • net6.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Voyager.Common.Results:

Package Downloads
Voyager.DBConnection

Modern, testable database access library using DbProviderFactory with Result monad pattern for explicit error handling. Features IDbCommandExecutor interface for easy mocking, command factory pattern for separation of concerns, and fluent parameter API. Supports SQL Server, PostgreSQL, MySQL, Oracle, SQLite, and ODBC for universal database connectivity.

Voyager.Common.Proxy.Abstractions

Abstractions and attributes for Voyager.Common.Proxy - HTTP service proxy generation

Voyager.Common.Resilience

Resilience patterns (Circuit Breaker) for Railway Oriented Programming - extends Voyager.Common.Results with stateful fault tolerance patterns

Voyager.Common.Proxy.Server.Core

Core logic for Voyager.Common.Proxy.Server - service scanning, endpoint metadata building, and request dispatching.

Voyager.Common.Proxy.Client

HTTP client proxy generation for C# interfaces with Result<T> support

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.11.0-preview.1 38 3/9/2026
1.10.0 393 2/26/2026
1.10.0-preview.1.4 51 2/26/2026
1.10.0-preview.1 45 2/26/2026
1.9.1-preview.1 49 2/25/2026
1.9.0 933 2/19/2026
1.9.0-preview.1.2 46 2/19/2026
1.9.0-preview.1 49 2/18/2026
1.8.0 438 2/16/2026
1.8.0-preview.1.1 50 2/16/2026
1.8.0-preview.1 47 2/16/2026
1.7.3-preview.2 50 2/16/2026
1.7.2 112 2/14/2026
1.7.2-preview.6.3 52 2/14/2026
1.7.2-preview.6 51 2/13/2026
1.7.2-preview.5 54 2/13/2026
1.7.2-preview.4 52 2/13/2026
1.7.2-preview.3 51 2/13/2026
1.7.2-preview.2 47 2/13/2026
1.7.2-preview 86 2/13/2026
Loading failed

See CHANGELOG.md for release history