Lời mở đầu
Xử lý lỗi là một phần không thể thiếu trong quá trình phát triển phần mềm. Trong một bài viết trước, mình đã giới thiệu cách xử lý ngoại lệ (exception) trong ứng dụng .NET. Mặc dù sử dụng custom exception kế thừa từ lớp Exception
là một cách mạnh mẽ để quản lý lỗi,tuy nhiên việc throw exception là một thao tác tốn tài nguyên do cần thu thập stack trace, điều này có thể ảnh hưởng đến hiệu năng của chương trình ở một khía cạnh nào đó.
Để giải quyết vấn đề này, chúng ta có thể áp dụng một cách tiếp cận khác bằng cách chia lỗi thành hai nhóm:
- Lỗi có thể quản lý: Những lỗi chúng ta xử lý rõ ràng và trả kết quả trong logic chương trình.
- Ngoại lệ không kiểm soát được: Những vấn đề không mong đợi, như lỗi kết nối cơ sở dữ liệu, cần sử dụng ngoại lệ.
Với cách tiếp cận này, chúng ta sẽ quản lý lỗi và ngoại lệ một cách độc lập, cải thiện hiệu năng bằng cách giảm thiểu việc throw exception không cần thiết. Result Pattern ra đời để giúp chúng ta thực hiện điều đó. Hãy cùng tìm hiểu cách triển khai pattern này nhé!!!
Result Pattern là gì?
Result Pattern là một cách tiếp cận thiết kế, gói gọn trạng thái thành công và thất bại của một thao tác vào một đối tượng duy nhất và an toàn kiểu dữ liệu. Thay vì sử dụng ngoại lệ để truyền lỗi, pattern này trả về một đối tượng Result
để chỉ rõ kết quả của một thao tác.
Một đối tượng Result
thường chứa:
- Giá trị (cho thao tác thành công).
- Loại lỗi hoặc thông báo lỗi (cho thao tác thất bại).
Các bước để implement Result Pattern trong ứng dụng của chúng ta
Có một vài cách để chúng ta implement Result Pattern trong ứng dụng .NET, tuy nhiên các cách này cơ bản đều thực hiện theo các bước sau. Cùng đọc tiếp xem các bước để implement Result Pattern là gì nhé. Đừng quyên hãy mở Editor lên và thử nó sau khi đọc để bạn có thể ứng dụng và mở rộng cách tiếp cận cho ứng dụng của mình nhé!!!
- Tạo ErrorType Enum
Enum này sẽ đại diện cho các loại lỗi khác nhau trong ứng dụng. Cá nhân mình thấy các lỗi chương trình có thể xếp vào 5 loại này. Nếu các bạn muốn custom thêm lỗi nào đó đặc biệt, và thường sử dụng cho ứng dụng của mình thì hãy bổ sung thêm vào Enum này.
public enum ErrorType
{
Validation,
NotFound,
Unauthorized,
Conflict,
Unknown
}
- Tạo Error Class
Class này chứa thông tin về lỗi, như loại và thông báo. Có nhiều cách để triển khai class này, có thể là một Abstract class chứa các thông tin chi tiết về lỗi mà bạn muốn expose ra. Có thể sử dụng Record để implement. Tuy nhiên ở bài viết này mình sẽ sử dụng class thường, và chỉ có hai trường Type và Message để các bạn dễ hình dung thôi nhé.(Trong dự án thực tế mình có trả thêm một số thông tin như TraceId, và một số thông tin như LineNumber, CallerMethod, FilePath, Source để phục vụ cho 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}";
}
- Tạo và xử lý logic trong Result Class
Class này đại diện cho kết quả của một thao tác, bao gồm thông tin thành công hoặc thất bại. Class này thường được dùng cho các action mà bạn chỉ muốn lấy giá trị thực hiện là thành công hay thất bại, mà không cần quan tâm đến giá trị trả ra khi thành công. Ví dụ như các Command chỉ cần quan tâm trạng thái là execute thành công hay thất bại.
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);
}
- Tạo và xử lý Result Class Generic
Phiên bản generic của Result cho phép trả giá trị khi thành công. Với phiên bản này, khi bạn cần quan tâm đến giá trị trả ra như là Query dữ liệu từ database, thì hãy dùng với Generic class nhé.
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);
}
- Tạo Extention Methods để xử lý Result Matching
Các phương thức mở rộng giúp xử lý Result ngắn gọn, liền mạch và nhất quán hơn.
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!);
}
}
- Bonus: Tạo Extention Methods để xử lý Error response nhất quán
Với các ứng dụng .NET WebApi chúng ta thường sẽ trả ra các ActionResult. Và việc đảm bảo các mã lỗi, cũng như response nhất quán ở tất cả các endpoint là quan trọng. Chúng ta sẽ tạo một Extention Method để xử lý việc này, đồng thời cũng tránh việc phải viết đi viết lại nhiều lần trong từng endpoint bên 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ụ sử dụng
Bên dưới là một đoạn code sample để các bạn hình dung được các sử dụng Result Pattern. Ở đây chỉ đơn giản là có UserService và Api sẽ thực hiện lấy dữ liệu thông qua UserService. Các bạn hoàn toàn có thể mở rộng với ứng dụng của mình có thể sử dụng Repository, hay CQRS, … chúng đều có thể áp dụng thêm Result Pattern nhé.
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();
}
}
Kết luận
Result Pattern mang lại một cách tiếp cận rõ ràng và có cấu trúc để xử lý lỗi trong ứng dụng .NET, giảm sự phụ thuộc vào ngoại lệ cho các lỗi có thể kiểm soát được. Cách tiếp cận này không chỉ cải thiện hiệu năng mà còn giúp phân biệt rõ ràng giữa lỗi có thể phục hồi và các ngoại lệ bất ngờ. Hy vọng qua bài viết này, bạn có thể dễ dàng tích hợp pattern này vào ứng dụng của mình để xử lý lỗi hiệu quả hơn.
Thank for reading!!!