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:
- Lỗi kiểm soát được — validation, not found, conflict → xử lý qua Result object
- 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.
Đă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.
