Quick Start Guide - ulfbou/Zentient.Results GitHub Wiki
Welcome to the Zentient.Results Quick Start Guide! This page will help you get up and running with the library in minutes, covering the essential steps for installation and basic usage.
First, add Zentient.Results to your .NET project. You can do this via the NuGet Package Manager Console or the .NET CLI:
dotnet add package Zentient.Results
Zentient.Results is compatible with .NET 6+ and later versions, including .NET 9.
Before diving into code, here are the fundamental ideas:
-
Result<T>
: Represents the outcome of an operation that produces a value of typeT
. It can be either a success (containingT
) or a failure (containing error information). -
Result
(Non-Generic): Represents the outcome of an operation that doesn't produce a specific value (e.g., a void method). It's purely about success or failure. -
ErrorInfo
: A rich, structured object that provides details about why an operation failed (e.g., error code, message, category, additional data). -
IResultStatus
: Defines the high-level status of an operation, often aligning with HTTP status codes (e.g., 200 OK, 400 Bad Request).ResultStatuses
provides common predefined statuses.
The core idea is to return results, not throw exceptions, for expected business outcomes.
Let's look at how to create instances of Result<T>
for both success and failure scenarios.
When an operation completes successfully and produces a value:
using Zentient.Results;
using System.Linq; // For .FirstOrDefault()
public class UserService
{
public IResult<string> GetUserName(int userId)
{
// Simulate successful retrieval
if (userId == 1)
{
return Result<string>.Success("Alice", "User retrieved successfully."); // Simple success with optional message
}
// Using a dedicated factory method for NotFound
return Result<string>.NotFound(
ErrorInfo.NotFound("UserNotFound", "User not found.")
);
}
}
When an operation fails, you use a Failure
factory method or a more specific convenience method (like BadRequest
, NotFound
), providing ErrorInfo
.
using Zentient.Results;
using System.Linq; // For .FirstOrDefault()
public class ProductService
{
public IResult<Product> GetProduct(string productId)
{
if (string.IsNullOrWhiteSpace(productId))
{
return Result<Product>.BadRequest( // Convenience method for 400
ErrorInfo.Validation("InvalidProductId", "Product ID cannot be empty.")
);
}
// Simulate not found scenario
if (productId == "NON_EXISTENT_ID")
{
return Result<Product>.NotFound( // Convenience method for 404
ErrorInfo.NotFound("ProductNotFound", $"Product {productId} not found.")
);
}
// ... (simulate success path)
return Result<Product>.Success(new Product { Id = productId, Name = "Example Product" });
}
}
public class Product { public string Id { get; set; } public string Name { get; set; } }
For operations that just indicate success or failure without returning a value:
using Zentient.Results;
using System;
using System.Linq; // For .FirstOrDefault()
public class EmailService
{
public IResult SendWelcomeEmail(string emailAddress)
{
if (!emailAddress.Contains("@"))
{
return Result.BadRequest( // Convenience method for 400
ErrorInfo.Validation("InvalidEmail", "Email address format is invalid.")
);
}
// Simulate sending email
Console.WriteLine($"Sending welcome email to {emailAddress}...");
return Result.Success("Email sent successfully."); // Non-generic success with optional message
}
}
Once you have a Result
object, you can check its state and access its contents.
using Zentient.Results;
using System;
using System.Linq; // For .FirstOrDefault()
public class ApplicationFlow
{
public void ProcessUserRequest()
{
var userService = new UserService(); // From previous example
// Scenario 1: Successful operation
IResult<string> successResult = userService.GetUserName(1);
if (successResult.IsSuccess)
{
string userName = successResult.Value; // Access the value
Console.WriteLine($"Successfully retrieved user: {userName}");
Console.WriteLine($"Success message: {successResult.Messages.FirstOrDefault()}");
}
else
{
// This block won't be hit for successResult
Console.WriteLine($"Operation failed: {successResult.ErrorMessage}"); // Access the first error message
}
// Scenario 2: Failed operation
IResult<string> failureResult = userService.GetUserName(99); // Simulates user not found
if (failureResult.IsFailure)
{
Console.WriteLine($"Operation failed with status {failureResult.Status.Code}: {failureResult.ErrorMessage}");
// Access all errors:
foreach (var error in failureResult.Errors)
{
Console.WriteLine($"- Error ({error.Category}/{error.Code}): {error.Message}. Detail: {error.Detail}");
}
}
else
{
// This block won't be hit for failureResult
Console.WriteLine($"Operation succeeded: {failureResult.Value}");
}
}
}
Zentient.Results provides a fluent API to chain operations, making your code concise and robust.
Map
allows you to transform the successful value of a Result<T>
into a Result<U>
without changing the success/failure state.
using Zentient.Results;
using System;
using System.Linq; // For .FirstOrDefault()
// Assume UserService.GetUserName(int) from above or define a new one for clarity
public class ExampleUserService
{
public IResult<string> GetUserName(int userId)
{
if (userId == 1) return Result<string>.Success("Alice");
if (userId == 2) return Result<string>.Success("BobTheBuilder"); // Longer name for example
return Result<string>.NotFound(ErrorInfo.NotFound("UserNotFound", "User not found."));
}
}
public class ReportGenerator
{
public IResult<int> GetUserNameLength(int userId)
{
return new ExampleUserService().GetUserName(userId)
.Map(userName => userName.Length); // Map string to int (length) if successful
}
public void Run()
{
var lengthResult = GetUserNameLength(1);
if (lengthResult.IsSuccess)
{
Console.WriteLine($"User name length: {lengthResult.Value}"); // Output: User name length: 5
}
var failedLengthResult = GetUserNameLength(0); // This would return a failure from GetUserName
if (failedLengthResult.IsFailure)
{
Console.WriteLine($"Could not get user name length: {failedLengthResult.ErrorMessage}");
}
}
}
Bind
allows you to chain a new operation that also returns a Result
. This is crucial for sequential operations where each step can fail.
using Zentient.Results;
using System;
using System.Linq; // For .FirstOrDefault()
// Assume ExampleUserService.GetUserName(int) from above
public class ExampleUserService
{
public IResult<string> GetUserName(int userId)
{
if (userId == 1) return Result<string>.Success("Alice");
if (userId == 2) return Result<string>.Success("BobTheBuilder"); // Longer name for example
return Result<string>.NotFound(ErrorInfo.NotFound("UserNotFound", "User not found."));
}
}
public class ValidationService
{
public IResult<string> ValidateName(string name)
{
if (name.Length > 10)
{
return Result<string>.BadRequest(
ErrorInfo.Validation("NameTooLong", "Name exceeds 10 characters.", detail: $"Length was {name.Length}.")
);
}
return Result<string>.Success(name);
}
}
public class ProfileProcessor
{
public IResult<string> ProcessUser(int userId)
{
var userService = new ExampleUserService();
var validationService = new ValidationService();
return userService.GetUserName(userId)
.Bind(userName => validationService.ValidateName(userName)); // Only proceeds if GetUserName was successful
}
public void Run()
{
var result1 = ProcessUser(1); // User "Alice" (length 5)
if (result1.IsSuccess)
{
Console.WriteLine($"Processed user: {result1.Value}"); // Output: Processed user: Alice
}
var result2 = ProcessUser(99); // Simulates user not found from GetUserName
if (result2.IsFailure)
{
Console.WriteLine($"Failed to process user: {result2.ErrorMessage}"); // Output: Failed to process user: User not found.
}
var result3 = ProcessUser(2); // User "BobTheBuilder" (length 13) -> will fail validation
if (result3.IsFailure)
{
Console.WriteLine($"Failed to process user (validation): {result3.ErrorMessage}");
Console.WriteLine($"Validation Detail: {result3.Errors.FirstOrDefault()?.Detail}");
}
}
}
You've now learned the absolute basics of Zentient.Results! To explore more advanced features and deeper architectural insights, please refer to these wiki pages:
- Basic Usage Patterns: More examples and common scenarios for creating and consuming results.
-
Structured Error Handling with
ErrorInfo
: A detailed dive into the error model. -
Chaining and Composing Operations: Advanced usage of
Map
,Bind
,Then
,OnSuccess
,OnFailure
, andMatch
. - Integrating with ASP.NET Core: Guidance on using Zentient.Results in web applications.
- Architecture Overview: Understand the library's design principles.
- Contributing: Learn how to contribute to the project.
For the full source code and README, visit the Zentient.Results GitHub Repository.