Seamless design · High performance
Model · Controller · CRUD · HTTP · EF Core · DbContext · Migration · Validation · Service · Relationships
محاضرة علمية نظرية · فصل جديد · .NET فقط
نطاق هذه المحاضرة: تغطي كل ما تعلّمته في مسار
.NET Web APIحتى الآن بصيغة نظرية أكاديمية للدراسة والمراجعة. لا تشمل: Repository Pattern، Roles، Claims، Custom Middleware — فهذه مواضيع متقدمة أو خارج نطاق هذا الفصل.
تطبيقات الويب الحديثة لا تعمل بمعزل عن بعضها. الهاتف، المتصفح، تطبيق سطح المكتب، أو نظام خارجي — كلها تحتاج قناة موحّدة لتبادل البيانات مع الخادم. هذه القناة في عالم .NET تُبنى غالبًا عبر ASP.NET Core Web API.
العميل هو الطرف الذي يطلب خدمة: يعرض واجهة، يجمع مدخلات المستخدم، ويرسل طلبًا عبر الشبكة.
الخادم هو الطرف الذي يستقبل الطلب، يُنفّذ المنطق، يتواصل مع قاعدة البيانات، ويُعيد استجابة (Response) بصيغة يفهمها العميل — غالبًا JSON.
Client ──HTTP Request──► Server (ASP.NET Core)
Client ◄──HTTP Response── Server
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 نادرًا) |
عند إرسال أو استقبال بيانات عبر Web API، الصيغة الأكثر شيوعًا هي JSON (JavaScript Object Notation). مثال لمنتج:
{
"id": 1,
"name": "Laptop",
"price": 1200
}
ASP.NET Core يحوّل تلقائيًا بين كائنات C# و JSON عبر Model Binding و Serialization — أي أن الـ Controller يتعامل مع Product بينما العميل يرى JSON.
البنية الطبقية (Layered Architecture) التي درستموها تفصل المسؤوليات وتمنع تكدّس المنطق في مكان واحد:
Client
│
▼
Controller ← استقبال الطلب وإرجاع الاستجابة
│
▼
Service ← منطق التطبيق (Business Logic)
│
▼
DbContext ← الوصول لقاعدة البيانات عبر EF Core
│
▼
Database
لماذا هذا التقسيم؟ الـ Controller يجب أن يبقى رفيعًا (Thin): يفهم HTTP فقط. المنطق الحسابي، التحقق المعقّد، وقواعد العمل تذهب إلى الـ Service. الوصول للبيانات يمر عبر DbContext. هذا يسهّل الاختبار، الصيانة، وتوسيع المشروع لاحقًا.
في 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 (أو Entity في سياق EF) هو class في C# يمثّل بيانات نريد تخزينها أو نقلها. يمكن اعتباره النسخة البرمجية من صف (Row) في جدول قاعدة بيانات.
مثال: جدول منتجات في SQL:
| Id | Name | Price |
|---|---|---|
| 1 | Laptop | 1200 |
المكافئ في C#:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
}
[Required].| في C# | في قاعدة البيانات (افتراضيًا) |
|---|---|
Product (class) | جدول Products |
Id أو ProductId | المفتاح الأساسي Primary Key |
string Name | عمود Name من نوع nvarchar |
خصائص من نوع int Id تُعامل تلقائيًا كمفتاح أساسي متزايد (Identity) ما لم تُعرّف خلاف ذلك.
الـ Controller هو الصنف المسؤول عن استقبال طلبات HTTP وتوجيهها إلى الدالة المناسبة (Action)، ثم إرجاع نتيجة مناسبة للعميل.
عندما يرسل العميل:
GET /api/products
يصل الطلب إلى ProductsController ويُنفَّذ الـ Action المرتبط بـ GET.
[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());
}
}
| السمة | الدور |
|---|---|
[ApiController] | تفعيل سلوك API: ربط تلقائي للنموذج، تحقق من ModelState، مسارات أوضح |
[Route("api/[controller]")] | قالب المسار — [controller] يُستبدل باسم الصنف بدون كلمة Controller |
[HttpGet] / [HttpPost] … | ربط الـ Action بفعل HTTP محدد |
[FromBody] | قراءة البيانات من جسم الطلب (JSON) |
[FromRoute] | قراءة قيمة من المسار — مثل {id} |
يفعل:
Ok(), NotFound(), …لا يفعل (في التصميم الجيد):
CRUD اختصار لأربع عمليات تغطي أغلب تطبيقات إدارة البيانات:
| الحرف | العملية | HTTP (شائع) | المعنى |
|---|---|---|---|
| C | Create | POST | إضافة سجل جديد |
| R | Read | GET | قراءة سجل أو قائمة |
| U | Update | PUT / PATCH | تعديل سجل موجود |
| D | Delete | DELETE | حذف سجل |
| العملية | الطلب | الوصف |
|---|---|---|
| Read (الكل) | GET /api/products | جلب كل المنتجات |
| Read (واحد) | GET /api/products/1 | جلب منتج بالمعرّف 1 |
| Create | POST /api/products + JSON في الجسم | إضافة منتج |
| Update | PUT /api/products/1 + JSON | استبدال/تحديث المنتج 1 |
| Delete | DELETE /api/products/1 | حذف المنتج 1 |
[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.
| الفعل | الاستخدام | آمن؟* | Idempotent؟** |
|---|---|---|---|
GET | جلب بيانات — لا يغيّر الحالة على الخادم | نعم | نعم |
POST | إنشاء مورد جديد | لا | لا |
PUT | تحديث كامل لمورد موجود | لا | نعم |
DELETE | حذف مورد | لا | نعم |
* آمن = لا يُفترض أن يغيّر بيانات الخادم. ** Idempotent = تكرار نفس الطلب يعطي نفس الأثر المنطقي.
كل استجابة HTTP تحمل رقم حالة يخبر العميل بنتيجة الطلب:
| الرمز | الاسم | متى نستخدمه في API |
|---|---|---|
| 200 | OK | نجاح مع إرجاع بيانات — Ok(data) |
| 201 | Created | تم الإنشاء — CreatedAtAction(...) |
| 204 | No Content | نجاح بدون جسم — تحديث/حذف — NoContent() |
| 400 | Bad Request | بيانات غير صالحة — فشل Validation |
| 404 | Not Found | المورد غير موجود — NotFound() |
| 500 | Internal Server Error | خطأ غير متوقع في الخادم |
العميل (Postman، تطبيق جوال، React) يقرأ هذا الرقم قبل قراءة JSON ليعرف هل نجحت العملية أم لا.
عندما ينتهي الـ Action، لا يُرجع دائمًا بيانات خام — بل يُرجع IActionResult يحدد نوع الاستجابة ورمز الحالة.
يعني: العملية نجحت والبيانات موجودة في الجسم. رمز الحالة 200.
return Ok(products);
المورد المطلوب غير موجود. رمز الحالة 404. يُستخدم عند البحث بـ id غير موجود.
return NotFound();
// أو مع رسالة: return NotFound("المنتج غير موجود");
بعد إنشاء مورد جديد، الأفضل إرجاع 201 Created مع رابط المورد الجديد في هيدر Location:
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product
);
هذا يخبر العميل: «تم الإنشاء، ويمكنك جلب العنصر من هذا المسار».
نجاح بدون إرجاع جسم — شائع بعد PUT أو DELETE. رمز الحالة 204.
return NoContent();
عند فشل التحقق من البيانات:
if (!ModelState.IsValid)
return BadRequest(ModelState);
مع [ApiController]، كثير من حالات التحقق تُعالَج تلقائيًا وتُرجع 400.
ORM = Object Relational Mapping — تقنية تربط بين:
بدل كتابة:
SELECT * FROM Products WHERE Price > 500
تكتب في C#:
_db.Products.Where(p => p.Price > 500).ToList();
EF Core لا يتصل بقاعدة البيانات مباشرة — يستخدم Provider مثل:
options.UseSqlServer(connectionString);
// أو
options.UseSqlite(connectionString);
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; }
}
| المهمة | الشرح |
|---|---|
| الاتصال | يفتح اتصالًا بقاعدة البيانات عند الحاجة |
| التتبع | يتتبع الكائنات المعدّلة قبل الحفظ |
| الاستعلام | يترجم LINQ إلى SQL |
| الحفظ | SaveChanges() يُنفّذ INSERT/UPDATE/DELETE |
| التكوين | يمكن ضبط العلاقات في OnModelCreating |
تُخزَّن في appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True"
}
}
وتُقرأ في Program.cs عند تسجيل DbContext — هذا يفصل إعدادات البيئة عن منطق التطبيق.
عمليات مثل Add و Remove تُعدّل في الذاكرة فقط حتى تستدعي:
_db.Products.Add(product);
_db.SaveChanges(); // هنا يُنفَّذ INSERT فعليًا
إذا نسيت
SaveChanges()، لن يُحفظ شيء في قاعدة البيانات — خطأ شائع للمبتدئين.
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 (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).
عند جلب كيان مع بيانات مرتبطة (مثل منتج مع فئته):
var products = _db.Products
.Include(p => p.Category)
.ToList();
بدون Include، خاصية Category قد تبقى null ما لم تُفعَّل التحميل الكسول (Lazy Loading) — في المشاريع الدراسية غالبًا نستخدم Include صراحةً.
عند تغيير الـ Models (إضافة خاصية، جدول جديد، علاقة)، نحتاج أن تنعكس التغييرات على قاعدة البيانات بشكل منظم وقابل للتكرار. الـ Migration هي «نسخة» من تغيير المخطط (Schema).
| الأمر | الوظيفة |
|---|---|
Add-Migration InitialCreate | إنشاء ملف Migration من حالة الـ Models الحالية |
Update-Database | تطبيق آخر Migration (أو كل المعلّقة) على قاعدة البيانات |
ما الذي يحدث داخليًا؟
OnModelCreatingUpdate-Database يُنفّذ SQL المناسب (CREATE TABLE، ALTER، …)قاعدة عملية: بعد أي تعديل على الـ Model، أنشئ Migration جديدة باسم وصفي — مثل
AddProductDescription— ثم طبّقها. لا تعدّل قاعدة البيانات يدويًا ثم تنسى مزامنة الكود.
تُخزَّن في مجلد Migrations/ داخل المشروع. تحتوي دالتي Up (تطبيق) و Down (تراجع) — مفيدة للفريق وللنشر على خوادم متعددة بنفس المخطط.
لا يمكن الوثوق بمدخلات العميل. قد يرسل اسمًا فارغًا، سعرًا سالبًا، أو بريدًا بصيغة خاطئة. التحقق يمنع دخول بيانات فاسدة إلى قاعدة البيانات ويُرجع للعميل رسالة واضحة (400 Bad Request).
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)] | طول نص محدد |
عند وصول الطلب، ASP.NET Core يملأ ModelState بنتائج التحقق. إذا فشل:
if (!ModelState.IsValid)
return BadRequest(ModelState);
مع [ApiController]، الـ Framework قد يُرجع 400 تلقائيًا دون هذا السطر — لكن فهم ModelState مهم لفهم ما يحدث خلف الكواليس.
وضع كل المنطق داخل 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();
}
بدل أن ينشئ 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 |
استخدام 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 — خارج نطاق هذه المحاضرة حسب منهجكم الحالي.
قواعد البيانات العلائقية تربط الجداول ببعضها. EF Core يعبّر عن هذه الروابط عبر Navigation Properties و Foreign Keys.
كل سجل في الجدول 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; }
}
الأكثر شيوعًا: فئة واحدة تحتوي عدة منتجات؛ كل منتج ينتمي لفئة واحدة.
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.
مثال: طلاب ↔ مواد دراسية — كل طالب في عدة مواد، وكل مادة فيها عدة طلاب.
في 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 يحمل المفتاحين.
عند حذف فئة، ماذا يحدث لمنتجاتها؟ الخيارات تشمل:
يُضبط في OnModelCreating أو عبر 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);
}
عند وصول طلب HTTP، يمر عبر خط أنابيب Middleware — سلسلة من المكونات المدمجة قبل وصوله للـ Controller.
Request
→ Routing (تحديد Controller و Action)
→ Authentication (إن وُجدت — خارج تفصيل Roles/Claims هنا)
→ Authorization
→ Controller Action
→ Response
أمثلة على Middleware مدمج (ليس مخصصًا):
UseRouting() — تحليل المسارUseAuthentication() / UseAuthorization() — الأمانUseHttpsRedirection() — إعادة توجيه HTTPSفهم وجود هذا الخط أنابيب يفسر لماذا يُسجَّل DbContext في
Program.csولماذا يصل Controller جاهزًا بـ Service مُحقون — دون أن تكتب أنت آلية التوصيل.
بعد دراسة هذه المحاضرة وما سبقها في المختبر، أصبحت الصورة الكاملة لـ ASP.NET Core Web API في مستواكم كالتالي:
| المهارة | الوصف المختصر |
|---|---|
| Model | تعريف كيانات البيانات والتحقق منها |
| Controller | ربط HTTP ب Actions ونتائج مناسبة |
| CRUD + HTTP | تصميم RESTful endpoints صحيحة |
| Action Results | 200, 201, 204, 404, 400 … |
| EF Core | ORM، DbContext، DbSet، LINQ |
| Migration | مزامنة المخطط مع قاعدة البيانات |
| Validation | حماية جودة المدخلات |
| Service + DI | فصل المنطق وحقن التبعيات |
| Relationships | ربط الجداول 1-1، 1-N، N-N |
هذه القاعدة تُمكّنكم من بناء API كامل لإدارة مورد حقيقي (منتجات، طلاب، مواعيد…) والانتقال لاحقًا إلى مواضيع متقدمة: مصادقة تفصيلية، أنماط تصميم إضافية، نشر على Azure أو IIS، وواجهات أمامية تستهلك الـ API.
نصيحة للمراجعة: اقرأ كل قسم واطرح على نفسك: «ما دور هذا الجزء في مسار طلب واحد من Postman حتى صف في SQL؟» — إذا ربطت الطبقات ببعضها، تثبت المعلومة أكثر من الحفظ المنفصل.