ErrorOr для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

ErrorOr для C# (.NET): полное руководство по обработке ошибок без исключений

В современной разработке на C# (.NET) обработка ошибок традиционно строится на механизме исключений (exceptions). Однако у этого подхода есть ряд недостатков: исключения нарушают поток выполнения, создают накладные расходы на стек, и, что самое важное, не всегда явно сигнализируют разработчику о том, что метод может завершиться ошибкой. Библиотека ErrorOr предлагает альтернативу — функциональный подход, при котором результат операции возвращается в виде специального объекта, который может содержать либо успешное значение, либо список ошибок.

Основная идея ErrorOr заключается в том, чтобы сделать обработку ошибок явной и типобезопасной. Вместо того чтобы бросать исключение, метод возвращает объект типа ErrorOr<TValue>. Вызывающий код обязан проверить, успешно ли завершилась операция, прежде чем использовать результат. Это предотвращает множество багов, связанных с необработанными исключениями, и делает код более предсказуемым и легким для понимания.

Библиотека вдохновлена языками с развитой системой типов, такими как Rust (тип Result<T, E>) и F# (тип Result<'T, 'TError>). ErrorOr идеально подходит для Clean Architecture, Domain-Driven Design (DDD) и CQRS-проектов, где важно четко разделять успешные и ошибочные сценарии. Она также отлично работает с паттерном «Railway Oriented Programming», позволяя строить цепочки операций с автоматической передачей ошибок.

Установка

Установка библиотеки ErrorOr в проект .NET производится через менеджер пакетов NuGet. Выполните одну из следующих команд в терминале или консоли диспетчера пакетов:

dotnet add package ErrorOr

Или, если вы используете Package Manager Console в Visual Studio:

Install-Package ErrorOr

После установки подключите пространство имен в вашем файле с кодом:

using ErrorOr;

Быстрый старт: минимальный рабочий пример

Давайте напишем простую функцию, которая делит одно число на другое. Вместо того чтобы бросать исключение при делении на ноль, мы вернем ErrorOr<double>.

using ErrorOr;

public static class Calculator
{
    public static ErrorOr<double> Divide(double dividend, double divisor)
    {
        if (divisor == 0)
        {
            // Возвращаем ошибку, если делитель равен нулю
            return Error.Validation("Calculator.DivisionByZero", "Divisor cannot be zero.");
        }

        // Возвращаем успешный результат
        return dividend / divisor;
    }
}

// Использование
public class Program
{
    public static void Main()
    {
        ErrorOr<double> result = Calculator.Divide(10, 2);

        if (result.IsError)
        {
            Console.WriteLine($"Operation failed: {result.FirstError.Description}");
        }
        else
        {
            Console.WriteLine($"Result: {result.Value}");
        }

        // Пример с ошибкой
        ErrorOr<double> errorResult = Calculator.Divide(10, 0);
        if (errorResult.IsError)
        {
            Console.WriteLine($"Operation failed: {errorResult.FirstError.Description}");
        }
    }
}

В этом примере мы видим ключевые элементы: создание ошибки через Error.Validation(), возврат успешного значения (неявное преобразование), и проверку результата через свойство IsError.

Основные методы, функции и классы

Ниже приведен подробный разбор ключевых компонентов библиотеки ErrorOr. Для каждого элемента указана сигнатура, описание и рабочий пример кода.

1. Класс ErrorOr<TValue>

Сигнатура: public readonly struct ErrorOr<TValue>

Описание: Это основной тип, который представляет результат операции. Он может содержать либо успешное значение типа TValue, либо список ошибок типа List<Error>. Является структурой (struct), что минимизирует выделение памяти в куче.

Пример:

ErrorOr<string> successResult = "Hello, World!"; // Неявное преобразование
ErrorOr<string> errorResult = Error.Failure("MyError", "Something went wrong.");

2. Свойство IsError

Сигнатура: public bool IsError { get; }

Описание: Возвращает true, если объект содержит ошибки (т.е. операция завершилась неудачей). Это основной способ проверки состояния результата.

Пример:

ErrorOr<int> result = GetData();
if (result.IsError)
{
    // Обработка ошибки
    Console.WriteLine("Error occurred.");
}

3. Свойство Value

Сигнатура: public TValue Value { get; }

Описание: Возвращает успешное значение. Важно: Доступ к этому свойству безопасен только если IsError равно false. В противном случае будет выброшено исключение InvalidOperationException. Это сделано для того, чтобы вы явно обрабатывали ошибки.

Пример:

ErrorOr<User> userResult = FindUser(id);
if (!userResult.IsError)
{
    User user = userResult.Value; // Безопасно, так как мы проверили IsError
    Console.WriteLine(user.Name);
}

4. Свойство Errors

Сигнатура: public List<Error> Errors { get; }

Описание: Возвращает список всех ошибок, если операция завершилась неудачей. Если результат успешен, список будет пуст. Позволяет получить доступ ко всем ошибкам, если их несколько.

Пример:

ErrorOr<string> result = ValidateInput(data);
if (result.IsError)
{
    foreach (var error in result.Errors)
    {
        Console.WriteLine($"Code: {error.Code}, Description: {error.Description}");
    }
}

5. Свойство FirstError

Сигнатура: public Error FirstError { get; }

Описание: Возвращает первую ошибку из списка. Это удобно, когда вам нужно быстро получить основную причину сбоя, не перебирая весь список. Если ошибок нет, выбрасывает исключение.

Пример:

ErrorOr<int> result = ProcessOrder(order);
if (result.IsError)
{
    // Быстрый доступ к первой ошибке
    Console.WriteLine($"First error: {result.FirstError.Description}");
}

6. Метод Match

Сигнатура: public TOut Match<TOut>(Func<TValue, TOut> onValue, Func<List<Error>, TOut> onError)

Описание: Функциональный метод для обработки результата. Вы принимаете два делегата: один для случая успеха, другой для случая ошибки. Метод гарантирует, что будет вызван ровно один из них. Это элегантная альтернатива конструкции if-else.

Пример:

ErrorOr<string> result = FetchData();
string message = result.Match(
    onValue: value => $"Success: {value}",
    onError: errors => $"Error: {string.Join(", ", errors.Select(e => e.Description))}"
);
Console.WriteLine(message);

7. Метод Then (или Bind)

Сигнатура: public ErrorOr<TNext> Then<TNext>(Func<TValue, ErrorOr<TNext>> binder)

Описание: Позволяет строить цепочки операций. Если текущий результат успешен, вызывается функция binder, которая принимает значение и возвращает новый ErrorOr<TNext>. Если текущий результат содержит ошибку, binder не вызывается, и ошибка передается дальше по цепочке.

Пример:

ErrorOr<int> GetNumber() => 5;
ErrorOr<string> NumberToString(int num) => num.ToString();
ErrorOr<int> ParseString(string str) => int.Parse(str); // Может выбросить исключение, но для примера

ErrorOr<string> result = GetNumber()
    .Then(NumberToString); // result будет содержать "5"

// Цепочка с ошибкой
ErrorOr<int> failingGetNumber() => Error.Failure("NoNumber", "Could not get number");
ErrorOr<string> chainResult = failingGetNumber()
    .Then(NumberToString); // chainResult будет содержать ошибку от failingGetNumber

8. Класс Error и статические фабрики

Сигнатура: public sealed record Error с методами Error.Failure(...), Error.Unexpected(...), Error.Validation(...), Error.NotFound(...), Error.Forbidden(...), Error.Unauthorized(...), Error.Conflict(...), Error.Custom(...).

Описание: Класс Error представляет собой единичную ошибку. Он содержит свойства Code (строка-идентификатор), Description (человекочитаемое описание) и Type (тип ошибки, например, Failure, Validation). Статические фабрики предоставляют удобный способ создания ошибок разных типов.

Пример:

// Разные способы создания ошибок
Error failure = Error.Failure("User.SaveFailed", "Could not save user to database.");
Error validation = Error.Validation("User.NameEmpty", "User name cannot be empty.");
Error notFound = Error.NotFound("User.NotFound", $"User with ID {id} not found.");
Error custom = Error.Custom(42, "MyCustomType", "My.Custom.Code", "A custom error.");

9. Неявное преобразование (Implicit Conversion)

Сигнатура: (Неявно определено для типа ErrorOr<TValue>)

Описание: ErrorOr<TValue> поддерживает неявное преобразование из TValue и из Error. Это значит, что вы можете просто вернуть значение или ошибку из метода, не оборачивая их явно в ErrorOr.

Пример:

public ErrorOr<string> GetGreeting(bool isMorning)
{
    if (isMorning)
    {
        return "Good morning!"; // Неявное преобразование из string в ErrorOr<string>
    }
    return Error.NotFound("Greeting.NotFound", "No greeting for this time."); // Неявное преобразование из Error
}

10. Метод Switch

Сигнатура: public void Switch(Action<TValue> onValue, Action<List<Error>> onError)

Описание: Аналог Match, но не возвращает значение, а выполняет действия (void-методы). Подходит для сценариев, где нужно выполнить побочные эффекты (логирование, запись в БД).

Пример:

ErrorOr<string> result = GetData();
result.Switch(
    onValue: value => Console.WriteLine($"Processing: {value}"),
    onError: errors => Console.WriteLine($"Errors: {errors.Count}")
);

Реальный практический пример (100-200 строк кода)

Рассмотрим пример из реального приложения — сервис для регистрации пользователя. Мы создадим доменный сервис, который проверяет входные данные, проверяет уникальность email и сохраняет пользователя. Все ошибки будут возвращаться через ErrorOr.

using ErrorOr;

// --- Доменные модели ---
public class User
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public string Email { get; private set; }

    public User(string name, string email)
    {
        Id = Guid.NewGuid();
        Name = name;
        Email = email;
    }
}

// --- Репозиторий (имитация) ---
public interface IUserRepository
{
    bool IsEmailUnique(string email);
    void Add(User user);
}

public class UserRepository : IUserRepository
{
    private readonly List<User> _users = new();

    public bool IsEmailUnique(string email)
    {
        return !_users.Any(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase));
    }

    public void Add(User user)
    {
        _users.Add(user);
        Console.WriteLine($"User {user.Name} added to database.");
    }
}

// --- Сервис ---
public class UserService
{
    private readonly IUserRepository _repository;

    public UserService(IUserRepository repository)
    {
        _repository = repository;
    }

    public ErrorOr<User> RegisterUser(string name, string email)
    {
        // 1. Валидация входных данных
        if (string.IsNullOrWhiteSpace(name))
        {
            return Error.Validation("User.NameRequired", "Name is required.");
        }

        if (string.IsNullOrWhiteSpace(email))
        {
            return Error.Validation("User.EmailRequired", "Email is required.");
        }

        if (!email.Contains('@'))
        {
            return Error.Validation("User.InvalidEmail", "Email must contain '@'.");
        }

        // 2. Проверка уникальности email
        if (!_repository.IsEmailUnique(email))
        {
            return Error.Conflict("User.EmailNotUnique",                    

Рекомендаций