
Data Access Logic
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _repository.GetProduct(id);
return Ok(product);
}
Business Logic
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _repository.GetProduct(id);
ApplyTheDiscount(product);
return Ok(product);
}
public ProductController(IServiceManager service)
{
_service = service;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _service.GetProductAndApplyDiscount(id);
return Ok(product);
}
Để rõ hơn, chúng ta quay về định nghĩa về repository pattern:
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
[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);
}
Bây giờ chúng ta đã giải quyết được vấn đề trước đó và kết quả là chúng ta đang gửi một class DTO thay vì cả thực thể và điều đó tốt hơn nhiều. Theo cách này, chúng ta sẽ tránh được rất nhiều vấn đề tiềm ẩn.
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _service.GetProductAndApplyDiscount(id);
var productDto = _mapper.Map<ProductDto>(product);
return Ok(productDto);
}
Code trên, đã minh hoạ được cách bạn sử dụng Auto Mapper, nhưng để code bạn trở nên gọn hơn và đảm bảo mục đích ban đầu đặt ra, bây giờ bạn cần chuyển việc sử dụng Auto Mapper bên trong service, và khi đó controller của chúng ta sẽ còn như sau:
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var productDto = await _service.GetProductAndApplyDiscount(id);
return Ok(productDto);
}
Bây giờ lớp service trả về cho chúng ta lớp DTO trực tiếp thay vì thực thể, mà chúng ta cần ánh xạ trong controller và do đó làm cho controller trở nên sạch hơn, ngắn gọn hơn.
Exception Handling
[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");
}
}
Chúng ta có thể cấu hình logic xử lý ngoại lệ trong phương thức Configure của class Startup (ở .NET 6 sẽ xử lý ở class Program):
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.ConfigureExceptionHandler(logger);
...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Cụ thể, cách xử lý ngoại lệ trên global, mình sẽ giới thiệu chi tiết ở bài sau.
Repetitive Logic
[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);
}
Như code trên, bạn có thể thấy được, chúng ta đã dùng câu lệnh if quá nhiều lần. Và giải pháp bây giờ là sử dụng action filter. Sau đã sử dụng, ta có validation filter đơn giản như sau:
[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);
}
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)
{
}
}
Và tất nhiên, trong những bài sắp tới, mình sẽ hướng dẫn các bạn rõ hơn về action filter trong .NET Core.
Manual Authorization
[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 );
}
Điều này có nghĩa là chỉ có những user với role Manager mới có thể thêm mới sản phẩm.
Synchronous
[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”
Lời kết
Mong bài viết hữu ích, chúc các bạn thành công.Hieu Ho.