Linger.Results 1.6.2

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

Linger.Results

A modern operation result handling library designed with a functional style approach, helping developers handle various operation results more elegantly. By using the Result pattern instead of exceptions, it enables more controllable and predictable error handling processes.

Upgrading from 1.6 to 2.0? See Preparing for Linger 2.0.

Features

  • Provides clear success/failure result representation
  • Supports generic results that can carry return values of any type
  • Offers rich functional operations (mapping, binding, combining, etc.)
  • Strong typing for error handling, more controllable than exceptions
  • Supports multiple frameworks including .Net10.0, .NET 9.0, .NET 8.0, .NET Standard 2.0, and .NET Framework 4.7.2

Installation

dotnet add package Linger.Results

Basic Usage

Creating Results

// Create success results
var success = Result.Success();
var successWithValue = Result.Success(42);

// Create failure results
var failure = Result.Failure("Operation failed");
var failureWithError = Result.Failure(new Error("ErrorCode", "Detailed error message"));

// Create not found results
var notFound = Result.NotFound();

Using Generic Results

// Define a method that returns user information
public Result<User> GetUser(int id)
{
    var user = _repository.FindById(id);
    if (user == null)
        return Result<User>.NotFound($"User with ID {id} not found");
    
    return Result<User>.Success(user);
}

// Elegant syntax with implicit conversion
public Result<User> GetUserWithValidation(string email)
{
    // Validate email
    if (string.IsNullOrEmpty(email))
    {
        // Directly return Result.Failure, automatically converts to Result<User>
        return Result.Failure(new Error("ValidationError", "Email cannot be empty"));
    }

    var user = _repository.FindByEmail(email);
    if (user == null)
    {
        // This also converts automatically
        return Result.NotFound("User not found");
    }

    return Result<User>.Success(user);
}

// Usage example
var result = GetUser(123);
if (result.IsSuccess)
{
    var user = result.Value; // Value is only accessible when result is successful
    Console.WriteLine($"Found user: {user.Name}");
}
else
{
    Console.WriteLine($"Error: {string.Join(", ", result.Errors.Select(e => e.Message))}");
}

Using Match Method

// Use Match method to handle different result states
string displayName = result.Match(
    user => $"User: {user.Name}",
    errors => $"Error: {string.Join(", ", errors.Select(e => e.Message))}"
);

Safe Value Access

// Use TryGetValue to safely access result value
if (result.TryGetValue(out var user))
{
    // Successfully obtained user
    Console.WriteLine($"User: {user.Name}");
}

// Use ValueOrDefault to get value or default value
var safeUser = result.ValueOrDefault; // null when failed

// Specify default value
var userOrGuest = result.GetValueOrDefault(new User { Name = "Guest" });

Method Chaining

// Use extension methods for method chaining
var finalResult = GetUser(123)
    .Ensure(user => user.IsActive, new Error("User.Inactive", "User is not active"))
    .Map(user => user.Email)
    .Bind(email => SendEmail(email));

Async Support

All async extension methods require a CancellationToken parameter for proper cancellation support:

// Async operations with CancellationToken
var result = await GetUserAsync(123)
    .MapAsync(async (user, token) => await GetUserPreferencesAsync(user, token), cancellationToken)
    .BindAsync(async (prefs, token) => await UpdatePreferencesAsync(prefs, token), cancellationToken);

Async Support with CancellationToken

// All async extension methods support CancellationToken for cancellable operations
public async Task<Result<OrderSummary>> ProcessOrderAsync(int orderId, CancellationToken cancellationToken)
{
    return await GetOrderAsync(orderId)
        // MapAsync with CancellationToken
        .MapAsync(async (order, token) => 
        {
            // Perform async transformation with cancellation support
            return await CalculateTotalAsync(order, token);
        }, cancellationToken)
        
        // BindAsync with CancellationToken
        .BindAsync(async (total, token) => 
        {
            // Chain another Result-returning async operation
            return await ValidatePaymentAsync(total, token);
        }, cancellationToken)
        
        // EnsureAsync with CancellationToken
        .EnsureAsync(
            async (payment, token) => await CheckInventoryAsync(payment, token),
            new Error("Inventory", "Insufficient inventory"),
            cancellationToken)
        
        // MatchAsync with CancellationToken
        .MatchAsync(
            async (payment, token) => 
            {
                await SendConfirmationEmailAsync(payment, token);
                return Result<OrderSummary>.Success(new OrderSummary(payment));
            },
            async (errors, token) => 
            {
                await LogErrorsAsync(errors, token);
                return Result<OrderSummary>.Failure(errors);
            },
            cancellationToken);
}

// Example: Using with HttpClient or database operations
public async Task<Result<User>> UpdateUserWithCancellationAsync(
    User user, 
    CancellationToken cancellationToken)
{
    return await ValidateUser(user)
        .MapAsync(async (validUser, token) => 
        {
            // Database operation with cancellation
            await _dbContext.Users.AddAsync(validUser, token);
            await _dbContext.SaveChangesAsync(token);
            return validUser;
        }, cancellationToken)
        .EnsureAsync(
            async (savedUser, token) => 
            {
                // Verify the save operation
                var exists = await _dbContext.Users
                    .AnyAsync(u => u.Id == savedUser.Id, token);
                return exists;
            },
            new Error("Database", "User save verification failed"),
            cancellationToken);
}

Using Result.Create for Condition Checking

// Create results based on boolean conditions
public Result ValidatePassword(string password)
{
    var results = new[]
    {
        Result.Create(password.Length >= 8),
        Result.Create(password.Any(char.IsUpper)),
        Result.Create(password.Any(char.IsDigit))
    };
    
    return results.Combine();
}

// Or use Ensure with Result<T> for chained validation
public Result<string> ValidateAndReturnPassword(string password)
{
    return Result.Success(password)
        .Ensure(p => p.Length >= 8, new Error("Password", "Password must be at least 8 characters"))
        .Ensure(p => p.Any(char.IsUpper), new Error("Password", "Password must contain uppercase letters"))
        .Ensure(p => p.Any(char.IsDigit), new Error("Password", "Password must contain digits"));
}

Implicit Conversion and Elegant Syntax

Linger.Results provides powerful implicit conversion features to make your code more concise and elegant. It supports three types of implicit conversions:

1. Implicit Conversion from Result to Result<T>

public Result<User> CreateUser(CreateUserRequest request)
{
    // Validate username
    if (string.IsNullOrEmpty(request.Username))
    {
        // Directly return Result.Failure, automatically converts to Result<User>
        return Result.Failure("Username cannot be empty");
    }
    
    // Check email format
    if (!IsValidEmail(request.Email))
    {
        // Using custom error object, also converts automatically
        return Result.Failure(new Error("Email.Invalid", "Invalid email format"));
    }
    
    // Check if user already exists
    if (UserExists(request.Username))
    {
        // NotFound also supports implicit conversion
        return Result.NotFound("Username is already taken");
    }
    
    // Success case
    var user = new User { Username = request.Username, Email = request.Email };
    return Result<User>.Success(user);
}

2. Implicit Conversion from Result<T> to Result

public Result ProcessUser(int userId)
{
    // Get user (returns Result<User>)
    Result<User> userResult = GetUser(userId);
    
    // Automatically converts to Result, loses specific value but preserves status and error info
    Result processResult = userResult; 
    
    if (processResult.IsSuccess)
    {
        // Execute processing logic
        return Result.Success();
    }
    
    // Error information is preserved
    return processResult;
}

3. Implicit Conversion from T to Result<T>

public Result<User> GetDefaultUser()
{
    var defaultUser = new User { Name = "Default User", Email = "default@example.com" };
    
    // Object automatically converts to successful Result<User>
    return defaultUser;
}

public Result<string> GetConfigValue(string key)
{
    string value = _configuration[key];
    
    // If value is null, automatically creates failure result
    // If value is not null, automatically creates success result
    return value; // Equivalent to Result<string>.Create(value)
}

Chained Conversion Examples

// Demonstrates different return types that can implicitly convert to Result<User>
private Result<User> GetUserById(int id)
{
    return id switch
    {
        1 => _testUser,                     // User → Result<User>
        0 => Result.Success(),              // Result → Result<User>  
        _ => Result.Failure("User not found") // Result → Result<User>
    };
}

// Can freely convert between different result types
private Result ProcessUserData(Result<User> userResult)
{
    // Result<User> → Result
    return userResult; 
}

### API Design Principles

After optimization, Linger.Results follows these design principles:

1. **Clear API Boundaries**: `Result` class focuses on non-generic operations, `Result<T>` class handles operations with return values
2. **Implicit Conversion Support**: Supports natural conversion from `Result` to `Result<T>`, but avoids unexpected value conversions
3. **Type Safety**: Ensures type correctness at compile time, avoiding runtime errors
4. **Concise Syntax**: Reduces boilerplate code and improves development efficiency

### Important Notes on Implicit Conversion

⚠️ **Important Notes**:
- `Result<T>` → `Result` conversion will **lose value information**, as non-generic Result doesn't store specific values
- In `T` → `Result<T>` conversion, if value is `null`, it automatically creates a failure result
- Accessing `.Value` property on a failed `Result<T>` will throw `InvalidOperationException`
- Recommended to use `.ValueOrDefault` or `.TryGetValue()` for safe value access

```csharp
// Correct usage examples
Result<User> userResult = GetUser(123);

// ✅ Safe value access
if (userResult.TryGetValue(out var user))
{
    Console.WriteLine($"User: {user.Name}");
}

// ✅ Using default value
var safeUser = userResult.ValueOrDefault;

// ❌ Dangerous: Will throw exception if result is failed
var user = userResult.Value; // May throw InvalidOperationException

Error Handling

// Use Try method to catch exceptions and convert to results
var result = ResultExtensions.Try(
    () => SomeOperationThatMightThrow(),
    ex => ex.ToError()
);

Advanced Usage

Combining Multiple Results

// Combine multiple results, succeeds only when all results succeed
var combinedResult = Result.Combine(
    ValidateUsername(request.Username),
    ValidateEmail(request.Email),
    ValidatePassword(request.Password)
);

if (combinedResult.IsSuccess)
{
    // All validations passed
    return Result.Success(new User { /* ... */ });
}

Custom Error Types

// Define domain-specific error types
public static class UserErrors
{
    public static readonly Error NotFound = new("User.NotFound", "User not found");
    public static readonly Error InvalidCredentials = new("User.InvalidCredentials", "Invalid username or password");
    public static readonly Error DuplicateEmail = new("User.DuplicateEmail", "Email is already in use");
}

// Use custom errors
public Result<User> Authenticate(string username, string password)
{
    var user = _repository.FindByUsername(username);
    if (user == null)
        return Result.Failure(UserErrors.NotFound); // Implicit conversion to Result<User>
        
    if (!ValidatePassword(password, user.PasswordHash))
        return Result.Failure(UserErrors.InvalidCredentials); // Implicit conversion to Result<User>
        
    return Result<User>.Success(user);
}

Conditional Branch Processing

public async Task<Result<OrderConfirmation>> ProcessOrder(Order order, CancellationToken cancellationToken)
{
    // Chain process order workflow
    return await ValidateOrder(order)
        .BindAsync(async (validOrder, token) => 
        {
            // Choose different processing paths based on payment method
            if (validOrder.PaymentMethod == PaymentMethod.CreditCard)
                return await ProcessCreditCardPayment(validOrder, token);
            else if (validOrder.PaymentMethod == PaymentMethod.BankTransfer)
                return await ProcessBankTransfer(validOrder, token);
            else
                return Result<OrderConfirmation>.Failure("Unsupported payment method");
        }, cancellationToken);
}

ResultStatus Enum

The ResultStatus enum includes three states:

  • Ok - Operation succeeded
  • NotFound - Resource not found
  • Error - Operation failed

For additional status codes, you can use Error with specific error codes:

// Define domain-specific error codes for different scenarios
public static class StatusErrors
{
    public static readonly Error Unauthorized = new("Status.Unauthorized", "Authentication required");
    public static readonly Error Forbidden = new("Status.Forbidden", "Access denied");
    public static readonly Error Conflict = new("Status.Conflict", "Resource conflict");
    public static readonly Error ValidationError = new("Status.ValidationError", "Validation failed");
}

// Usage
public Result<User> GetUser(int id, string token)
{
    if (!IsValidToken(token))
        return Result.Failure(StatusErrors.Unauthorized);
        
    if (!HasPermission(token, "read:users"))
        return Result.Failure(StatusErrors.Forbidden);
        
    // ...
}

Best Practices

  1. Prefer Result and Result<T> over exceptions:

    • Return Result for expected errors (validation errors, resource not found, etc.)
    • Use exceptions only for truly exceptional situations (program errors, unexpected system failures)
  2. Maintain consistency in return values:

    • Service methods should consistently return Result or Result<T>, not mix results and exceptions
    • Maintain unified error handling patterns for easier handling by callers
  3. Use meaningful error codes:

    • Define domain-specific error constants
    • Use structured error codes (like "Category.SubCategory.Error")
  4. Leverage implicit conversion to simplify code:

    • In methods returning Result<T>, you can directly return Result.Failure() or Result.NotFound()
    • This makes code more concise and elegant while maintaining type safety
  5. Use API correctly:

    • For generic results, use Result<T>.Success(), Result<T>.Failure() methods directly
    • Leverage implicit conversion from Result to Result<T> instead of relying on removed forwarding methods
  6. Leverage method chaining:

    • Use functional composition instead of traditional conditional statements
    • Map, Bind, Tap methods can greatly improve code readability
  7. For Web APIs:

    • Combine with Linger.Results.AspNetCore package to convert to HTTP responses
    • Use ProblemDetails format to return standardized error responses

Comparison with Exception Handling

Aspect Result Pattern Exception Mechanism
Visibility Explicit return type, visible at compile time Implicit throwing, known only at runtime
Performance Better, no stack capture overhead Worse, especially in high-frequency call scenarios
Composability Excellent, supports chaining and composition operations Weak, requires multiple try-catch layers
Type Safety Strong typing, compiler assistance Weak typing, based on string matching
Use Cases Business logic, expected errors Program errors, unexpected exceptions

License

MIT

Migration from ExecuteResult

⚠️ Note: ExecuteResult, ExecuteResult<T>, and ErrorObj are now obsolete and will be removed in a future version. Please migrate to Result and Result<T>.

// Old code (deprecated)
var result = new ExecuteResult(true, "Success");
var resultWithValue = new ExecuteResult<User>(user);

// New code (recommended)
var result = Result.Success();
var resultWithValue = Result.Success(user);

// Accessing errors
// Old: result.Message
// New: result.FirstError.Message or result.Errors

FirstError Property

Both Result and Result<T> now have a FirstError property for convenient access to the first error:

// Old way
var message = result.Errors.FirstOrDefault()?.Message ?? "";

// New way
var message = result.FirstError.Message; // Returns Error.None if no errors
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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 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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 is compatible.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETFramework 4.7.2

    • No dependencies.
  • .NETStandard 2.0

    • No dependencies.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Linger.Results:

Package Downloads
Linger.Results.AspNetCore

ASP.NET Core integration for Linger.Results library, providing extension methods to seamlessly convert Result objects to ActionResults with appropriate HTTP status codes. Simplifies API response handling with RFC 7807 support and automatic status mapping.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.6.4 58 8/16/2026
1.6.3 109 8/5/2026
1.6.2 124 8/2/2026
1.6.0 126 7/25/2026
1.5.5 125 7/23/2026
1.5.4-preview 106 7/21/2026
1.5.3-preview 111 7/20/2026
1.5.2-preview 117 7/19/2026
1.5.1-preview 110 7/15/2026
1.5.0-preview 109 7/14/2026
1.4.4-preview 126 6/16/2026
1.4.3-preview 113 6/15/2026
1.4.2 128 5/20/2026
1.4.1-preview 126 5/12/2026
1.4.0 134 5/6/2026
1.3.3-preview 116 5/5/2026
1.3.2-preview 122 4/29/2026
1.3.1-preview 120 4/28/2026
1.3.0-preview 112 4/27/2026
1.2.0-preview 130 3/29/2026
Loading failed