Endpoint
كود GetProducts
Endpoint
عشان توصل للـ GetProducts
، بتروح على /api/Products
زي ما قلت.
الكود بيكون كالتالي:
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await _productsRepo.GetAllAsync();
// الطريقة الأولى: رجعنا الـ products على هيئة Json
JsonResult result = new JsonResult(products);
result.StatusCode = 200;
// لو شيلت سطر الكود دا مش هيرجع ستيتس كود
return result;
}
الحل الثاني باستخدام OkObjectResult
لو عايز تتفادى ضبط الـ Status Code يدوي، تقدر تستخدم OkObjectResult
اللي بترجع الـ Status Code كـ 200 بشكل تلقائي:
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await _productsRepo.GetAllAsync();
// الطريقة الثانية: باستخدام `OkObjectResult`
OkObjectResult result = new OkObjectResult(products);
return result;
}
استخدام Ok
Helper Method
أحسن طريقة هي استخدام الـ Helper Method Ok()
، اللي بترجع برضو الـ Status Code كـ 200 بشكل تلقائي ومباشر:
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await _productsRepo.GetAllAsync();
// الطريقة الثالثة: باستخدام Helper Method `Ok`
return Ok(products);
}
ملاحظة: الـ
Ok
Helper Method بتسهل الشغل جدًا لأنها بتختصر استخدام الـ Special Classes زيOkObjectResult
وبتخلي الكود أنظف وأبسط.
Action Result
المشكلة
لما بتستخدم Task<IActionResult>
، مش بيديك الـ Swagger الشكل المحدد للـ Response، وده بيسبب مشكلة لما تحب تعرض شكل الداتا المتوقع لكل API Endpoint في Swagger.
لو حددت إن الدالة ترجع IEnumerable<Product>
، هيبان شكل الـ Response، لكن كده بتقيد الـ Response بأن يكون دايمًا من نوع IEnumerable<Product>
، وده مش عملي، خصوصًا لو عايز ترجع أنواع مختلفة من الـ Status Codes زي NotFound
لما المنتج مش موجود، أو BadRequest
في حالة حدوث خطأ.
الحل باستخدام ActionResult<T>
بدل ما تستخدم IActionResult
، الحل الأفضل هنا هو استخدام الكلاس ActionResult
بنوع Generic، زي كده:
Task<ActionResult<IEnumerable<Product>>>
ليه ActionResult<T>
هو الخيار الأنسب؟
-
دعم أنواع متعددة من الـ Responses:
- باستخدام
ActionResult<T>
, تقدر ترجعOk(products)
لما يكون عندك بيانات، أوNotFound()
لو مش موجودة، وده بيدي مرونة أكتر في أنواع الـ Responses اللي ممكن ترجعها.
- باستخدام
-
تحديد شكل الـ Response في Swagger:
- لما تستخدم
ActionResult<IEnumerable<Product>>
، هيظهر في Swagger شكل الداتا المتوقع اللي من نوعIEnumerable<Product>
. يعني لو حد شاف الـ API، هيكون عارف الـ Response المتوقع، وده بيسهل فهم API للأشخاص اللي هيستخدموها.
- لما تستخدم
-
المرونة في التعامل مع الحالات المختلفة:
- زي ما قلت، تقدر ترجع أنواع مختلفة زي
Ok
,NotFound
,BadRequest
، مع إنك بتحدد نوع الـ Data المتوقع في نفس الوقت.
- زي ما قلت، تقدر ترجع أنواع مختلفة زي
مثال كامل على GetProducts
باستخدام ActionResult
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
var products = await _productsRepo.GetAllAsync();
if (products == null || !products.Any())
{
return NotFound("No products available.");
}
return Ok(products);
}
نقاط إضافية:
-
لما ترجع
NotFound()
أوOk()
: بـActionResult<T>
، تقدر تتعامل مع أنواع مختلفة من النتائج بدون ما تفقد مرونة نوع الـ Response. -
تحسين تجربة Swagger: بـ
ActionResult<T>
، هيبان نوع الداتا اللي هيرجع، وده بيدي وثائق أكتر وضوحًا لأي حد بيشوف الـ API في Swagger.
ملخص
استخدام Task<ActionResult<IEnumerable<Product>>>
بدل Task<IActionResult>
بيدي مرونة أكتر في أنواع الـ Responses اللي ممكن ترجعها، وبيحسن من توثيق الـ API في Swagger، ويخليك تستفيد من أفضل مميزات ASP.NET Core في التحكم بالـ Status Codes.
Related Data (Loading)
مشكلة الـ Related Data في Swagger
لما بنرجع البيانات، بنلاحظ إن الـ Related Data زي Category
و Brand
بيكونوا null
في الـ Response، لأنهم عبارة عن Navigational Properties، وبشكل افتراضي مش بيتم تحميلهم مع البيانات الأساسية. عشان نحملهم، عندنا 3 طرق:
- Explicit Loading: تحميل البيانات يدويًا عند الحاجة.
- Eager Loading: تحميل البيانات دفعة واحدة مع الكيان الرئيسي.
- Lazy Loading: تحميل البيانات تلقائيًا عند الوصول للخاصية.
هنا هنستثني Explicit Loading، وهنركز على Lazy Loading وEager Loading.
1. Lazy Loading
تعريف Lazy Loading
الـ Lazy Loading هو أسلوب لتحميل البيانات المرتبطة فقط عند الحاجة إليها، وده بيساعد في تقليل كمية البيانات المحملة، وبالتالي تحسين الأداء في بعض الحالات.
كيفية تفعيل Lazy Loading
-
إضافة الـ Proxies Package:
- نستخدم الأمر التالي لتثبيت الحزمة:
dotnet add package Microsoft.EntityFrameworkCore.Proxies
- نستخدم الأمر التالي لتثبيت الحزمة:
-
تفعيل الـ Lazy Loading Proxies في الـ DbContext:
- داخل ملف
DbContext
، بنضيف تفعيل للـ Lazy Loading عن طريقOnConfiguring
أو فيConfigureServices
:optionsBuilder.UseLazyLoadingProxies();
- داخل ملف
-
تحديد الـ Navigational Properties كـ
virtual
:- كل الـ Navigational Properties لازم تكون
virtual
عشان الـ Lazy Loading تشتغل بشكل صحيح. مثال:public class Product { public int Id { get; set; } public string Name { get; set; } public virtual Category Category { get; set; } public virtual Brand Brand { get; set; } }
- كل الـ Navigational Properties لازم تكون
متى نستخدم Lazy Loading؟
- لو البيانات المترابطة مش مطلوبة دائمًا: بيساعد في تقليل كمية البيانات المحملة.
- لو بتحتاج مرونة أكبر في الوصول للبيانات المرتبطة.
ملاحظة: في حالة استخدام الـ Lazy Loading، لازم يكون الـ DbContext مفتوح عند الوصول للـ Related Data، لأنك لو قفلته قبل ما تطلب البيانات، هيظهر لك خطأ.
2. Eager Loading
تعريف Eager Loading
الـ Eager Loading هو أسلوب لتحميل البيانات المرتبطة كلها مرة واحدة مع الكيان الرئيسي. بنستخدمه عشان نضمن إن البيانات المرتبطة بتتحمل دفعة واحدة، وده بيساعد في تحسين الأداء لما نحتاج كل البيانات دفعة واحدة.
كيفية تفعيل Eager Loading
نستخدم Include
عشان نحدد الـ Related Data اللي عايزين نحملها مع الـ Query.
مثال على Eager Loading في دالة GetProducts
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
var products = await _productsRepo
.GetAll()
.Include(p => p.Category)
.Include(p => p.Brand)
.ToListAsync();
if (!products.Any())
{
return NotFound("No products available.");
}
return Ok(products);
}
متى نستخدم Eager Loading؟
- لو كنت عايز تحمل كل البيانات المرتبطة مرة واحدة: خصوصًا لو بتحتاجها بشكل مستمر.
- لتقليل عدد الاستعلامات: Eager Loading بيحسن الأداء في الحالات اللي فيها بيانات مترابطة كتير.
ملخص
النوع | تعريف | متى يُستخدم |
---|---|---|
Lazy Loading | تحميل البيانات المرتبطة فقط عند الوصول إليها | لما تكون البيانات المترابطة مش مطلوبة دائمًا وتحتاج مرونة أكبر |
Eager Loading | تحميل كل البيانات المرتبطة دفعة واحدة باستخدام Include | لما تحتاج كل البيانات المرتبطة مرة واحدة ولتجنب تكرار الاستعلامات |
باختصار:
- لو عايز البيانات المرتبطة تتحمل تلقائيًا وقت الحاجة، استخدم Lazy Loading.
- لو عايز تحمل كل البيانات المرتبطة مرة واحدة، استخدم Eager Loading عن طريق
Include
.
Include
إعداد الـ Include في GenericRepository
في ملف الـ Generic Repository، لما بنتعامل مع نوع محدد زي Product
، ممكن نحتاج نحمل الـ Navigational Properties
زي Brand
و Category
، فبنستخدم Include
كالتالي:
if (typeof(T) == typeof(Product))
{
var products = await _dbContext.Set<Product>()
.Include(p => p.Brand)
.Include(p => p.Category)
.ToListAsync();
return (IEnumerable<T>)products;
}
لماذا استخدمنا Include
فقط بدون ThenInclude
؟
لأن كلا من Brand
و Category
هما Navigational Properties مباشرة داخل كلاس Product
.
- استخدام
Include
يكفي لتحميلهم لأنهم موجودين بشكل مباشر في الكلاس. - متى نستخدم
ThenInclude
؟ نستخدمThenInclude
فقط عندما تكون عندنا علاقة داخل علاقة أخرى. مثلًا، لو كان الـCategory
موجودًا داخلBrand
وليس بشكل مباشر فيProduct
، كنا هنستخدمThenInclude
عشان نصل إلى الـCategory
.
هل يتم تنفيذ Inner Join عند استخدام Include
؟
نعم، عند استخدام Include
لتحميل الـ Navigational Properties
، الـ Entity Framework Core بينشئ استعلام INNER JOIN
في الخلفية لجلب البيانات المرتبطة. الاستعلام بيكون مشابه لـ:
SELECT *
FROM Products
INNER JOIN Brands ON Products.BrandId = Brands.Id
INNER JOIN Categories ON Products.CategoryId = Categories.Id
ملاحظة: في حالة لو كانت القيم المرتبطة (مثل
Brand
أوCategory
) بتقبلnull
، بيتم تنفيذ LEFT JOIN بدل منINNER JOIN
، عشان نضمن استرجاع كل المنتجات حتى لو مش مرتبط بيهاBrand
أوCategory
.
حل المشكلة باستخدام Specification Design Pattern
أفضل حل هنا هو استخدام Specification Design Pattern لأنه بيوفر مرونة في تحديد شروط البحث والـ Includes المطلوبة.
- هذا الـ Pattern بيسمح لنا بكتابة الشروط المطلوبة والـ Includes مرة واحدة فقط، وإعادة استخدامها بدون تكرار.
- يساعد في جعل الكود أكتر تنظيمًا وأسهل في الصيانة، لأنه بيجمع الشروط المشتركة في مكان واحد يمكن تطبيقه في عدة أماكن.
باستخدام الـ Specification Pattern، نقدر نحدد الشروط زي Include
وWhere
بسهولة، ويكون الحل مثالي للـ Queries اللي بتحتاج أكتر من شرط أو شرط معين على نوع معين من البيانات.
DTOs
شكل الـ Return مش كويس
حاليًا، الـ API بيعرض بيانات المنتج بالشكل ده:
{
"name": "Double Caramel Frappuccino",
"description": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.",
"price": 200.00,
"pictureUrl": "images/products/sb-ang1.png",
"brand": {
"name": "Starbucks",
"id": 1
}
}
المشكلة
فيه تكرار في البيانات، مثلًا BrandId
بيظهر فوق، وتحت تاني بيظهر الـ ID وName للـ Brand. كمان شكل البيانات مش مظبوط، ودي مشكلة في الـ Mapping، يعني اللي ظاهر قدامي دا هو الـ Model الأساسي.
الحل باستخدام View Model أو DTO
المفروض أعمل Return باستخدام View Model، اللي هنا بنسميه DTO.
- مثلًا، عايز أظبط الـ
PictureUrl
عشان يظهر بشكل كامل، وأعدل باقي البيانات عشان تظهر بشكل أفضل.
المشكلة في الـ Navigational Properties المتداخلة
لما يكون عندك Navigational Property داخل Brand، زي لو كان الـ Product
مرتبط بالـ Brand
، فالـ Brand هيظهر كجزء من الـ Product
، ولو دخلنا جوا الـ Brand هنلاقي بيانات تانية مرتبطة بالـ Product
، وده بيعمل حلقة متكررة.
يعني كل ما تضغط، هتلاقي البيانات متداخلة مع بعضها، وممكن تفضل تتنقل بينهم بدون نهاية.
الحل باستخدام DTO لتجنب التداخل
عشان نتجنب المشكلة دي، بنستخدم DTO (Data Transfer Object)، اللي بيخلينا نتحكم بشكل كامل في البيانات اللي هنرجعها.
هنحدد فقط المعلومات المطلوبة في الـ ProductDto
من غير الـ Navigational Properties اللي ممكن تعمل تداخل.
مثال عملي
- هننشئ DTO يضم البيانات المطلوبة بس، بدون أي تداخل للـ Navigational Properties.
- بنرجع البيانات في شكل مبسط وواضح.
ده بيمنع إن أي
Navigational Property
جواBrand
تربطنا ببياناتProduct
تاني، فبالتالي بنضمن إن البيانات متبقاش متداخلة.