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.
- 1Xử lý lỗi trong .NET với Result Pattern
- 2Global Error Handling trong ASP.NET Core Web API
- 3Feature Flags trong .NET — Từ cơ bản đến Production
- 45 Architecture Tests bạn nên thêm vào dự án .NET
Đă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.
