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 theIResultStatus
isBadRequest (400)
, theErrorInfo
collection 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
ResultStatuses
don'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
,ResultStatus
instances 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 internally
When 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
Failure
factory methods for bothResult
andResult<T>
accept anIResultStatus
as a parameter. - Convenience methods like
Result.NotFound()
,Result<T>.BadRequest()
automatically set the appropriateResultStatuses
internally.
- 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 inResultStatuses
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), andErrorInfo
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.
With a clear understanding of operation statuses, you can now explore:
-
Integrating with ASP.NET Core Controllers and Minimal APIs: Learn how to map
IResultStatus
directly to HTTP responses. - Chaining and Composing Operations: See how statuses affect the flow of chained operations.
-
Structured Error Handling with
ErrorInfo
andErrorCategory
: A deeper dive into the detailed error model