Để giữ cho controller trở nên clean và gọn gàng, chúng ta cần phải triển khai từ đầu và đã quen khi làm việc với mô hình MVC. Nhưng khi dự án phát triển và các thành viên khác trong nhóm tham gia dự án, mọi thứ có thể vượt quá tầm tay và khó kiểm soát. Và đây là một số tips giới thiệu đến các bạn với 10 điều cần tránh khi làm việc với controller trong .NET Core.
Để giúp bạn tổ chức các controller trong .NET Core Web API của mình tốt hơn, sau đây mình giới thiệu hàng loạt kỹ thuật đơn giản với các ví dụ giúp giữ cho controller của bạn trở nên hiệu quả hơn trong thời gian dài.
Data Access Logic
Chúng ta không nên sử dụng controller để truy cập dữ liệu trực tiếp. Mặc dù đây là quy tắc chung, nhưng không phải tất cả các dự án đều cần quá nhiều layers và một số có lẽ tốt nhất nên đơn giản.
Đối với các dự án khác, đặc biệt là những dự án lớn hơn, chúng ta không nên sử dụng data access logic trong controller. Hầu hết một phương thức truy cập dữ liệu trở thành hai, và sau đó chúng ta cần thêm một phương thức nữa… Và sau một vài tháng, chúng ta đã hoàn toàn lộn xộn bên trong controller đến mức chúng ta thậm chí không biết chuyện gì đang xảy ra.
Trong trường hợp này, repository pattern là một cách tuyệt vời để che dấu data access logic và tạo một layer riêng cho nó, tuy nhiên, chúng ta không nên sử dụng trực tiếp nó trong controller:
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _repository.GetProduct(id);
return Ok(product);
}
Business Logic
Giả sử chúng ta cần trả lại giá trị product mới, cụ thể là cần tính giá sản phẩm sau khi đã áp dụng discount:
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _repository.GetProduct(id);
ApplyTheDiscount(product);
return Ok(product);
}
Như các bạn thấy, code trên đã đáp ứng được bài toán là trả về danh sách sản phẩm sau khi đã áp dụng discount, nhưng, cách làm này không sai nhưng nó khó duy trì trong thời gian dài, các nghiệp vụ sẽ luôn thay đổi theo thời gian. Không chỉ vậy, mà controller của chúng ta đã mất đi mục đích, thay vì làm điều gì đó như thế này, chúng ta có thể tạo một lớp service và sử dụng nó để lưu trữ tất cả logic nghiệp vụ của chúng ta trong đó, cùng với data access logic.
public ProductController(IServiceManager service)
{
_service = service;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _service.GetProductAndApplyDiscount(id);
return Ok(product);
}
Repository Pattern là một mẫu kiến trúc cho phép chúng ta tách biệt các tầng khác nhau của ứng dụng, giúp cho mã nguồn trở nên trong sáng và dễ duy trì và mở rộng hơn. Các tầng trong repository pattern bao gồm:
- Tầng controller: Xử lý request và response của HTTP
- Tầng service: Xử lý các logic nghiệp vụ
- Tầng repository: Xử lý các thao tác truy xuất CSDL
Mapping Model Classes
Như code ở tầng nghiệp vụ ở trên, các bạn có thể thấy chúng ta đã trả về cả một entity, nhưng thực tế chúng ta chỉ nên trả về những trường cần thiết. Ví dụ, bạn có thực thể Quiz để làm chức năng trắc nghiệp, bên trong thực thể chứa cả câu hỏi, lựa chọn và đáp án. Vậy khi trả về câu hỏi cho người dùng, có cần thiết để trả về trường đáp án trong trường hợp này.
Cụ thể, bạn cần cần một class DTO (Data Transfer Object), và trả về những trường bạn muốn:
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _service.GetProductAndApplyDiscount(id);
var productDto = new ProductDto
{
Name = Name,
Details = Details,
Price = Price
};
return Ok(productDto);
}
Nhưng như bạn có thể thấy, chúng ta đang thực hiện ánh xạ từ thực thể sang DTO theo cách thủ công. Điều này không chỉ gây mệt mỏi và lặp đi lặp lại mà còn khiến mã thực sự không thể đọc được. Đối với các thực thể có quá nhiều trường, việc ánh xạ từng trường mất rất nhiều thời gian.
Ở đây chúng ta có một cách tiếp cận khác, đó là sử dụng thư viện ánh xạ như AutoMapper để làm cho quá trình này dễ dàng hơn.
Bằng cách sử dụng AutoMapper, chúng ta có thể dễ dàng thực hiện tương tự:
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _service.GetProductAndApplyDiscount(id);
var productDto = _mapper.Map<ProductDto>(product);
return Ok(productDto);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var productDto = await _service.GetProductAndApplyDiscount(id);
return Ok(productDto);
}
Exception Handling
Xử lý ngoại lệ là điều cần thiết trong bất kỳ ứng dụng nào. Nhưng liệu controller có phải là nơi để làm điều đó? Câu trả lời ngắn gọn là không.
.NET Core cung cấp một cách tuyệt vời để xử lý các ngoại lệ trên global, đó là thông qua middleware. Kết hợp global exception middleware với status codes chính xác cho các tình huống là một cách tốt nhất để giữ controller luôn clean và tránh các tình huống khó chịu và sự cố ứng dụng.
Thay vì thực hiện xử lý ngoại lệ theo cách thủ công như sau:
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
try
{
var productDto = await _service.GetProductAndApplyDiscount(id);
return Ok(productDto);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong while getting the product: {ex}");
return StatusCode(500, "Internal server error");
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.ConfigureExceptionHandler(logger);
...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Repetitive Logic
Trong một dự án, chúng ta không thể tránh khỏi một số phần, hoạt động lặp đi lặp lại. và bây giờ ta xử lý chương trình và khắc phục hạn chế tình trạng này.
Cụ thể, mình minh hoạ các bạn khi có method thêm mới sản phẩm với code sau đây:
[HttpPost]
public async Task<IActionResult> CreateProduct([FromBody] ProductDto productDto)
{
if (productDto == null)
{
_logger.LogError("Object sent from the frontend is null.");
return BadRequest("Object sent from the frontend is null.");
}
if (!ModelState.IsValid)
{
_logger.LogError("Invalid model state for the ProductDto object");
return UnprocessableEntity(ModelState);
}
var product = await _service.CreateProduct(productDto);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpPost]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public async Task<IActionResult> CreateProduct([FromBody] ProductDto productDto)
{
var product = await _service.CreateProduct(productDto);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
Và bên trong class ValidationFilterAttribute sẽ xử lý như sau:
public class ValidationFilterAttribute : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var param = context.ActionArguments.SingleOrDefault(p => p.Value is IEntity);
if(param.Value == null)
{
context.Result = new BadRequestObjectResult("Object is null");
return;
}
if(!context.ModelState.IsValid)
{
context.Result = new UnprocessableEntityObjectResult(context.ModelState);
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
Việc tạo các action filter không khó lắm nhưng cũng không phải là quá dễ. Nhưng một khi bạn thành thạo chúng, chúng sẽ trở thành những công cụ rất mạnh để phát triển API trong dự án của bạn.
Manual Authorization
.NET Core cung cấp một số tools để Authorize users và protect resources của bạn phù hợp.
Bạn không cần phải cố tạo ra một số cơ chế phức tạp và không cần thiết để thực hiện authorize. Trong hầu hết các trường hợp, thuộc tính [Authorize] có thể làm nên điều kỳ diệu như code sau:
[HttpPost, Authorize(Roles = "Manager")]
public async Task<IActionResult> CreateProduct([FromBody] ProductDto productDto)
{
var product = await _service.CreateProduct(productDto);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product );
}
Synchronous
Khi tạo các API linh hoạt, một trong những điều quan trọng nhất là làm cho nó bất đồng bộ từ trên xuống dưới.
API bất đồng bộ cung cấp trải nghiệm tốt hơn nhiều và đó là cách nên làm.
Entity Framework Core đã được cung cấp các phương thức bất đồng bộ để truy cập cơ sở dữ liệu, vậy tại sao không bắt đầu từ trên cùng (trong controller) và đẩy cách tiếp cận không đồng bộ xuống data access layer.
[HttpPost]
public async Task<IActionResult> CreateProduct([FromBody] ProductDto productDto)
{
var product = await _service.CreateProduct(productDto);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
"HTTP GET Đạo"
Tất cả chúng ta đều biết rằng một API tốt bao gồm tất cả các loại phương thức, GET, POST, PUT, DELETE, PATCH, và nhiều hơn nữa.
Một số người không thích dùng quá nhiều HTTP method và họ thích tạo bộ quy tắc của riêng mình về cách tổ chức API REST. Theo đó, họ xem các phương thức GET là tất cả những gì bạn sẽ cần, vì vậy họ sử dụng chúng như một phần chính của API của mình.
Lời kết
Đây là những điều quan trọng nhất bạn nên nghĩ đến khi cố gắng giữ cho các controller của bạn trở nên clean hơn trong .NET Core.
Sau khi đã đi sai lệch thì code sau này rất khó maintain, và đây là những tips để bạn tham khảo và áp dụng phù hợp. Nhưng trong những tình huống khác nhau bạn nên linh động và chọn giải pháp phù hợp.
Mong bài viết hữu ích, chúc các bạn thành công.Hieu Ho.