Skip to content

modabas/ModResults

Repository files navigation

ModResults

Nuget License: MIT

ModResults implements the Result pattern, providing a structured way to represent the outcome of operations as either success or failure. Instead of using exceptions or loosely-typed error codes, ModResults encapsulates operation results in strongly-typed objects to improve code readability, maintainability, and error handling. Uses nullability annotations for safer, more predictable code.

✨ Features

  • Result Pattern: Encapsulates success or failure states with associated error messages and additional information.
  • Easy-to-Use: Simplifies error handling and result management with a clear API.
  • Serialization: System.Text.Json serialization support without need for special configuration. Microsoft.Orleans serialization support via ModResults.Orleans package.
  • Ready-to-Use Implementations:
    • Result and Result<TValue> with a default Failure type.
    • Result<TValue, TFailure> for custom failure states.
  • 📦 Extension Packages for Various Scenarios:
    • ModResults.FluentValidation bridges FluentValidation with ModResults for unified validation error handling.
    • ModResults.MinimalApis provides extensions to convert Result and Result<TValue> instances to ASP.NET Core Minimal APIs' IResult responses with proper HTTP status codes and response formatting.
    • ModResults.Orleans provides necessary surrogate, converter and populator implementations required by the Orleans serializer.

🛠️ How To Use

Creating a Result or Result<TValue> Instance

  • Success: Use Result.Ok() or Result<TValue>.Ok(TValue value).
  • Failure: Use static factory methods like Result.NotFound() or Result<TValue>.Invalid() to create failed results with a specific failure type. Also, FailureResult class has same set of static factory methods to serve as a helper for returning failed Result<TValue> instances without the need to specify TValue directly. FailureResult can be converted to either Result or Result<TValue> via implicit operators or explicit AsResult(), AsResult<TValue>(), ToResult() and ToResult<TValue>() methods.

For each failure type (like Error, Forbidden, Unauthorized, etc.), there are several overloads that creates a failed result with the given FailureType:

  • No parameters: Creates a failed result with the given FailureType and no errors.
  • params IEnumerable<String> errorMessages: Converts each string to an Error and includes them in the result.
  • params IEnumerable<Error> errors: Directly includes the provided Error objects in the result.
  • params IEnumerable<Exception> exceptions: Converts each exception to an Error and includes them in the result.

These methods make it easy and consistent to create failed results with detailed error information, categorized by failure type, for both non-generic and generic result types.

public async Task<Result<GetBookByIdResponse>> GetBookById(GetBookByIdRequest req, CancellationToken ct)
{
    var entity = await db.Books.FirstOrDefaultAsync(b => b.Id == req.Id, ct);

    return entity is null ?
      FailureResult.NotFound() :
      Result.Ok(new GetBookByIdResponse(
        Id: entity.Id,
        Title: entity.Title,
        Author: entity.Author,
        Price: entity.Price));
}

Checking the State of a Result

A result can be either in an Ok or Failed state.

  • Ok State: If the IsOk property is true (IsFailed is false), a Result<TValue> instance will have a non-null Value property of type TValue.
  • Failed State: If the IsOk property is false (IsFailed is true), both Result and Result<TValue> instances will have a non-null Failure property.

Note: When using nullable contexts, the compiler will not generate "may be null" warnings in these cases, as the library leverages conditional attributes to assist null-state analysis.

Checking the state of a result to run some business logic is straightforward:

public async Task<Result<PerformGetBookByIdResponse>> PerformGetBookById(GetBookByIdRequest req, CancellationToken ct)
{
    var result = await GetBookById(req, ct);

    if (result.IsFailed)
    {
        // Handle failure case,
        Console.WriteLine($"GetBookById has failed.");
        // Return the failure information as a failed Result<PerformGetBookByIdResponse>
        // FailureResult can be implicitly converted to Result<TAnyValue>, so we can return it directly.
        return FailureResult.From(result);
    }
    // Handle success case, access value via result.Value
    // ...
}

Sometimes, you may want to convert a Result<TValue> to a Result (which doesn't hold a value object, just state and information related to it) while preserving the failure information. You can do this using the implicit operator or the AsResult()/ToResult() methods:

public async Task<Result> PerformGetBookById(GetBookByIdRequest req, CancellationToken ct)
{
    var result = await GetBookById(req, ct);
    if (result.IsOk)
    {
        Console.WriteLine($"GetBookById is successful. Book title is {result.Value.Title}");
    }
    else
    {
        Console.WriteLine($"GetBookById has failed.");
    }
    // Implicitly convert Result<GetBookByIdResponse> to Result, preserving state and any failure information.
    return result
}

Other times, you may want to convert a Result<TSourceValue> to a Result<TTargetValue> while preserving the failure information. You can do this using the AsResult<TTargetValue>()/ToResult<TTargetValue>() methods or any similar overloads with same name:

public async Task<Result<PerformGetBookByIdResponse>> PerformGetBookById(GetBookByIdRequest req, CancellationToken ct)
{
    var result = await GetBookById(req, ct);

    // Convert Result<GetBookByIdResponse> to Result<PerformGetBookByIdResponse>, 
    // preserving state and any failure information.
    // Factory parameter will be called only if the source result is in Ok state,
    // and it will receive the value of type GetBookByIdResponse (TSourceValue)
    // to create a value of type PerformGetBookByIdResponse (TTargetValue).
    return result.AsResult<PerformGetBookByIdResponse>(v => 
    {
        // Construct a PerformGetBookByIdResponse using the value of type GetBookByIdResponse (v) and return it. 
        // This factory will only be called if the result is in Ok state, 
        // otherwise the failure information will be preserved and returned as a failed Result<PerformGetBookByIdResponse>.
        return new PerformGetBookById(v.Name);
    });
}

Explore More Features

For further examples showcasing other functionalities, refer to the following:

About

Result pattern library for handling success/failure states for either in-process or networked scenarios.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages