671 từ
3 phút
Error Handling in .NET With the Result Pattern

Tại sao không nên lạm dụng exception?#

Mỗi lần throw, CLR phải thu thập stack trace — tốn tài nguyên và chậm. Với những lỗi bạn biết trước sẽ xảy ra (validation fail, not found, unauthorized), dùng exception là quá tay.

Chia lỗi thành hai nhóm:

  1. Lỗi kiểm soát được — validation, not found, conflict → xử lý qua Result object
  2. Exception thật sự — database die, network timeout → để exception middleware bắt

Result Pattern cho phép bạn gói trạng thái thành công/thất bại vào một object type-safe, không cần throw gì cả.

Result Pattern hoạt động thế nào?#

Thay vì throw exception, method trả về một Result object chứa:

  • Thành công: giá trị kết quả
  • Thất bại: loại lỗi + message mô tả

Implement từng bước#

1. ErrorType Enum#

Phân loại lỗi theo HTTP semantics. Thêm loại mới nếu app của bạn cần.

public enum ErrorType
{
    Validation,
    NotFound,
    Unauthorized,
    Conflict,
    Unknown
}

2. Error Class#

Giữ đơn giản với Type + Message. Trong production, bạn có thể thêm TraceId, CallerMethod, FilePath để phục vụ logging.

public class Error
{
    public ErrorType Type { get; }
    public string Message { get; }

    public Error(ErrorType type, string message)
    {
        Type = type;
        Message = message;
    }

    public override string ToString() => $"{Type}: {Message}";
}

3. Result Class (non-generic)#

Dùng khi chỉ cần biết thành công hay thất bại — ví dụ: Command không trả data.

public class Result
{
    public bool IsSuccess { get; }
    public Error? Error { get; }

    protected Result(bool isSuccess, Error? error)
    {
        IsSuccess = isSuccess;
        Error = error;
    }

    public static Result Success() => new Result(true, null);

    public static Result Failure(Error error) => new Result(false, error);
}

4. Result<T> (generic)#

Dùng khi cần trả data — ví dụ: Query lấy user từ database.

public class Result<T> : Result
{
    public T? Value { get; }

    private Result(T value) : base(true, null)
    {
        Value = value;
    }

    private Result(Error error) : base(false, error)
    {
        Value = default;
    }

    public static Result<T> Success(T value) => new Result<T>(value);

    public static Result<T> Failure(Error error) => new Result<T>(error);
}

5. Extension Methods — Match#

Pattern matching cho Result: xử lý cả hai nhánh success/failure một cách tường minh.

public static class ResultExtensions
{
    public static void Match(
        this Result result,
        Action onSuccess,
        Action<Error> onFailure)
    {
        if (result.IsSuccess)
            onSuccess();
        else
            onFailure(result.Error!);
    }

    public static void Match<T>(
        this Result<T> result,
        Action<T> onSuccess,
        Action<Error> onFailure)
    {
        if (result.IsSuccess)
            onSuccess(result.Value!);
        else
            onFailure(result.Error!);
    }
}

6. Bonus: Map Result → ActionResult#

Map ErrorType sang HTTP status code. Viết một lần, dùng ở mọi endpoint — không lặp code trong Controller.

public static class ApiResponseExtensions
{
    public static IActionResult ToActionResult(this Result result)
    {
        if (result.IsSuccess)
            return new OkResult();

        return new ObjectResult(result.Error)
        {
            StatusCode = result.Error!.Type switch
            {
                ErrorType.Validation => 400,
                ErrorType.NotFound => 404,
                ErrorType.Unauthorized => 401,
                _ => 500
            }
        };
    }

    public static IActionResult ToActionResult<T>(this Result<T> result)
    {
        if (result.IsSuccess)
            return new OkObjectResult(result.Value);

        return result.ToActionResult();
    }
}

Ví dụ thực tế: UserService + Controller#

Service trả Result<User>, Controller chỉ cần gọi .ToActionResult(). Kết hợp thêm Repository, CQRS hay MediatR đều được.

public class UserService
{
    public Result<User> GetUserById(int id)
    {
        if (id <= 0)
            return Result<User>.Failure(new Error(ErrorType.Validation, "ID người dùng không hợp lệ"));

        var user = GetUserFromDatabase(id);
        return user is not null
            ? Result<User>.Success(user)
            : Result<User>.Failure(new Error(ErrorType.NotFound, "Người dùng không tồn tại"));
    }

    private User? GetUserFromDatabase(int id)
    {
        // Giả lập lấy dữ liệu từ database
        return id == 1 ? new User { Id = 1, Name = "John Doe" } : null;
    }
}

// Ví dụ trong Controller
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
    private readonly UserService _userService = new();

    [HttpGet("{id}")]
    public IActionResult GetUser(int id)
    {
        var result = _userService.GetUserById(id);
        return result.ToActionResult();
    }
}

Tóm lại#

  • Exception dành cho lỗi bất ngờ (database die, network fail) — để global middleware xử lý.
  • Result Pattern dành cho lỗi kiểm soát được (validation, not found) — type-safe, không tốn stack trace.
  • Code rõ ràng hơn: caller buộc phải xử lý cả success lẫn failure, không “quên” catch.

Bạn có thể mở rộng thêm: dùng record thay class cho Error, thêm FluentValidation kết hợp, hoặc dùng library như FluentResults nếu không muốn tự build.

Error Handling in .NET With the Result Pattern
https://www.devwithxuan.com/vi/posts/dotnet-error-handling/
Tác giả
XuanPD
Ngày đăng
2024-05-01
Giấy phép
CC BY-NC-SA 4.0
Chia sẻ:

Đăng ký nhận bản tin

Nhận thông báo khi có bài viết mới. Không spam, hủy bất cứ lúc nào.

Bình luận