Skip to content

Full Example

ferdi kurnaz edited this page Aug 27, 2026 · 1 revision

Full Example

This page walks through a complete, realistic example, based on the Resultron.Sample console app included in the repository. It's a simple "user management" system: creating, fetching, updating, and deleting users.

The pieces

1. The model

public record User(Guid Id, string Name, string Email, DateTime CreatedAt);

2. The errors

public static class UserErrors
{
    public static Error NotFound(Guid id) =>
        new("User.NotFound", $"User with id '{id}' was not found.");

    public static Error EmailAlreadyExists(string email) =>
        new("User.EmailAlreadyExists", $"A user with email '{email}' already exists.");

    public static Error InvalidName =>
        new("User.InvalidName", "Name must be at least 2 characters.");

    public static Error InvalidEmail =>
        new("User.InvalidEmail", "Email must contain '@'.");
}

Notice how each error has a clear Code (like "User.NotFound") and a friendly Description. This makes debugging and logging much easier.

3. The repository interface

public interface IUserRepository
{
    Result<User> GetById(Guid id);
    Result<List<User>> GetAll();
    Result<User> Add(User user);
    Result<User> Update(User user);
    Result Delete(Guid id);
    bool EmailExists(string email);
}

Notice that almost every method returns a Result or Result<T> — this keeps the "success or failure" idea consistent throughout the whole app.

4. The service (business logic)

public sealed class UserService
{
    private readonly IUserRepository _repository;

    public UserService(IUserRepository repository) => _repository = repository;

    public Result<User> Create(string name, string email)
    {
        return ValidateName(name)
            .Bind(() => ValidateEmail(email))
            .Bind(() => CheckEmailUniqueness(email))
            .Bind(() => SaveUser(name, email));
    }

    public Result<User> GetById(Guid id) => _repository.GetById(id);

    public Result<List<User>> GetAll() => _repository.GetAll();

    public Result<User> UpdateName(Guid id, string newName)
    {
        return ValidateName(newName)
            .Bind(() => _repository.GetById(id))
            .Map(user => new User(user.Id, newName, user.Email, user.CreatedAt))
            .Bind(_repository.Update);
    }

    public Result Delete(Guid id)
    {
        return _repository.GetById(id)
            .Bind(user => _repository.Delete(user.Id));
    }

    private static Result ValidateName(string name) =>
        name.Length >= 2 ? Result.Success() : Result.Failure(UserErrors.InvalidName);

    private static Result ValidateEmail(string email) =>
        email.Contains('@') ? Result.Success() : Result.Failure(UserErrors.InvalidEmail);

    private Result CheckEmailUniqueness(string email) =>
        _repository.EmailExists(email)
            ? Result.Failure(UserErrors.EmailAlreadyExists(email))
            : Result.Success();

    private Result<User> SaveUser(string name, string email)
    {
        var user = new User(Guid.NewGuid(), name, email, DateTime.UtcNow);
        return _repository.Add(user);
    }
}

5. Using the service

var repository = new InMemoryUserRepository();
var service = new UserService(repository);

// Successful creation
var createResult = service.Create("Arda Terekeci", "arda@example.com");
createResult.Match(
    onSuccess: user => Console.WriteLine($"[OK] User created: {user.Id} - {user.Name}"),
    onFailure: error => Console.WriteLine($"[ERROR] {error.Code}: {error.Description}")
);

// Trying to create a user with a duplicate email
var duplicateResult = service.Create("Someone Else", "arda@example.com");
duplicateResult.Match(
    onSuccess: user => Console.WriteLine($"[OK] {user.Name}"),
    onFailure: error => Console.WriteLine($"[ERROR] {error.Code}: {error.Description}")
);
// Output: [ERROR] User.EmailAlreadyExists: A user with email 'arda@example.com' already exists.

// Trying to create a user with an invalid name
var invalidResult = service.Create("A", "a@example.com");
invalidResult.Match(
    onSuccess: user => Console.WriteLine($"[OK] {user.Name}"),
    onFailure: error => Console.WriteLine($"[ERROR] {error.Code}: {error.Description}")
);
// Output: [ERROR] User.InvalidName: Name must be at least 2 characters.

What this example teaches

  1. Validation as small, reusable steps (ValidateName, ValidateEmail) that each return a Result.
  2. Chaining with Bind so the whole "Create" operation reads top-to-bottom, like a checklist.
  3. Map used for the simple, "cannot fail" transformation of building an updated User object.
  4. Consistent error codes (User.InvalidName, User.NotFound, etc.) that make it easy to know exactly what happened, and could be mapped to specific HTTP status codes in a web API (for example, NotFound → HTTP 404, InvalidName/InvalidEmail → HTTP 400).
  5. Match used at the very end, as the single place where the app decides what to print (or, in a real app, what HTTP response to send).

The full sample project is available in the repository at sample/Resultron.Sample.

Next: check the FAQ for common questions.

Clone this wiki locally