Quick Start Guide - ulfbou/Zentient.Results GitHub Wiki

🚀 Quick Start Guide

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.


⬇️ 1. Installation

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.


💡 2. Core Concepts at a Glance

Before diving into code, here are the fundamental ideas:

  • Result<T>: Represents the outcome of an operation that produces a value of type T. It can be either a success (containing T) 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.


➕ 3. Basic Usage: Creating Results

Let's look at how to create instances of Result<T> for both success and failure scenarios.

Creating a Success Result

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.")
        );
    }
}

Creating a Failure Result

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; } }

Creating a Non-Generic Result (for void operations)

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
    }
}

✔️ 4. Basic Usage: Consuming Results

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}");
        }
    }
}

🔗 5. Chaining Operations (Fluent API Introduction)

Zentient.Results provides a fluent API to chain operations, making your code concise and robust.

Map: Transform the Value on Success

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: Chain Operations that Also Return a Result

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}");
        }
    }
}

➡️ 6. Next Steps

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:

For the full source code and README, visit the Zentient.Results GitHub Repository.

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