ASP.NET Core Web API — المحاضرة النظرية الشاملة

Model · Controller · CRUD · HTTP · EF Core · DbContext · Migration · Validation · Service · Relationships

محاضرة علمية نظرية · فصل جديد · .NET فقط


جدول المحتويات

#الموضوع
0مقدمة — Web API والعميل والخادم
1معمارية التطبيق في ASP.NET Core
2Model — نموذج البيانات
3Controller — متحكم الطلبات
4CRUD — العمليات الأساسية
5HTTP Methods ورموز الحالة
6Action Results — أنواع الاستجابة
7Entity Framework Core — ORM
8DbContext — الجسر مع قاعدة البيانات
9DbSet و LINQ — التعامل مع الجداول
10Migration — تحويل النماذج إلى جداول
11Validation — التحقق من صحة البيانات
12Service و Dependency Injection
13Relationships — العلاقات بين الجداول
14دورة حياة الطلب في ASP.NET Core
15خاتمة وملخص شامل

نطاق هذه المحاضرة: تغطي كل ما تعلّمته في مسار .NET Web API حتى الآن بصيغة نظرية أكاديمية للدراسة والمراجعة. لا تشمل: Repository Pattern، Roles، Claims، Custom Middleware — فهذه مواضيع متقدمة أو خارج نطاق هذا الفصل.


مقدمة — لماذا Web API؟

تطبيقات الويب الحديثة لا تعمل بمعزل عن بعضها. الهاتف، المتصفح، تطبيق سطح المكتب، أو نظام خارجي — كلها تحتاج قناة موحّدة لتبادل البيانات مع الخادم. هذه القناة في عالم .NET تُبنى غالبًا عبر ASP.NET Core Web API.

العميل (Client) والخادم (Server)

العميل هو الطرف الذي يطلب خدمة: يعرض واجهة، يجمع مدخلات المستخدم، ويرسل طلبًا عبر الشبكة.

الخادم هو الطرف الذي يستقبل الطلب، يُنفّذ المنطق، يتواصل مع قاعدة البيانات، ويُعيد استجابة (Response) بصيغة يفهمها العميل — غالبًا JSON.

Client ──HTTP Request──► Server (ASP.NET Core) Client ◄──HTTP Response── Server

ما المقصود بـ API؟

API اختصار لـ Application Programming Interface — واجهة برمجية تُعرّف كيف يتصل نظام خارجي بالتطبيق: أي عناوين (URLs)، أي أفعال HTTP، وأي بيانات يُرسل أو يُستقبل.

في سياق الويب، نتحدث عن RESTful API: تصميم يعتمد على موارد (Resources) مثل /api/products، وكل مورد يُدار عبر أفعال HTTP القياسية (GET, POST, PUT, DELETE).

مفهوم RESTالمعنى العملي
Resourceكيان قابل للتعامل معه — منتج، طالب، فئة
Endpointعنوان URL محدد — GET /api/products/5
Statelessكل طلب مستقل؛ الخادم لا «يتذكر» جلسة سابقة افتراضيًا
Representationالبيانات تُعرض كـ JSON (أو XML نادرًا)

صيغة JSON

عند إرسال أو استقبال بيانات عبر Web API، الصيغة الأكثر شيوعًا هي JSON (JavaScript Object Notation). مثال لمنتج:

{ "id": 1, "name": "Laptop", "price": 1200 }

ASP.NET Core يحوّل تلقائيًا بين كائنات C# و JSON عبر Model Binding و Serialization — أي أن الـ Controller يتعامل مع Product بينما العميل يرى JSON.


معمارية التطبيق في ASP.NET Core

البنية الطبقية (Layered Architecture) التي درستموها تفصل المسؤوليات وتمنع تكدّس المنطق في مكان واحد:

Client │ ▼ Controller ← استقبال الطلب وإرجاع الاستجابة │ ▼ Service ← منطق التطبيق (Business Logic) │ ▼ DbContext ← الوصول لقاعدة البيانات عبر EF Core │ ▼ Database

لماذا هذا التقسيم؟ الـ Controller يجب أن يبقى رفيعًا (Thin): يفهم HTTP فقط. المنطق الحسابي، التحقق المعقّد، وقواعد العمل تذهب إلى الـ Service. الوصول للبيانات يمر عبر DbContext. هذا يسهّل الاختبار، الصيانة، وتوسيع المشروع لاحقًا.

ملف Program.cs — نقطة البداية

في ASP.NET Core 6+، ملف Program.cs يُسجّل الخدمات ويبني التطبيق:

var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped<IProductService, ProductService>(); var app = builder.Build(); app.MapControllers(); app.Run();

هنا نلاحظ: تسجيل الـ DbContext، تسجيل الـ Service في Dependency Injection، وتفعيل الـ Controllers — موضوع نفصّله لاحقًا مع الـ Service.


أولًا: Model — نموذج البيانات

ما هو Model؟

الـ Model (أو Entity في سياق EF) هو class في C# يمثّل بيانات نريد تخزينها أو نقلها. يمكن اعتباره النسخة البرمجية من صف (Row) في جدول قاعدة بيانات.

مثال: جدول منتجات في SQL:

IdNamePrice
1Laptop1200

المكافئ في C#:

public class Product { public int Id { get; set; } public string Name { get; set; } public double Price { get; set; } }

وظائف الـ Model

اتفاقيات التسمية في EF Core

في C#في قاعدة البيانات (افتراضيًا)
Product (class)جدول Products
Id أو ProductIdالمفتاح الأساسي Primary Key
string Nameعمود Name من نوع nvarchar

خصائص من نوع int Id تُعامل تلقائيًا كمفتاح أساسي متزايد (Identity) ما لم تُعرّف خلاف ذلك.


ثانيًا: Controller — متحكم الطلبات

ما هو Controller؟

الـ Controller هو الصنف المسؤول عن استقبال طلبات HTTP وتوجيهها إلى الدالة المناسبة (Action)، ثم إرجاع نتيجة مناسبة للعميل.

عندما يرسل العميل:

GET /api/products

يصل الطلب إلى ProductsController ويُنفَّذ الـ Action المرتبط بـ GET.

تشريح Controller نموذجي

[ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly IProductService _service; public ProductsController(IProductService service) { _service = service; } [HttpGet] public IActionResult GetAll() { return Ok(_service.GetAll()); } }

السمات (Attributes) المهمة

السمةالدور
[ApiController]تفعيل سلوك API: ربط تلقائي للنموذج، تحقق من ModelState، مسارات أوضح
[Route("api/[controller]")]قالب المسار — [controller] يُستبدل باسم الصنف بدون كلمة Controller
[HttpGet] / [HttpPost]ربط الـ Action بفعل HTTP محدد
[FromBody]قراءة البيانات من جسم الطلب (JSON)
[FromRoute]قراءة قيمة من المسار — مثل {id}

وظائف Controller — وماذا لا يفعل

يفعل:

لا يفعل (في التصميم الجيد):


ثالثًا: CRUD — العمليات الأساسية

CRUD اختصار لأربع عمليات تغطي أغلب تطبيقات إدارة البيانات:

الحرفالعمليةHTTP (شائع)المعنى
CCreatePOSTإضافة سجل جديد
RReadGETقراءة سجل أو قائمة
UUpdatePUT / PATCHتعديل سجل موجود
DDeleteDELETEحذف سجل

أمثلة على Endpoints

العمليةالطلبالوصف
Read (الكل)GET /api/productsجلب كل المنتجات
Read (واحد)GET /api/products/1جلب منتج بالمعرّف 1
CreatePOST /api/products + JSON في الجسمإضافة منتج
UpdatePUT /api/products/1 + JSONاستبدال/تحديث المنتج 1
DeleteDELETE /api/products/1حذف المنتج 1

Controller كامل لـ CRUD (مفهومي)

[HttpGet] public IActionResult GetAll() => Ok(_service.GetAll()); [HttpGet("{id}")] public IActionResult GetById(int id) { var item = _service.GetById(id); if (item == null) return NotFound(); return Ok(item); } [HttpPost] public IActionResult Create(Product product) { var created = _service.Create(product); return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); } [HttpPut("{id}")] public IActionResult Update(int id, Product product) { if (!_service.Update(id, product)) return NotFound(); return NoContent(); } [HttpDelete("{id}")] public IActionResult Delete(int id) { if (!_service.Delete(id)) return NotFound(); return NoContent(); }

PUT مقابل PATCH: PUT يفترض إرسال الكائن كاملًا واستبداله. PATCH يعدّل حقولًا جزئية فقط — موضوع متقدم؛ في المشاريع الدراسية غالبًا يكفي PUT.


رابعًا: HTTP Methods ورموز الحالة

أفعال HTTP الأساسية

الفعلالاستخدامآمن؟*Idempotent؟**
GETجلب بيانات — لا يغيّر الحالة على الخادمنعمنعم
POSTإنشاء مورد جديدلالا
PUTتحديث كامل لمورد موجودلانعم
DELETEحذف موردلانعم

* آمن = لا يُفترض أن يغيّر بيانات الخادم. ** Idempotent = تكرار نفس الطلب يعطي نفس الأثر المنطقي.

رموز حالة HTTP (Status Codes)

كل استجابة HTTP تحمل رقم حالة يخبر العميل بنتيجة الطلب:

الرمزالاسممتى نستخدمه في API
200OKنجاح مع إرجاع بيانات — Ok(data)
201Createdتم الإنشاء — CreatedAtAction(...)
204No Contentنجاح بدون جسم — تحديث/حذف — NoContent()
400Bad Requestبيانات غير صالحة — فشل Validation
404Not Foundالمورد غير موجود — NotFound()
500Internal Server Errorخطأ غير متوقع في الخادم

العميل (Postman، تطبيق جوال، React) يقرأ هذا الرقم قبل قراءة JSON ليعرف هل نجحت العملية أم لا.


خامسًا: Action Results — أنواع الاستجابة

عندما ينتهي الـ Action، لا يُرجع دائمًا بيانات خام — بل يُرجع IActionResult يحدد نوع الاستجابة ورمز الحالة.

Ok()

يعني: العملية نجحت والبيانات موجودة في الجسم. رمز الحالة 200.

return Ok(products);

NotFound()

المورد المطلوب غير موجود. رمز الحالة 404. يُستخدم عند البحث بـ id غير موجود.

return NotFound(); // أو مع رسالة: return NotFound("المنتج غير موجود");

CreatedAtAction()

بعد إنشاء مورد جديد، الأفضل إرجاع 201 Created مع رابط المورد الجديد في هيدر Location:

return CreatedAtAction( nameof(GetById), new { id = product.Id }, product );

هذا يخبر العميل: «تم الإنشاء، ويمكنك جلب العنصر من هذا المسار».

NoContent()

نجاح بدون إرجاع جسم — شائع بعد PUT أو DELETE. رمز الحالة 204.

return NoContent();

BadRequest() — مع Validation

عند فشل التحقق من البيانات:

if (!ModelState.IsValid) return BadRequest(ModelState);

مع [ApiController]، كثير من حالات التحقق تُعالَج تلقائيًا وتُرجع 400.


سادسًا: Entity Framework Core — ORM

ما هو ORM؟

ORM = Object Relational Mapping — تقنية تربط بين:

بدل كتابة:

SELECT * FROM Products WHERE Price > 500

تكتب في C#:

_db.Products.Where(p => p.Price > 500).ToList();

فوائد Entity Framework Core

مزود قاعدة البيانات (Provider)

EF Core لا يتصل بقاعدة البيانات مباشرة — يستخدم Provider مثل:

options.UseSqlServer(connectionString); // أو options.UseSqlite(connectionString);

سابعًا: DbContext — الجسر مع قاعدة البيانات

ما هو DbContext؟

DbContext هو الصنف المركزي في EF Core: يمثّل جلسة عمل مع قاعدة البيانات خلال طلب HTTP واحد (عادةً).

public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; } }

ماذا يفعل DbContext؟

المهمةالشرح
الاتصاليفتح اتصالًا بقاعدة البيانات عند الحاجة
التتبعيتتبع الكائنات المعدّلة قبل الحفظ
الاستعلاميترجم LINQ إلى SQL
الحفظSaveChanges() يُنفّذ INSERT/UPDATE/DELETE
التكوينيمكن ضبط العلاقات في OnModelCreating

سلسلة الاتصال Connection String

تُخزَّن في appsettings.json:

{ "ConnectionStrings": { "DefaultConnection": "Server=.;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True" } }

وتُقرأ في Program.cs عند تسجيل DbContext — هذا يفصل إعدادات البيئة عن منطق التطبيق.

SaveChanges() — نقطة الحفظ الفعلية

عمليات مثل Add و Remove تُعدّل في الذاكرة فقط حتى تستدعي:

_db.Products.Add(product); _db.SaveChanges(); // هنا يُنفَّذ INSERT فعليًا

إذا نسيت SaveChanges()، لن يُحفظ شيء في قاعدة البيانات — خطأ شائع للمبتدئين.


ثامنًا: DbSet و LINQ — التعامل مع الجداول

ما هو DbSet؟

DbSet<T> يمثّل جدولًا (أو مجموعة كيانات من نوع T) داخل DbContext.

public DbSet<Product> Products { get; set; }

عمليات شائعة

العمليةالكودما يحدث بعد SaveChanges
إضافة_db.Products.Add(product)INSERT
قراءة الكل_db.Products.ToList()SELECT
بحث بالمعرّف_db.Products.Find(id)SELECT بالمفتاح
شرط_db.Products.FirstOrDefault(p => p.Id == id)SELECT مع WHERE
حذف_db.Products.Remove(product)DELETE
تحديثتعديل خصائص الكائن المتتبَعUPDATE

LINQ مع EF Core

LINQ (Language Integrated Query) يتيح كتابة استعلامات بلغة C#:

// منتجات أغلى من 1000 var expensive = _db.Products .Where(p => p.Price > 1000) .OrderBy(p => p.Name) .ToList(); // عدد المنتجات int count = _db.Products.Count(); // هل يوجد منتج بهذا الاسم؟ bool exists = _db.Products.Any(p => p.Name == "Laptop");

EF يترجم هذه التعبيرات إلى SQL عند تنفيذ ToList() أو FirstOrDefault() — وهذا يُسمى تأجيل التنفيذ (Deferred Execution).

تحميل العلاقات — Include

عند جلب كيان مع بيانات مرتبطة (مثل منتج مع فئته):

var products = _db.Products .Include(p => p.Category) .ToList();

بدون Include، خاصية Category قد تبقى null ما لم تُفعَّل التحميل الكسول (Lazy Loading) — في المشاريع الدراسية غالبًا نستخدم Include صراحةً.


تاسعًا: Migration — تحويل النماذج إلى جداول

لماذا Migration؟

عند تغيير الـ Models (إضافة خاصية، جدول جديد، علاقة)، نحتاج أن تنعكس التغييرات على قاعدة البيانات بشكل منظم وقابل للتكرار. الـ Migration هي «نسخة» من تغيير المخطط (Schema).

الأوامر الأساسية (Package Manager Console)

الأمرالوظيفة
Add-Migration InitialCreateإنشاء ملف Migration من حالة الـ Models الحالية
Update-Databaseتطبيق آخر Migration (أو كل المعلّقة) على قاعدة البيانات

ما الذي يحدث داخليًا؟

  1. EF يقرأ كل الـ DbSet والـ Models والتكوين في OnModelCreating
  2. يُنشئ ملف C# يصف التغيير (Up/Down)
  3. عند Update-Database يُنفّذ SQL المناسب (CREATE TABLE، ALTER، …)

قاعدة عملية: بعد أي تعديل على الـ Model، أنشئ Migration جديدة باسم وصفي — مثل AddProductDescription — ثم طبّقها. لا تعدّل قاعدة البيانات يدويًا ثم تنسى مزامنة الكود.

ملفات Migration

تُخزَّن في مجلد Migrations/ داخل المشروع. تحتوي دالتي Up (تطبيق) و Down (تراجع) — مفيدة للفريق وللنشر على خوادم متعددة بنفس المخطط.


عاشرًا: Validation — التحقق من صحة البيانات

لماذا Validation؟

لا يمكن الوثوق بمدخلات العميل. قد يرسل اسمًا فارغًا، سعرًا سالبًا، أو بريدًا بصيغة خاطئة. التحقق يمنع دخول بيانات فاسدة إلى قاعدة البيانات ويُرجع للعميل رسالة واضحة (400 Bad Request).

Data Annotations — الطريقة الشائعة

public class Product { public int Id { get; set; } [Required(ErrorMessage = "اسم المنتج مطلوب")] [MaxLength(100)] public string Name { get; set; } [Range(0.01, 999999)] public double Price { get; set; } [MinLength(3)] public string Description { get; set; } }

أهم سمات التحقق

السمةالمعنى
[Required]الحقل إلزامي — لا null ولا فارغ للنصوص
[MinLength(n)]الحد الأدنى لطول النص
[MaxLength(n)]الحد الأقصى لطول النص
[Range(min, max)]قيمة رقمية ضمن مدى
[EmailAddress]صيغة بريد إلكتروني
[StringLength(n)]طول نص محدد

ModelState في Controller

عند وصول الطلب، ASP.NET Core يملأ ModelState بنتائج التحقق. إذا فشل:

if (!ModelState.IsValid) return BadRequest(ModelState);

مع [ApiController]، الـ Framework قد يُرجع 400 تلقائيًا دون هذا السطر — لكن فهم ModelState مهم لفهم ما يحدث خلف الكواليس.


الحادي عشر: Service و Dependency Injection

لماذا Service Layer؟

وضع كل المنطق داخل Controller يؤدي إلى:

الحل: طبقة Service تحتوي منطق التطبيق، والـ Controller يقتصر على التنسيق.

قبل وبعد

بدون Service (غير مفضّل للمشاريع المتوسطة):

public IActionResult Get() { var products = _db.Products.ToList(); return Ok(products); }

مع Service (مفضّل):

// Controller public IActionResult Get() => Ok(_service.GetAll()); // Service public List<Product> GetAll() { return _db.Products.ToList(); }

Dependency Injection — حقن التبعيات

بدل أن ينشئ Controller الـ Service بنفسه (new ProductService())، يطلبها عبر المُنشئ (Constructor) والنظام يوفّرها:

public class ProductsController : ControllerBase { private readonly IProductService _service; public ProductsController(IProductService service) { _service = service; } }

التسجيل في Program.cs:

builder.Services.AddScoped<IProductService, ProductService>();
نوع التسجيلمتى يُستخدم
AddScopedمثيل واحد لكل طلب HTTP — الأنسب لـ DbContext و Services
AddTransientمثيل جديد في كل مرة يُطلب فيها
AddSingletonمثيل واحد لكل التطبيق — حذر مع DbContext

الواجهة IProductService

استخدام interface يفصل العقد عن التنفيذ ويسهّل الاختبار والتبديل:

public interface IProductService { List<Product> GetAll(); Product GetById(int id); Product Create(Product product); bool Update(int id, Product product); bool Delete(int id); }

ملاحظة: نمط Repository طبقة إضافية بين Service و DbContext — خارج نطاق هذه المحاضرة حسب منهجكم الحالي.


الثاني عشر: Relationships — العلاقات بين الجداول

قواعد البيانات العلائقية تربط الجداول ببعضها. EF Core يعبّر عن هذه الروابط عبر Navigation Properties و Foreign Keys.

One-to-One — واحد إلى واحد

كل سجل في الجدول A مرتبط بسجل واحد فقط في B.

مثال: مستخدم ← → ملف شخصي (Profile)

public class User { public int Id { get; set; } public string UserName { get; set; } public Profile Profile { get; set; } // navigation } public class Profile { public int Id { get; set; } public string Bio { get; set; } public int UserId { get; set; } // foreign key public User User { get; set; } }

One-to-Many — واحد إلى متعدد

الأكثر شيوعًا: فئة واحدة تحتوي عدة منتجات؛ كل منتج ينتمي لفئة واحدة.

public class Category { public int Id { get; set; } public string Name { get; set; } public List<Product> Products { get; set; } } public class Product { public int Id { get; set; } public string Name { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } }

في قاعدة البيانات: جدول Products يحمل عمود CategoryId يشير إلى Categories.Id.

Many-to-Many — متعدد إلى متعدد

مثال: طلاب ↔ مواد دراسية — كل طالب في عدة مواد، وكل مادة فيها عدة طلاب.

في EF Core 5+ يمكن تعريفها مباشرة:

public class Student { public int Id { get; set; } public string Name { get; set; } public List<Course> Courses { get; set; } } public class Course { public int Id { get; set; } public string Title { get; set; } public List<Student> Students { get; set; } }

EF يُنشئ تلقائيًا جدول ربط (Join Table) مثل CourseStudent يحمل المفتاحين.

سلوك الحذف Cascade

عند حذف فئة، ماذا يحدث لمنتجاتها؟ الخيارات تشمل:

يُضبط في OnModelCreating أو عبر Fluent API — مهم لفهم سلامة البيانات.

Fluent API (لمحة)

بديل لـ Data Annotations لضبط العلاقات بدقة:

protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>() .HasOne(p => p.Category) .WithMany(c => c.Products) .HasForeignKey(p => p.CategoryId) .OnDelete(DeleteBehavior.Restrict); }

دورة حياة الطلب في ASP.NET Core

عند وصول طلب HTTP، يمر عبر خط أنابيب Middleware — سلسلة من المكونات المدمجة قبل وصوله للـ Controller.

Request → Routing (تحديد Controller و Action) → Authentication (إن وُجدت — خارج تفصيل Roles/Claims هنا) → Authorization → Controller Action → Response

أمثلة على Middleware مدمج (ليس مخصصًا):

فهم وجود هذا الخط أنابيب يفسر لماذا يُسجَّل DbContext في Program.cs ولماذا يصل Controller جاهزًا بـ Service مُحقون — دون أن تكتب أنت آلية التوصيل.


خاتمة — ماذا أصبحت تستطيع؟

بعد دراسة هذه المحاضرة وما سبقها في المختبر، أصبحت الصورة الكاملة لـ ASP.NET Core Web API في مستواكم كالتالي:

المهارةالوصف المختصر
Modelتعريف كيانات البيانات والتحقق منها
Controllerربط HTTP ب Actions ونتائج مناسبة
CRUD + HTTPتصميم RESTful endpoints صحيحة
Action Results200, 201, 204, 404, 400 …
EF CoreORM، DbContext، DbSet، LINQ
Migrationمزامنة المخطط مع قاعدة البيانات
Validationحماية جودة المدخلات
Service + DIفصل المنطق وحقن التبعيات
Relationshipsربط الجداول 1-1، 1-N، N-N

هذه القاعدة تُمكّنكم من بناء API كامل لإدارة مورد حقيقي (منتجات، طلاب، مواعيد…) والانتقال لاحقًا إلى مواضيع متقدمة: مصادقة تفصيلية، أنماط تصميم إضافية، نشر على Azure أو IIS، وواجهات أمامية تستهلك الـ API.

نصيحة للمراجعة: اقرأ كل قسم واطرح على نفسك: «ما دور هذا الجزء في مسار طلب واحد من Postman حتى صف في SQL؟» — إذا ربطت الطبقات ببعضها، تثبت المعلومة أكثر من الحفظ المنفصل.