LightResults

repository·develop·Indexed 42 days ago

https://github.com/jscarle/lightresults

A lightweight, high-performance .NET library for implementing the Result Pattern. It provides structured types—Result, Result<TValue>, and Error—to represent operation outcomes and replace exception-based control flow. Designed for minimal allocations and low latency, it offers features such as custom domain errors, metadata support, and pattern-matching style access via out parameters in v9.0 and v10.0.

Tokens
7K
Snippets
19
Records
29
Agent score
52%

What's inside LightResults

  1. Overview of LightResults

    develop

    LightResults is a lightweight and modern .NET library that implements the Result Pattern. The Result Pattern provides a structured way to represent the outcome of an operation, explicitly distinguishing between success and failure instead of relying on exceptions for control flow.

    Key characteristics include:

    • Immutable: Results and errors cannot be modified after creation.
    • Thread-safe: Error and metadata collections are read-only.
    • AOT Compatible: Supports ahead-of-time compilation (Native AOT).
    • High Performance: Optimized and benchmarked for minimal overhead.
  2. LightResults Namespaces and Target Frameworks

    develop

    Namespaces

    • LightResults: Contains core types: Result, Result<TValue>, Error, IResult, IResult<TValue>, and IError.
    • LightResults.Common: Contains static-abstract interfaces IActionableResult<TResult> and IActionableResult<TValue, TResult>.

    Target Frameworks

    LightResults is designed for modern .NET and is AOT-compatible. It targets:

    • net8.0
    • net9.0
    • net10.0
  3. Performance characteristics of LightResults

    develop

    LightResults is designed as a high-performance alternative to feature-rich result pattern libraries like FluentResults or Ardalis.Result. It achieves exceptional performance by intentionally simplifying its API to minimize allocations and execution time.

    Key performance advantages include:

    • Zero or minimal allocations for many common operations (e.g., Result.Success(), Result.Failure(), and result.IsSuccess()).
    • Low latency for returning and checking results, often performing in the low nanosecond range.
    • Efficient value retrieval using out parameters to avoid extra allocations.
  4. How LightResults core types work together

    develop

    LightResults is built around three primary types that implement the Result Pattern:

    1. Result: A struct representing a generic outcome (success or failure).
    2. Result<TValue>: A struct representing a successful outcome that carries a specific value of type TValue, or a failure.
    3. Error: A struct representing a failure, containing a message, optional metadata (key-value pairs), and an optional exception.

    By using these types, you can replace throwing exceptions with explicit, structured return types, making your code more predictable and easier to reason about.

  5. Core types in LightResults

    develop

    LightResults is built around three primary types that facilitate the operation result pattern in .NET:

    • Result: A struct representing a generic result indicating success or failure without an associated value.
    • Result<TValue>: A struct representing a success or failure result that carries a value of type TValue upon success.
    • Error: A struct representing an error, containing a message, optional metadata, and an optional Exception.
  6. Breaking changes in LightResults v9.0 API

    develop

    The v9.0 redesign introduced several breaking changes to improve performance and safety:

    • Factory Method Renaming:
      • Result.Ok() $\rightarrow$ Result.Success()
      • Result.Fail() $\rightarrow$ Result.Failure()
      • Result<TValue>.Ok() $\rightarrow$ Result.Success<TValue>()
      • Result<TValue>.Fail() $\rightarrow$ Result.Failure<TValue>()
    • Property Removal: The .Value and .Error properties have been removed to prevent unsafe access. Use IsSuccess(out var value) and IsFailure(out var error) instead.
    • Error Constructor Changes:
      • Metadata constructors using IDictionary<string, object> were removed.
      • Error(string message, IDictionary<string, object> metadata) is now Error(string message, IReadOnlyDictionary<string, object> metadata).
  7. Implement and use custom errors

    develop

    To represent domain-specific errors, inherit from Error and pass the error message to the base constructor. This allows you to perform type-based checks on results.

    // 1. Define the custom error
    public sealed class NotFoundError : Error
    {
        public NotFoundError()
            : base("The resource cannot be found.")
        {
        }
    }
    
    // 2. Use it in a result
    var notFoundError = new NotFoundError();
    var notFoundResult = Result.Failure(notFoundError);
    
    // 3. Check for the specific error type
    if (result.IsFailure(out var error) && error is NotFoundError)
    {
        // Handle specific error logic
    }
    
    // 4. Or check if the result contains that error type
    if (result.IsFailure() && result.HasError<NotFoundError>())
    {
        // At least one of the errors was a NotFoundError.
    }
  8. Implement custom errors

    develop

    To represent domain-specific errors, inherit from Error and pass the error message to the base constructor. This allows you to use type checking to handle specific error scenarios.

    // 1. Define the custom error
    public sealed class NotFoundError : Error
    {
        public NotFoundError()
            : base("The resource cannot be found.")
        {
        }
    }
    
    // 2. Use it in a result
    var notFoundError = new NotFoundError();
    var notFoundResult = Result.Failure(notFoundError);
    
    // 3. Check for the specific error type
    if (result.IsFailure(out var error) && error is NotFoundError)
    {
        // Handle specific error logic
    }
    
    // Or check if the result contains an error of a specific type
    if (result.IsFailure() && result.HasError<NotFoundError>())
    {
        // Handle logic
    }
  9. Handle exceptions by converting them to Results

    develop

    Instead of letting exceptions bubble up, you can catch them and return a failed result using Result.Failure(ex). This keeps your method signatures explicit about failure states.

    public Result DoSomeWork()
    {
        try
        {
            // Perform work
        }
        catch(Exception ex)
        {
            return Result.Failure(ex);
        }
    
        return Result.Success();
    }
  10. Migrate from LightResults v8.0 to v9.0/v10.0

    develop

    If you are upgrading from version 8.0, the API has been redesigned to prevent exceptions caused by accessing properties without checking the result state. The primary change is the removal of .Value and .Error properties in favor of pattern-matching style methods that use out parameters.

    Migration Steps

    To minimize manual refactoring, follow these steps in order:

    1. Replace Result.Ok( with Result.Success(.
    2. Replace Result.Fail( with Result.Failure(.
    3. Use regex to replace Result<[^>]+>\.Ok\( with Result.Success$1(.
    4. Use regex to replace Result<[^>]+>\.Fail\( with Result.Failure$1(.
    5. Replace .IsSuccess with IsSuccess(out var value).
    6. Replace .IsFailed with IsFailure(out var error).
    7. Refactor result.Value to use the value variable exposed by the IsSuccess() method.
    8. Refactor result.Error to use the error variable exposed by the IsFailure() method.
  11. LightResults Usage Patterns and Best Practices

    develop

    To use LightResults effectively in your .NET application, follow these patterns:

    1. Avoid Exceptions for Flow Control: Return Result or Result<TValue> from methods instead of throwing exceptions for expected failure states.
    2. Factory Methods: Always use Result.Success() or Result.Failure() instead of attempting to use constructors.
    3. Explicit State Checking: Always check IsSuccess() or IsFailure() before attempting to access the value or error properties of a result.
    4. Type-Specific Error Handling: Use HasError<TError>() to branch logic based on specific error types (e.g., ValidationError vs DatabaseError).
    5. Immutability: Treat Result, Result<TValue>, and Error as immutable objects.