Managing Operation Status with IResultStatus and ResultStatuses - ulfbou/Zentient.Results GitHub Wiki
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.
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 theIResultStatusisBadRequest (400), theErrorInfocollection would contain specific details like "Username is too short," or "Email format is invalid."
| 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"). |
Using an interface for IResultStatus provides:
-
Extensibility: Allows you to define your own custom status types if the predefined
ResultStatusesdon't fully cover your domain's needs. -
Polymorphism: Enables methods to accept any type that implements
IResultStatus, promoting flexible design.
ResultStatus is the concrete readonly struct that implements IResultStatus. It's the standard way to represent an operation's status within Zentient.Results.
- It has
Code(int) andDescription(string) properties, mirroringIResultStatus. - Being a
readonly struct,ResultStatusinstances are immutable. Once created, their values cannot be changed, ensuring predictable behavior and thread safety.
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)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
}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.
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 |
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 internallyWhen you create Result or Result<T> instances, you often provide an IResultStatus.
-
Success Methods:
-
Result.Success()defaults toResultStatuses.Success. -
Result<T>.Success(value)defaults toResultStatuses.Success. - You can explicitly pass a status:
Result.Success(ResultStatuses.NoContent),Result<T>.Success(user, ResultStatuses.Created).
-
-
Failure Methods:
- Most
Failurefactory methods for bothResultandResult<T>accept anIResultStatusas a parameter. - Convenience methods like
Result.NotFound(),Result<T>.BadRequest()automatically set the appropriateResultStatusesinternally.
- Most
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; } }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.");
}
}
}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
}-
Prioritize
ResultStatuses: Always use the predefined statuses inResultStatusesfirst, 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
ResultStatusesentry 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:
IResultStatusfor what happened (e.g., 400 Bad Request), andErrorInfofor why (e.g., "Field 'x' is required"). -
Map to HTTP: If you're building a web API, always map your internal
IResultStatuscodes to appropriate HTTP status codes in your controllers.
With a clear understanding of operation statuses, you can now explore:
-
Integrating with ASP.NET Core Controllers and Minimal APIs: Learn how to map
IResultStatusdirectly to HTTP responses. - Chaining and Composing Operations: See how statuses affect the flow of chained operations.
-
Structured Error Handling with
ErrorInfoandErrorCategory: A deeper dive into the detailed error model