Basic Usage Patterns - ulfbou/Zentient.Results GitHub Wiki
This guide expands on the "Quick Start Guide" by diving deeper into common usage patterns for Zentient.Results
. You'll find more detailed examples of how to create, consume, and manage result objects in various scenarios, making your code robust and expressive.
The fundamental pattern is to have your methods return an IResult<T>
(or IResult
for void operations) instead of T
or throwing exceptions.
This is the most common scenario: an operation that might succeed or fail based on business rules or external factors.
using Zentient.Results;
using System;
using System.Text.RegularExpressions;
public class UserRegistrationService
{
// Simulates a user data store
private static List<string> _registeredUsernames = new List<string> { "alice", "bob" };
public IResult<User> RegisterUser(string username, string password)
{
// 1. Input Validation
if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
{
return Result<User>.BadRequest( // Convenient static method for 400
new ErrorInfo(ErrorCategory.Validation, "InvalidUsername", "Username must be at least 3 characters and not empty.")
);
}
if (!Regex.IsMatch(password, @"^(?=.*[A-Za-z])(?=.*d)[A-Za-zd]{8,}$"))
{
return Result<User>.BadRequest(
new ErrorInfo(ErrorCategory.Validation, "WeakPassword", "Password must be at least 8 characters, contain letters and numbers.")
);
}
// 2. Business Rule Validation (e.g., uniqueness)
if (_registeredUsernames.Contains(username.ToLower()))
{
return Result<User>.Conflict( // Convenient static method for 409
new ErrorInfo(ErrorCategory.Conflict, "UsernameTaken", $"Username '{username}' is already taken.")
);
}
// 3. Simulate Successful Registration
var newUser = new User { Id = Guid.NewGuid(), Username = username };
_registeredUsernames.Add(username.ToLower());
Console.WriteLine($"User '{username}' registered successfully.");
return Result<User>.Created(newUser); // Convenient static method for 201
}
}
public class User { public Guid Id { get; set; } public string Username { get; set; } }
After a method returns a Result
, you need to consume it to determine the next steps.
The most straightforward way to handle success or failure.
using Zentient.Results;
public class ClientApplication
{
public void RunRegistrationFlow()
{
var service = new UserRegistrationService();
// Scenario 1: Successful Registration
IResult<User> successResult = service.RegisterUser("charlie", "SecureP@ss123");
if (successResult.IsSuccess)
{
User registeredUser = successResult.Value; // Access the value
Console.WriteLine($"Registration successful for user: {registeredUser.Username} (ID: {registeredUser.Id})");
}
else
{
Console.WriteLine($"Registration failed (Status: {successResult.Status.Code}): {successResult.Error}");
}
// Scenario 2: Validation Failure
IResult<User> validationFailureResult = service.RegisterUser("jo", "pass"); // Too short username, weak password
if (validationFailureResult.IsFailure)
{
Console.WriteLine($"Validation failed (Status: {validationFailureResult.Status.Code}): {validationFailureResult.Error}");
foreach (var error in validationFailureResult.Errors)
{
Console.WriteLine($"- Error ({error.Code}): {error.Message}");
}
}
// Scenario 3: Conflict Failure
IResult<User> conflictFailureResult = service.RegisterUser("charlie", "AnotherP@ss123"); // Charlie already registered
if (conflictFailureResult.IsFailure)
{
Console.WriteLine($"Conflict failed (Status: {conflictFailureResult.Status.Code}): {conflictFailureResult.Error}");
}
}
}
-
result.Value
: Only access ifresult.IsSuccess
istrue
. -
result.Error
: Provides the first error message ifIsFailure
istrue
. -
result.Errors
: Provides anIReadOnlyList<ErrorInfo>
of all errors.
Provides a default value if the result is a failure, preventing null reference exceptions.
public class DataConsumer
{
public string GetUserNameOrDefault(int userId)
{
var userService = new UserService(); // Assume GetUserName from Quick Start Guide
IResult<string> userNameResult = userService.GetUserName(userId);
// If success, returns the user name. If failure, returns "Guest".
return userNameResult.GetValueOrDefault("Guest");
}
}
ThrowIfFailure()
is an extension method that will throw a ResultException
if the result is a failure. Use this when you genuinely want to stop execution if an operation fails, often at the boundaries of your application (e.g., API controllers after all business logic has run) or in CLI tools where unhandled exceptions are acceptable for critical failures.
using Zentient.Results;
using System;
public class CriticalOperation
{
public IResult<ReportData> GenerateCriticalReport(int input)
{
if (input < 0)
{
return Result<ReportData>.BadRequest(new ErrorInfo(ErrorCategory.Validation, "InvalidInput", "Input cannot be negative."));
}
// Simulate complex report generation
return Result<ReportData>.Success(new ReportData { Id = input });
}
public void ExecuteReportGeneration(int input)
{
try
{
// Use ThrowIfFailure() at the boundary
ReportData data = GenerateCriticalReport(input).ThrowIfFailure().Value;
Console.WriteLine($"Report {data.Id} generated successfully.");
}
catch (ResultException ex)
{
Console.Error.WriteLine($"Critical report generation failed: {ex.Message}");
foreach (var error in ex.Errors)
{
Console.Error.WriteLine($"- Error ({error.Code}): {error.Message}");
}
}
catch (Exception ex)
{
// Catch other unexpected exceptions
Console.Error.WriteLine($"An unexpected error occurred: {ex.Message}");
}
}
}
public class ReportData { public int Id { get; set; } }
The Match
method provides a clean way to handle both success and failure branches, similar to pattern matching. It ensures you've considered both possibilities.
using Zentient.Results;
using System;
public class ProductFetcher
{
public IResult<Product> GetProductById(string id)
{
if (id == "123") return Result<Product>.Success(new Product { Id = id, Name = "Laptop" });
if (id == "404") return Result<Product>.NotFound(new ErrorInfo(ErrorCategory.NotFound, "ProductNotFound", "Product was not found."));
return Result<Product>.BadRequest(new ErrorInfo(ErrorCategory.Validation, "InvalidId", "Invalid product ID format."));
}
public void DisplayProductDetails(string productId)
{
GetProductById(productId).Match(
onSuccess: product =>
{
Console.WriteLine($"Product found: {product.Name} (ID: {product.Id})");
},
onFailure: errors =>
{
// errors is an IReadOnlyList<ErrorInfo>
Console.WriteLine($"Failed to retrieve product '{productId}':");
foreach (var error in errors)
{
Console.WriteLine($"- {error.Message} (Code: {error.Code}, Category: {error.Category})");
// Can also check error.Status to get HTTP-aligned status
}
}
);
}
}
Use IResult
and Result
when an operation's primary outcome is just success or failure, without a specific return value.
using Zentient.Results;
using System;
public class DataDeleter
{
public IResult DeleteRecord(Guid recordId)
{
if (recordId == Guid.Empty)
{
return Result.BadRequest(
new ErrorInfo(ErrorCategory.Validation, "InvalidId", "Record ID cannot be empty.")
);
}
// Simulate successful deletion
if (recordId == new Guid("A0000000-0000-0000-0000-000000000001"))
{
Console.WriteLine($"Record {recordId} deleted successfully.");
return Result.Success(ResultStatuses.NoContent); // Common for deletions
}
// Simulate not found
return Result.NotFound(
new ErrorInfo(ErrorCategory.NotFound, "RecordNotFound", $"Record {recordId} not found.")
);
}
public void TryDeleteData()
{
IResult deleteResult = DeleteRecord(new Guid("A0000000-0000-0000-0000-000000000001"));
if (deleteResult.IsSuccess)
{
Console.WriteLine("Deletion operation completed successfully.");
}
else
{
Console.WriteLine($"Deletion failed: {deleteResult.Error} (Status: {deleteResult.Status.Code})");
}
IResult invalidIdResult = DeleteRecord(Guid.Empty);
if (invalidIdResult.IsFailure)
{
Console.WriteLine($"Invalid ID for deletion: {invalidIdResult.Error}");
}
}
}
Zentient.Results allows you to return multiple errors within a single Result
, particularly useful for input validation where many issues might be found at once.
using Zentient.Results;
using System.Collections.Generic;
public class RegistrationValidator
{
public IResult ValidateRegistrationRequest(RegistrationRequest request)
{
var errors = new List<ErrorInfo>();
if (string.IsNullOrWhiteSpace(request.Username) || request.Username.Length < 5)
{
errors.Add(new ErrorInfo(ErrorCategory.Validation, "UsernameLength", "Username must be at least 5 characters."));
}
if (string.IsNullOrWhiteSpace(request.Email) || !request.Email.Contains("@"))
{
errors.Add(new ErrorInfo(ErrorCategory.Validation, "InvalidEmail", "Email format is invalid."));
}
if (string.IsNullOrWhiteSpace(request.Password) || request.Password.Length < 8)
{
errors.Add(new ErrorInfo(ErrorCategory.Validation, "PasswordLength", "Password must be at least 8 characters."));
}
if (errors.Any()) // Use LINQ's Any()
{
// Create a failure result with all collected errors
return Result.Validation(errors); // Convenience method for 400 validation error
}
return Result.Success();
}
}
public class RegistrationRequest
{
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
The ErrorInfo.Data
property is an object?
that allows you to attach any additional contextual information relevant to the error. This is powerful for debugging or providing rich client-side error details.
using Zentient.Results;
using System.Collections.Generic;
public class FileProcessingService
{
public IResult ProcessFile(string fileName)
{
if (!System.IO.File.Exists(fileName))
{
return Result.NotFound(new ErrorInfo(
ErrorCategory.NotFound,
"FileNotFound",
$"File '{fileName}' was not found.",
new { RequestedPath = fileName, CurrentDirectory = Environment.CurrentDirectory } // Attach anonymous object as data
));
}
if (new System.IO.FileInfo(fileName).Length > 1024 * 1024) // 1MB
{
return Result.BadRequest(new ErrorInfo(
ErrorCategory.Validation,
"FileTooLarge",
$"File '{fileName}' exceeds maximum allowed size.",
new { MaxSizeMB = 1, ActualSizeKB = new System.IO.FileInfo(fileName).Length / 1024 } // Attach structured data
));
}
Console.WriteLine($"File '{fileName}' processed successfully.");
return Result.Success();
}
}
You've explored the core patterns for creating and consuming Zentient.Results
. To learn more about specific aspects and advanced techniques, proceed to:
-
Chaining and Composing Operations: Dive into
Map
,Bind
,Then
, andMatch
for building robust pipelines. - Integrating with ASP.NET Core: Learn how to use Zentient.Results in your web API controllers.
-
Structured Error Handling with
ErrorInfo
: A comprehensive guide to the error model.