إضافة 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
:
-
إضافة
AutoMapper
في المشروع:- نثبت الحزمة:
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
- إضافة Mapping Profile: هننشئ ملف جديد في فولدر
Helpers
ونسميهMappingProfile
.
using AutoMapper;
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<CustomerBasketDto, CustomerBasket>();
CreateMap<BasketItemDto, BasketItem>();
}
}
- تسجيل
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. فوائد
- فصل الـ Entities عن الـ DTOs:
- يساعد على الحفاظ على الكود نظيف ومنظم.
- ضمان صحة البيانات:
- التحقق من صحة المدخلات باستخدام Data Annotations.
- قابلية التوسع:
- يمكن تعديل الـ DTOs بسهولة بدون التأثير على الـ Entities.
- إعادة استخدام AutoMapper:
- يمكن استخدام نفس الـ Mapping في أي مكان آخر بالمشروع.
مشكلة
ممكن برضو معلومات زي الـ Price ميبقاش موثوق فيها عادي بس أنا هتأكد من المعلومات دي من الـ Database لما أجي أعمل Checkout فالآخر فأنا كل اللي انا عايزه دلوقتي تقريبًا هو الـ Id و الـ Quantity