Managing Operation Status with IResultStatus and ResultStatuses - ulfbou/Zentient.Results GitHub Wiki

Managing Operation Status with IResultStatus and ResultStatuses

In Zentient.Results, the IResultStatus interface and the ResultStatuses static class play a crucial role in providing a high-level, standardized indicator of an operation's outcome. While ErrorInfo details why a failure occurred, IResultStatus succinctly describes what kind of outcome it was.

This page explains how these components work to enhance clarity and consistency in your application's responses.


1. The Role of IResultStatus

IResultStatus is an interface that defines the contract for any object representing the status of an operation. It's designed to be a concise summary of the result, often aligning with well-known numeric codes like HTTP status codes.

Key Distinction: IResultStatus vs. ErrorInfo

It's important to understand the difference between these two:

  • IResultStatus (What happened?): Provides a high-level summary of the result. Think of it as the HTTP status code: 200 OK, 404 Not Found, 400 Bad Request. It tells you the category of the outcome.
  • ErrorInfo (Why it happened?): Provides granular, detailed reasons for a failure. For example, if the IResultStatus is BadRequest (400), the ErrorInfo collection would contain specific details like "Username is too short," or "Email format is invalid."

IResultStatus Properties:

Property Type Description
Code int A numeric code representing the status (e.g., 200, 404, 500).
Description string A human-readable description of the status (e.g., "OK", "Not Found").

Why an Interface?

Using an interface for IResultStatus provides:

  • Extensibility: Allows you to define your own custom status types if the predefined ResultStatuses don't fully cover your domain's needs.
  • Polymorphism: Enables methods to accept any type that implements IResultStatus, promoting flexible design.

2. ResultStatus Struct: The Default Implementation

ResultStatus is the concrete readonly struct that implements IResultStatus. It's the standard way to represent an operation's status within Zentient.Results.

Properties and Immutability:

  • It has Code (int) and Description (string) properties, mirroring IResultStatus.
  • Being a readonly struct, ResultStatus instances are immutable. Once created, their values cannot be changed, ensuring predictable behavior and thread safety.

Equality (IEquatable<ResultStatus>):

ResultStatus implements IEquatable<ResultStatus>, meaning you can reliably compare two ResultStatus instances for value equality:

using Zentient.Results;

var status1 = ResultStatuses.NotFound;
var status2 = ResultStatus.Custom(404, "Not Found");

Console.WriteLine(status1 == status2); // Output: True (compares by Code and Description)

Creating Custom ResultStatus Instances:

While ResultStatuses provides many defaults, you can create your own custom ResultStatus using the ResultStatus.Custom() static method:

using Zentient.Results;

public static class CustomDomainStatuses
{
    public static readonly IResultStatus OrderPartiallyFulfilled =
        ResultStatus.Custom(206, "Partially Fulfilled"); // HTTP 206 Partial Content
    
    public static readonly IResultStatus InsufficientStock =
        ResultStatus.Custom(428, "Insufficient Stock"); // Example: Custom client-side status
}

3. ResultStatuses Class: Predefined Standard Statuses

ResultStatuses is a static class that acts as a central registry for commonly used IResultStatus instances. It provides a comprehensive collection of statuses, largely aligned with standard HTTP status codes.

This class reduces boilerplate, improves consistency, and provides clear, well-understood status indicators, especially for applications that expose APIs.

Examples of Predefined ResultStatuses:

ResultStatuses Property Code Description Typical HTTP Mapping
Success 200 "OK" 200 OK
Created 201 "Created" 201 Created
NoContent 204 "No Content" 204 No Content
BadRequest 400 "Bad Request" 400 Bad Request
Unauthorized 401 "Unauthorized" 401 Unauthorized
Forbidden 403 "Forbidden" 403 Forbidden
NotFound 404 "Not Found" 404 Not Found
Conflict 409 "Conflict" 409 Conflict
UnprocessableEntity 422 "Unprocessable Entity" 422 Unprocessable Entity
InternalServerError 500 "Internal Server Error" 500 Internal Server Error
ServiceUnavailable 503 "Service Unavailable" 503 Service Unavailable

Usage:

using Zentient.Results;

// Using predefined statuses for clarity
return Result<User>.Success(newUser, ResultStatuses.Created); // Specific success type
return Result.Failure(errorInfo, ResultStatuses.BadRequest); // Common for validation errors
return Result<Order>.NotFound(errorInfo); // Convenience method uses ResultStatuses.NotFound internally

4. Integrating IResultStatus with Result Types

When you create Result or Result<T> instances, you often provide an IResultStatus.

  • Success Methods:
    • Result.Success() defaults to ResultStatuses.Success.
    • Result<T>.Success(value) defaults to ResultStatuses.Success.
    • You can explicitly pass a status: Result.Success(ResultStatuses.NoContent), Result<T>.Success(user, ResultStatuses.Created).
  • Failure Methods:
    • Most Failure factory methods for both Result and Result<T> accept an IResultStatus as a parameter.
    • Convenience methods like Result.NotFound(), Result<T>.BadRequest() automatically set the appropriate ResultStatuses internally.
using Zentient.Results;
using System;

public class ProfileService
{
    public IResult<UserProfile> GetProfile(Guid id)
    {
        if (id == Guid.Empty)
        {
            // Explicitly pass status
            return Result<UserProfile>.Failure(
                default,
                new ErrorInfo(ErrorCategory.Validation, "InvalidId", "ID cannot be empty."),
                ResultStatuses.BadRequest);
        }
        // ... (logic)
        return Result<UserProfile>.NotFound(
            new ErrorInfo(ErrorCategory.NotFound, "ProfileNotFound", $"Profile for {id} was not found.")
        );
    }
}
public class UserProfile { public Guid Id { get; set; } }

5. Consuming IResultStatus

Once you have a Result object, you can access its Status property to make high-level decisions, especially in presentation layers.

using Zentient.Results;
using System;
using Microsoft.AspNetCore.Mvc; // Assuming ASP.NET Core context

public class UserController : ControllerBase
{
    private ProfileService _profileService = new ProfileService();

    [HttpGet("{id}")]
    public IActionResult GetUserProfile(Guid id)
    {
        IResult<UserProfile> result = _profileService.GetProfile(id);

        // Accessing status code and description
        Console.WriteLine($"Result Status: {result.Status.Code} - {result.Status.Description}");

        if (result.IsSuccess)
        {
            return Ok(result.Value); // Returns 200 OK
        }
        else
        {
            // Map ResultStatus code to HttpStatusCode and return appropriate IActionResult
            return StatusCode(result.Status.Code, new { errors = result.Errors, message = result.Error });
            // This will return 400 BadRequest, 404 NotFound etc., based on result.Status.Code
        }
    }

    // Example of using status for conditional logic
    public void ProcessQueryResult(IResult someResult)
    {
        if (someResult.Status == ResultStatuses.NotFound) // Using value equality
        {
            Console.WriteLine("The requested resource was not found. Display a user-friendly '404' page.");
        }
        else if (someResult.Status.Code >= 400 && someResult.Status.Code < 500)
        {
            Console.WriteLine("A client-side error occurred. Prompt user to fix input.");
        }
        else if (someResult.Status.Code >= 500)
        {
            Console.WriteLine("A server error occurred. Log and notify support.");
        }
    }
}

6. Extending IResultStatus (Custom Statuses)

While ResultStatuses covers common HTTP-aligned scenarios, your domain might have unique outcomes that don't perfectly map to standard HTTP codes. In such cases, you can define your own custom IResultStatus instances.

When to use custom statuses:

  • For internal domain-specific states that don't need to be exposed as standard HTTP codes.
  • When a standard HTTP code exists, but you want a more descriptive internal name (e.g., ResultStatus.Custom(202, "AcceptedForProcessing")).
  • To represent a successful but non-standard outcome (e.g., a batch operation where some items succeed and some fail, resulting in a specific "partial success" status).
using Zentient.Results;

// Define your custom statuses
public static class MyServiceStatuses
{
    public static readonly IResultStatus OrderPartiallyProcessed =
        ResultStatus.Custom(207, "Partially Processed"); // HTTP 207 Multi-Status, example use

    public static readonly IResultStatus ItemOutOfStock =
        ResultStatus.Custom(470, "Item Out of Stock"); // A custom client error code, e.g., 4xx range not used by HTTP
}

public class OrderService
{
    public IResult<Order> ProcessOrder(Order order)
    {
        // ... some logic ...
        if (order.Items.Any(item => item.Quantity > GetStock(item.ProductId)))
        {
            // Return custom status if some items are out of stock
            return Result<Order>.Failure(
                order, // Optionally pass the order itself
                new ErrorInfo(ErrorCategory.Conflict, "InsufficientStock", "Some items are out of stock."),
                MyServiceStatuses.ItemOutOfStock
            );
        }
        // ... successful processing ...
        return Result<Order>.Success(order);
    }

    private int GetStock(string productId) => 5; // Placeholder
}

7. Best Practices

  • Prioritize ResultStatuses: Always use the predefined statuses in ResultStatuses first, as they promote consistency and interoperability, especially for public APIs.
  • Use Custom Statuses Sparingly: Create custom statuses only when a standard HTTP code or an existing ResultStatuses entry genuinely doesn't fit your domain's specific outcome.
  • Document Custom Statuses: If you define custom statuses, ensure they are clearly documented for all consumers of your code/API.
  • Status for High-Level, Errors for Detail: Remember the separation: IResultStatus for what happened (e.g., 400 Bad Request), and ErrorInfo for why (e.g., "Field 'x' is required").
  • Map to HTTP: If you're building a web API, always map your internal IResultStatus codes to appropriate HTTP status codes in your controllers.

Next Steps

With a clear understanding of operation statuses, you can now explore:

⚠️ **GitHub.com Fallback** ⚠️