إضافة Validation باستخدام Data Annotations

المشكلة:

حاليًا، يمكن للـ Consumer إرسال بيانات غير صحيحة مثل:

  • Quantity يساوي 0.
  • Price يساوي 0 أو أقل.

للتأكد من صحة البيانات، نحتاج إلى استخدام Data Annotations للتحقق من صحة المدخلات. لكن، بما إننا ما ينفعش نضيف Validation مباشرة على الـ Entity Class (لأنها مرتبطة بالبنية الداخلية)، هنستخدم الـ DTOs.


1. إنشاء CustomerBasketDto و BasketItemDto

هنضيف DTOs داخل فولدر DTOs في مشروع الـ API.

CustomerBasketDto:

يمثل بيانات السلة، ويحتوي على قائمة من العناصر.

public class CustomerBasketDto
{
    [Required] // Required to ensure Id is not null or empty
    public string Id { get; set; }
 
    [Required] // Ensure the list of items is not null
    public List<BasketItemDto> Items { get; set; }
}

BasketItemDto:

يمثل العنصر داخل السلة، مع التحقق من صحة كل خاصية.

public class BasketItemDto
{
    [Required] // Ensure Id is not null
    public int Id { get; set; }
 
    [Required] // Ensure ProductName is not null or empty
    public string ProductName { get; set; }
 
    [Required] // Ensure PictureUrl is not null
    public string PictureUrl { get; set; }
 
    [Required]
    [Range(0.1, double.MaxValue, ErrorMessage = "Price must be greater than zero!!")]
    public decimal Price { get; set; }
 
    [Required] // Ensure Category is not null
    public string Category { get; set; }
 
    [Required] // Ensure Brand is not null
    public string Brand { get; set; }
 
    [Required]
    [Range(1, int.MaxValue, ErrorMessage = "Quantity must be at least one item")]
    public int Quantity { get; set; }
}

ملاحظات:

  • استخدمنا [Required] لضمان أن القيم الأساسية مش Null.
  • استخدمنا [Range] للتحقق من:
    • أن السعر (Price) أكبر من 0.
    • أن الكمية (Quantity) على الأقل 1.
  • سبب عدم استخدام 0.1M مع Range:
    • لأن Range لا تدعم القيم من نوع decimal.

2. تعديل Endpoint لاستخدام CustomerBasketDto

الكود المعدل للـ Endpoint:

[HttpPost] // POST: /api/basket
public async Task<ActionResult<CustomerBasket>> UpdateBasket(CustomerBasketDto basketDto)
{
    // Validate Model
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
 
    // Mapping DTO to Entity
    var customerBasket = _mapper.Map<CustomerBasket>(basketDto);
 
    var createdOrUpdatedBasket = await _basketRepository.UpdateBasketAsync(customerBasket);
 
    if (createdOrUpdatedBasket == null)
        return BadRequest(new ApiResponse(400));
 
    return Ok(createdOrUpdatedBasket);
}

3. إنشاء Mapping Profile

هنستخدم AutoMapper لتحويل الـ DTOs إلى Entities. اتكلمنا عنها قبل كدا في Product DTO using Auto Mapper

خطوات إعداد AutoMapper:

  1. إضافة AutoMapper في المشروع:

    • نثبت الحزمة:
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
  1. إضافة Mapping Profile: هننشئ ملف جديد في فولدر Helpers ونسميه MappingProfile.
using AutoMapper;
 
public class MappingProfile : Profile
{
	public MappingProfile()
	{
		CreateMap<CustomerBasketDto, CustomerBasket>();
		CreateMap<BasketItemDto, BasketItem>();
	}
}
  1. تسجيل AutoMapper في DI Container: في ملف Program.cs:
builder.Services.AddAutoMapper(typeof(Program));

4. التحقق من صحة البيانات في الـ Controller

  • قبل تنفيذ أي عملية، يتم التحقق من صحة البيانات باستخدام ModelState.IsValid.
  • إذا كانت البيانات غير صحيحة، يتم إرجاع 400 Bad Request مع الأخطاء.

الكود النهائي للـ Controller

using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
 
public class BasketController : BaseApiController
{
    private readonly IBasketRepository _basketRepository;
    private readonly IMapper _mapper;
 
    public BasketController(IBasketRepository basketRepository, IMapper mapper)
    {
        _basketRepository = basketRepository;
        _mapper = mapper;
    }
 
    // GET: /api/basket/{id}
    [HttpGet("{id}")]
    public async Task<ActionResult<CustomerBasket>> GetBasket(string id)
    {
        var basket = await _basketRepository.GetBasketAsync(id);
        return Ok(basket ?? new CustomerBasket(id));
    }
 
    // POST: /api/basket
    [HttpPost]
    public async Task<ActionResult<CustomerBasket>> UpdateBasket(CustomerBasketDto basketDto)
    {
        // Validate Model
        if (!ModelState.IsValid)
            return BadRequest(ModelState);
 
        // Map DTO to Entity
        var customerBasket = _mapper.Map<CustomerBasket>(basketDto);
 
        var createdOrUpdatedBasket = await _basketRepository.UpdateBasketAsync(customerBasket);
 
        if (createdOrUpdatedBasket == null)
            return BadRequest(new ApiResponse(400));
 
        return Ok(createdOrUpdatedBasket);
    }
 
    // DELETE: /api/basket/{id}
    [HttpDelete("{id}")]
    public async Task<ActionResult> DeleteBasket(string id)
    {
        await _basketRepository.DeleteBasketAsync(id);
        return NoContent();
    }
}

5. الكود النهائي للـ CustomerBasketDto و BasketItemDto

CustomerBasketDto:

public class CustomerBasketDto
{
    [Required]
    public string Id { get; set; }
 
    [Required]
    public List<BasketItemDto> Items { get; set; }
}

BasketItemDto:

public class BasketItemDto
{
    [Required]
    public int Id { get; set; }
 
    [Required]
    public string ProductName { get; set; }
 
    [Required]
    public string PictureUrl { get; set; }
 
    [Required]
    [Range(0.1, double.MaxValue, ErrorMessage = "Price must be greater than zero!!")]
    public decimal Price { get; set; }
 
    [Required]
    public string Category { get; set; }
 
    [Required]
    public string Brand { get; set; }
 
    [Required]
    [Range(1, int.MaxValue, ErrorMessage = "Quantity must be at least one item")]
    public int Quantity { get; set; }
}

6. فوائد

  1. فصل الـ Entities عن الـ DTOs:
    • يساعد على الحفاظ على الكود نظيف ومنظم.
  2. ضمان صحة البيانات:
    • التحقق من صحة المدخلات باستخدام Data Annotations.
  3. قابلية التوسع:
    • يمكن تعديل الـ DTOs بسهولة بدون التأثير على الـ Entities.
  4. إعادة استخدام AutoMapper:
    • يمكن استخدام نفس الـ Mapping في أي مكان آخر بالمشروع.

مشكلة

ممكن برضو معلومات زي الـ Price ميبقاش موثوق فيها عادي بس أنا هتأكد من المعلومات دي من الـ Database لما أجي أعمل Checkout فالآخر فأنا كل اللي انا عايزه دلوقتي تقريبًا هو الـ Id و الـ Quantity