الواجهات البرمجية الحدية هي ميزة مثيرة تم إدخالها في .NET 6، مصممة لتحدث ثورة في كيفية إنشاء الواجهات البرمجية.

تخيل بناء واجهات برمجية قوية برمز بحد أدنى وبدون أي أكواد مكررة – لا داعي للتعامل مع المتحكمين أو التوجيه أو الوسيطات. هذا ما تتيحه لك الواجهات البرمجية الحدية. الفكرة وراء هذه الواجهات البرمجية هي تبسيط عملية التطوير، مما يجعلها سهلة وفعالة للغاية.

في هذه المقالة، سنستكشف عالم الواجهات البرمجية الحدية في .NET 8 وسنرشدك خلال إنشاء واجهة برمجية لمكتبة كتب قابلة للتفاعل بشكل كامل. ستتعلم كيفية الحصول على كل الكتب، استرداد كتاب بواسطة معرّفه، إضافة كتب جديدة، وحتى حذف كتب. هيا بنا نبدأ.

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

المتطلبات المسبقة

قبل البدء، تأكد من تثبيت المتطلبات المسبقة التالية على جهازك:

بالإضافة إلى ذلك، يمكنك استخدام Visual Studio 2022، الذي يأتي مع دعم مدمج لـ .NET 8. ولكن في هذه المقالة، سنستخدم محرر الشفرة Visual Studio Code. إنه خفيف الوزن، سهل الاستخدام، ومتعدد المنصات.

سنستخدم Swagger UI لاختبار واجهة برمجة التطبيقات الخاصة بنا. Swagger UI هي أداة قوية تسمح لك بالتفاعل مباشرة مع واجهة برمجة التطبيقات الخاصة بك من متصفح الويب الخاص بك. توفر واجهة مستخدم وديّة لاختبار نقاط نهاية واجهة برمجة التطبيقات الخاصة بك، مما يسهل عليك اختبار وتصحيح أخطاء واجهة برمجة التطبيقات الخاصة بك.

عند إنشاء مشروع جديد، سيقوم تلقائيًا بتثبيت الحزم الضرورية وتكوين المشروع لاستخدام Swagger UI. يتضمن .NET 8 Swagger UI افتراضيًا، لذلك سواء قمت بإنشاء تطبيقك في Visual Studio أو بـ .NET، سيتم تكوين Swagger UI لديك.

قم بتشغيل تطبيقك، وسيتم فتح Swagger UI تلقائيًا في متصفحك – ولكن نظرًا لأننا نستخدم VS Code، يجب علينا النقر على رقم المنفذ في الطرفية الخاصة بنا.

يمكنك العثور على الشيفرة المصدرية لهذا المشروع على GitHub.

مقدمة حول واجهات برمجة التطبيقات الدنيا

تخيل العمل في قاعدة بيانات برمجية بها عدد كبير من نقاط النهاية، مما يجعلها كبيرة ومعقدة. تقليديًا، يتضمن بناء واجهة برمجة التطبيقات في ASP.NET Core استخدام المتحكمات، والتوجيه، والوسيط، وكمية كبيرة من شيفرة البويلربليت. ولكن هناك نهجان لبناء واجهة برمجة التطبيقات في ASP.NET Core: النهج التقليدي والنهج الدني.

النهج التقليدي مألوف لدى معظم المطورين، حيث يتضمن استخدام المتحكمات وشيفرة البنية التحتية الواسعة. أما النهج الدني، الذي تم تقديمه في .NET 6، يتيح لك إنشاء واجهات برمجة التطبيقات بشيفرة دنيا وبدون بويلربليت. يبسط هذا النهج عملية التطوير، مما يمكنك من التركيز على كتابة منطق الأعمال بدلاً من التعامل مع شيفرة البنية التحتية.

الواجهات البرمجية الدنيا خفيفة الوزن، سريعة، ومثالية لبناء واجهات برمجة تطبيقات صغيرة إلى متوسطة الحجم. إنها مثالية لعمل نماذج أولية، وبناء خدمات صغيرة، أو إنشاء واجهات برمجية بسيطة لا تتطلب الكثير من التعقيد. في هذا الكتيب، سنتعرف على عالم الواجهات البرمجية الدنيا في .NET 6 ونتعلم كيفية إنشاء واجهة برمجة تطبيقات لمكتبة كتب متكاملة من البداية.

كيفية إنشاء واجهة برمجة تطبيقات دنيا

إن إنشاء واجهة برمجة تطبيقات دنيا أمر بسيط عند استخدام dotnet CLI، حيث أن القالب الافتراضي بالفعل واجهة برمجة تطبيقات دنيا. ولكن إذا كنت تستخدم برنامج Visual Studio، ستحتاج إلى إزالة شيفرة البويلربليت التي تأتي مع قالب المشروع.

لنبدأ باستخدام dotnet CLI لإنشاء مشروع API بسيط.


dotnet new webapi  -n BookStoreApi

يعمل أمر dotnet new webapi على إنشاء مشروع API بسيط جديد باسم BookStoreApi. يحتوي هذا المشروع على الملفات والمجلدات اللازمة للبدء.

دعنا نستكشف هيكل المشروع:

  • Program.cs: نقطة الدخول للتطبيق، حيث يتم تكوين المضيف.

  • bookapi-minimal.sln: ملف الحل الذي يحتوي على المشروع.

  • bookapi-minimal.http: ملف يحتوي على طلبات HTTP عينة لاختبار API.

  • bookapi-minimal.csproj: ملف المشروع الذي يحتوي على تكوين المشروع.

  • appsettings.json: ملف التكوين الذي يخزن إعدادات التطبيق.

  • appsettings.Development.json : ملف التكوين لبيئة التطوير.

عند فتح ملف program.cs، ستلاحظ أن الكود بسيط. يحتوي ملف Program.cs على الكود التالي:


var builder = WebApplication.CreateBuilder(args);

// أضف الخدمات إلى الحاوية.
// تعرف أكثر عن تكوين Swagger/OpenAPI على https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// قم بتكوين خط أوامر طلب HTTP.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast =  Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

إذا لم تفهم الكود تمامًا بعد، لا داعي للقلق – سنغطيه بالتفصيل في الأقسام القادمة. النقطة الأساسية هي أن واجهات برمجة التطبيقات الدقيقة تتطلب كمية قليلة جدًا من الكود، وهذا هو أحد مزاياها الرئيسية.

الكود الافتراضي يقوم بإعداد واجهة برمجة تطبيقات بسيطة لتوقعات الطقس يمكنك استخدامها لاختبار إعدادك. ينشئ قائمة من توقعات الطقس ويُرجعها عندما تقوم بطلب GET إلى نقطة النهاية /weatherforecast. كما يتضمن الكود واجهة المستخدم Swagger لمساعدتك في اختبار واجهة البرمجة التطبيقية.

امنح اهتمامًا خاصًا لطريقة app.MapGet، التي تعيّن مسارًا لدالة معالجة. في هذه الحالة، تعيّن مسار /weatherforecast إلى دالة تُرجع قائمة من توقعات الطقس. سنستخدم أساليب مماثلة لإنشاء نقاط نهاية خاصة بنا في الأقسام القادمة.

قبل أن نبدأ في إنشاء هيكل مجلد مشروعنا، دعنا نفهم الأساليب الHTTP في كل من المنهج القائم على التحكم وواجهات برمجة التطبيقات الدقيقة.

الأساليب الHTTP في المنهج القائم على التحكم وواجهات برمجة التطبيقات الدقيقة

في المنهج القائم على التحكم، الذي هو الطريقة التقليدية لإنشاء واجهات برمجة تطبيقات الويب، تحتاج إلى إنشاء فئة تحكم وتعريف أساليب لكل طريقة HTTP. على سبيل المثال:

  • لإنشاء طريقة GET، تستخدم السمة [HttpGet].

  • لإنشاء طريقة POST، تستخدم السمة [HttpPost].

  • لإنشاء طريقة PUT، تستخدم السمة [HttpPut].

  • لإنشاء طريقة DELETE، تستخدم السمة [HttpDelete].

هذه هي كيفية إنشاء نقاط النهاية في نهج قائم على الوحدات التحكم.

على النقيض من ذلك، تستخدم واجهات برمجة تطبيقات مصغرة طرقًا مثل app.MapGet، app.MapPost، app.MapPut، و app.MapDelete لإنشاء نقاط النهاية. هذه هي الفارق الرئيسي بين النهجين: تستخدم واجهات برمجة التطبيقات القائمة على الوحدات التحكم السمات لتحديد نقاط النهاية، بينما تستخدم واجهات برمجة التطبيقات مصغرة الطرق.

الآن بعد فهمك لكيفية التعامل مع طلبات HTTP في الوحدات التحكم وواجهات برمجة التطبيقات مصغرة، لنقم بإنشاء بنية مجلد مشروعنا.

قبل أن نقوم بإنشاء هيكل مجلد مشروعنا، دعنا نشغل ما لدينا أولاً. كما تعلمنا سابقًا، عند إنشاء مشروع باستخدام Visual Studio أو .NET CLI، يأتي بمشروع افتراضي لتوقعات الطقس يمكننا تشغيله ورؤيته على واجهة المستخدم. دعنا نشغله للتأكد من أن كل شيء يعمل قبل أن نواصل في إنشاء هيكل مجلد مشروعنا.

قم بتشغيل هذا الأمر:


dotnet run

يجب أن ترى الناتج التالي:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5228
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Devolopemnt\Dotnet\bookapi-minimal

هذا يعني أن التطبيق يعمل ويستمع على http://localhost:5228. كما ذكرت أعلاه، نظرًا لأننا نستخدم dotnet CLI و Visual Studio Code، فإن التطبيق لن يفتح المتصفح تلقائيًا بالنسبة لنا. علينا القيام بذلك يدويًا.

افتح متصفحك وانتقل إلى http://localhost:5228/swagger/index.html لرؤية الاستجابة الافتراضية من الواجهة البرمجية للتطبيق.

يجب أن ترى شيئًا مشابهًا لهذا:

الآن الخطوة التالية بالنسبة لنا هي إيجاد طريقة لهيكلة مشروعنا وإنشاء الملفات والمجلدات اللازمة للبدء.

ملفات مشروع Minimal API

لتنظيم مشروعنا، سنقوم بإنشاء تسلسل هرمي للمجلدات. سيساعدنا هذا في الحفاظ على رمزنا نظيفًا وقابلًا للصيانة. إليك هيكل المجلدات الذي سنستخدمه:

  • AppContext: يحتوي على سياق قاعدة البيانات والتكوينات ذات الصلة.

  • التكوينات: تحتوي على تكوينات Entity Framework Core وبيانات بذور لقاعدة البيانات.

  • العقود: تحتوي على كائنات نقل البيانات (DTOs) المستخدمة في تطبيقنا.

  • نقاط النهاية: حيث نحدد ونكونفجر نقاط نهاية واجهة برمجة التطبيقات الخاصة بنا.

  • الاستثناءات: تحتوي على فئات استثناء مخصصة تستخدم في المشروع.

  • الامتدادات: تحتوي على طرق امتداد سنستخدمها في جميع أنحاء المشروع.

  • النماذج: تحتوي على نماذج منطق الأعمال.

  • الخدمات: تحتوي على فئات خدمات تنفذ منطق الأعمال.

  • الواجهات: تحتوي على تعريفات واجهة تستخدم لرسم خريطة خدماتنا.

في Visual Studio Code، يمكنك إنشاء هيكل المجلدات هذا على النحو التالي:

- AppContext
- Configurations
- Contracts
- Endpoints
- Exceptions
- Extensions
- Models
- Services
- Interfaces

بعد الإعداد، يجب أن يبدو هيكل مجلد مشروعك على النحو التالي:

الآن بعد أن تم إعداد هيكل مشروعنا يمكننا المتابعة والبدء في كتابة الشفرة. لنبدأ بإنشاء نماذجنا.

كيفية إنشاء النماذج

في هذا القسم، سنقوم بإنشاء نماذج لتطبيقنا. النماذج هي العناصر الأساسية في تطبيقنا، تمثل البيانات التي سيعمل تطبيقنا معها. لمثالنا، سنقوم بإنشاء نموذج لكتاب.

للبدء، قم بإنشاء مجلد يحمل اسم Models في دليل مشروعك. داخل هذا المجلد، قم بإنشاء ملف يحمل اسم BookModel.cs وأضف الشيفرة التالية:

// Models/BookModel.cs


namespace bookapi_minimal.Models
{
    public class BookModel
    {
        public Guid Id { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public string Language { get; set; }
        public int TotalPages { get; set; }
    }
}

هذا الفصيل BookModel class يحدد الخصائص التي تمثل تفاصيل كتاب، مثل العنوان، المؤلف، الوصف، الفئة، اللغة، وإجمالي الصفحات. كل خاصية مصممة لاحتواء معلومات محددة حول الكتاب، مما يجعل من السهل إدارة وتلاعب البيانات الخاصة بالكتاب ضمن تطبيقنا.

الآن بعد أن قمنا بإنشاء نموذجنا، لنقم بإنشاء سياق قاعدة البيانات الخاص بنا.

كيفية إنشاء سياق قاعدة البيانات

سياق قاعدة البيانات هو فئة تمثل جلسة مع قاعدة البيانات. إنها مسؤولة عن التفاعل مع قاعدة البيانات وتنفيذ عمليات قاعدة البيانات. في تطبيقنا، سنستخدم Entity Framework Core للتفاعل مع قاعدة البيانات.

قم بتثبيت الحزم المطلوبة

قبل إنشاء سياق قاعدة البيانات الخاصة بنا، نحتاج إلى تثبيت الحزم التالية:

يمكنك تثبيت هذه الحزم باستخدام الأوامر التالية:

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package FluentValidation.DependencyInjectionExtensions

التحقق من تثبيت الحزمة

للتحقق من تثبيت الحزم، افتح ملف bookapi-minimal.csproj في دليل مشروعك الرئيسي. يجب أن ترى الحزم المثبتة مدرجة على النحو التالي:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <RootNamespace>bookapi_minimal</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
   <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>

هذا يؤكد أن الحزم قد تم تثبيتها بنجاح.

الآن دعنا ننشئ سياق قاعدة البيانات الخاص بنا.

في مجلد AppContext، قم بإنشاء ملف جديد بالاسم ApplicationContext.cs وأضف الكود التالي:

// AppContext/ApplicationContext.cs

using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;

namespace bookapi_minimal.AppContext
{

    public class ApplicationContext(DbContextOptions<ApplicationContext> options) : DbContext(options)
    {

        // Default schema for the database context
        private const string DefaultSchema = "bookapi";


       // DbSet to represent the collection of books in our database
        public DbSet<BookModel> Books { get; set; }

        // Constructor to configure the database context

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.HasDefaultSchema(DefaultSchema);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationContext).Assembly);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationContext).Assembly);

        }

    }
}

لنقوم بتفكيك الكود أعلاه:

  • نقوم بتعريف فئة بالاسم ApplicationContext ترث من DbContext. فئة DbContext هي جزء من Entity Framework Core وتمثل جلسة مع قاعدة البيانات.

  • يقبل المُنشئ نسخة من DbContextOptions<ApplicationContext>. يُستخدم هذا المُنشئ لتكوين خيارات سياق قاعدة البيانات.

  • نقوم بتعريف خاصية بالاسم Books من نوع DbSet<BookModel>. تمثل هذه الخاصية مجموعة الكتب في قاعدة البيانات الخاصة بنا.

  • نعدل على الطريقة OnModelCreating لتكوين مخطط قاعدة البيانات وتطبيق أي تكوينات محددة في تطبيقنا.

الآن بعد أن قمنا بإنشاء سياق قاعدة البيانات الخاص بنا، دعنا نقوم بإنشاء طريقة التمديد وتسجيل سياق قاعدة البيانات الخاص بنا في حاوية حقن التبعية.

إنشاء طريقة تمديد

قبل أن نقوم بإنشاء طريقة التمديد، دعنا نفهم ما هي طريقة التمديد في سياق ASP.NET Core.

طريقة التمديد هي طريقة ثابتة تضيف وظائف جديدة إلى نوع موجود دون تعديل النوع الأصلي. في ASP.NET Core، تُستخدم طرق التمديد بشكل شائع لتوسيع وظائف واجهة IServiceCollection، التي تُستخدم لتسجيل الخدمات في حاوية حقن التبعية.

الخدمات هي مكونات توفر وظائف للتطبيق، مثل الوصول إلى قاعدة البيانات وتسجيل الأحداث، والتكوين. من خلال إنشاء طريقة تمديد لواجهة IServiceCollection، يمكنك تبسيط عملية تسجيل الخدمات في حاوية حقن التبعية.

بدلاً من وضع كل شيء في ملف Program.cs، سنقوم بإنشاء طريقة تمديد لتسجيل خدماتنا في حاوية حقن الإعتماد. سيساعدنا ذلك في الحفاظ على نظافة وتنظيم شفرتنا.

في مجلد Extensions، قم بإنشاء ملف جديد بإسم ServiceExtensions.cs وأضف الشفرة التالية:

using System.Reflection;
using bookapi_minimal.AppContext;
using FluentValidation;
using Microsoft.EntityFrameworkCore;

namespace bookapi_minimal.Extensions
{
    public static class ServiceExtensions
    {
        public static void AddApplicationServices(this IHostApplicationBuilder builder)
        {
            if (builder == null) throw new ArgumentNullException(nameof(builder));
            if (builder.Configuration == null) throw new ArgumentNullException(nameof(builder.Configuration));

            // إضافة سياق قاعدة البيانات
            builder.Services.AddDbContext<ApplicationContext>(configure =>
            {
                configure.UseSqlServer(builder.Configuration.GetConnectionString("sqlConnection"));
            });

            // إضافة محققين من التجميع الحالي
            builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
        }
    }
}

لنقم بتفكيك الشفرة أعلاه:

  • نقوم بتعريف فئة ثابتة بإسم ServiceExtensions تحتوي على طريقة تمديد بإسم AddApplicationServices. تمتد هذه الطريقة من واجهة IHostApplicationBuilder التي تُستخدم لتكوين خط معالجة طلبات التطبيق.

  • تقبل طريقة AddApplicationServices مثيلًا من IHostApplicationBuilder كمعلمة. يُستخدم هذا المعلمة للوصول إلى تكوين التطبيق والخدمات.

  • نضيف ApplicationContext إلى حاوية حقن التبعيات ونقوم بتكوينها لاستخدام SQL Server كمزود لقاعدة البيانات. نسترجع سلسلة الاتصال من ملف appsettings.json باستخدام طريقة GetConnectionString.

  • نضيف validators من التجميع الحالي باستخدام الطريقة AddValidatorsFromAssembly. تقوم هذه الطريقة بفحص التجميع الحالي للصفوف التي تنفذ واجهة IValidator وتسجلها في حاوية حقن التبعيات.

بعد ذلك، نحتاج إلى إضافة سلسلة الاتصال إلى ملف appsettings.json. أضف الكود التالي إلى ملف appsettings.json الخاص بك:

{ 
     "ConnectionStrings": {
    "sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
  }
  }

تأكد من استبدال your_password بكلمة مرور SQL Server الفعلية.

يجب أن يبدو ملف appsettings.json الخاص بك كالتالي:


{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
  },
  "AllowedHosts": "*"
}

تهانينا! لقد قمت بإنشاء سياق قاعدة البيانات، والطريقة الموسعة، وسلسلة الاتصال بنجاح لتطبيقك. في القسم التالي، سنقوم بإنشاء عقد.

كيفية إنشاء عقد

العقود هي كائنات نقل البيانات (DTOs) التي تحدد هيكل البيانات المتبادلة بين العميل والخادم. في تطبيقنا، سنقوم بإنشاء عقود لتمثيل البيانات المرسلة والمستلمة من نقاط نهاية واجهة برمجة التطبيقات API الخاصة بنا.

فيما يلي العقود التي سنقوم بإنشائها:

  • CreateBookRequest: يمثل البيانات المرسلة عند إنشاء كتاب جديد.

  • UpdateBookRequest: يمثل البيانات المرسلة عند تحديث كتاب موجود.

  • BookResponse: يمثل البيانات المُرجعة عند استرداد كتاب.

  • ErrorResponse: يمثل الاستجابة بالخطأ المرجعة عند حدوث استثناء.

  • ApiResponse: يمثل الاستجابة المرجعة بواسطة واجهة برمجة التطبيقات API.

في مجلد العقود، قم بإنشاء ملف جديد باسم CreateBookRequest وأضف الكود التالي:

// Contracts/CreateBookRequest.cs

namespace bookapi_minimal.Contracts
{

    public record CreateBookRequest
    { 

        public string Title { get; init; }
        public string Author { get; init; }
        public string Description { get; init; }
        public string Category { get; init; }
        public string Language { get; init; }
        public int TotalPages { get; init; }
    }
}

في مجلد العقود، قم بإنشاء ملف جديد باسم UpdateBookRequest وأضف الكود التالي:


// عقود/UpdateBookRequest.cs

namespace bookapi_minimal.Contracts
{

    public record UpdateBookRequest
    {
       public string Title { get; set; }
        public string Author { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public string Language { get; set; }
        public int TotalPages { get; set; }

    }
}

في مجلد عقود، أنشئ ملفًا جديدًا ب اسم BookResponse وأضف الكود التالي:

// عقود/BookResponse.cs
namespace bookapi_minimal.Contracts
{

    public record BookResponse
    {
        public Guid Id { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public string Language { get; set; }
        public int TotalPages { get; set; }
    }
}

في مجلد عقود، أنشئ ملفًا جديدًا ب اسم ErrorResponse وأضف الكود التالي:



// عقود/ErrorResponse.cs
namespace bookapi_minimal.Contracts
{

        public record ErrorResponse
    {
        public string Title { get; set; }
        public int StatusCode { get; set; }
        public string Message { get; set; }

    }

}

في مجلد عقود، أنشئ ملفًا جديدًا ب اسم ApiResponse وأضف الكود التالي:

// عقود/ApiResponse.cs
namespace bookapi_minimal.Contracts
{

    public class ApiResponse<T>
    {
        public T Data { get; set; }
        public string Message { get; set; }

        public ApiResponse(T data, string message)
        {
            Data = data;
            Message = message;
        }
    }
}

هذه العقود تساعدنا في تحديد هيكل البيانات المتبادلة بين العميل والخادم، مما يجعل من السهل التعامل مع البيانات في تطبيقنا.

في القسم التالي، سننشئ خدمات لتنفيذ منطق الأعمال في تطبيقنا.

كيفية إضافة الخدمات

الخدمات هي مكونات توفر وظائف لتطبيق. في تطبيقنا، سننشئ خدمات لتنفيذ منطق الأعمال في تطبيقنا. سننشئ خدمات للتعامل مع عمليات CRUD للكتب، وللتحقق من بيانات الكتب، وللتعامل مع الاستثناءات.

في ASP.NET Core، يتم تسجيل الخدمات في حاوية حقن الإعتماد ويمكن حقنها في مكونات أخرى، مثل التحكمات ونقاط النهاية، ولكن هذا API أساسي لذلك سنقوم بحقن الخدمات مباشرة في نقاط النهاية.

دعونا ننشئ واجهة لخدماتنا. في مجلد Interfaces، قم بإنشاء ملف جديد ب اسم IBookService.cs وأضف الكود التالي:

 // Interfaces/IBookService.cs



using bookapi_minimal.Contracts;

namespace bookapi_minimal.Interfaces
{
      public interface IBookService
    {
        Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest);
        Task<BookResponse> GetBookByIdAsync(Guid id);
        Task<IEnumerable<BookResponse>> GetBooksAsync();
        Task<BookResponse> UpdateBookAsync(Guid id,  UpdateBookRequest  updateBookRequest);
        Task<bool> DeleteBookAsync(Guid id);
    }
}

لنقسم الكود أعلاه: لقد قمنا بتحديد واجهة تسمى IBookService تحتوي على طرق للتعامل مع عمليات CRUD للكتب. الواجهة تحدد الطرق التالية:

  • AddBookAsync: يضيف كتاب جديد إلى قاعدة البيانات.

  • GetBookByIdAsync: يسترجع كتاب بواسطة معرفه.

  • GetBooksAsync: يسترجع جميع الكتب من قاعدة البيانات.

  • UpdateBookAsync: يحدث كتاب موجود.

نحن نستخدم العقد الذي أنشأناه سابقًا في المجلد Contracts. تحدد واجهة IBookService هيكل الطرق التي ستنفذها فئات الخدمة. هذا يساعدنا في فصل الواجهة عن التنفيذ، مما يجعل من السهل صيانة واختبار الكود الخاص بنا.

الآن بعد أن قمنا بإنشاء الواجهة، دعونا ننشئ فئة الخدمة التي تنفذ الواجهة.

كيفية تنفيذ خدمة الكتاب

هذه الخدمة ستنفذ واجهة IBookService وتوفر المنطق التجاري لتطبيقنا. في مجلد Services، قم بإنشاء ملف جديد بالاسم BookService.cs. يجب أن يبدو الملف الأولي الخاص بك كالتالي:


// Services/BookService.cs

namespace bookapi_minimal.Services
{
    public class BookService
    {

    }
}

أول شيء نحتاج إلى القيام به هو إضافة الواجهة إلى فئة BookService. قم بتحديث فئة BookService لتنفيذ واجهة IBookService على النحو التالي:



// Services/BookService.cs



using bookapi_minimal.Interfaces;

namespace bookapi_minimal.Services
{
    public class BookService:IBookService
    {

    }
}

عند القيام بذلك، قد يظهر لك خطأ في VS Code لأننا لم نقم بتنفيذ الأساليب في الواجهة. لنقم بتنفيذ الأساليب في فئة BookService.

في VS Code يمكنك استخدام الاختصار Ctrl + . لتنفيذ الأساليب في الواجهة. سترى بعد ذلك الكود التالي الذي تم إنشاؤه لك:


using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;

namespace bookapi_minimal.Services
{
     // فئة الخدمة لإدارة الكتب
   public class BookService : IBookService
   {
       // الطريقة لإضافة كتاب جديد إلى قاعدة البيانات
       public Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
       {
           throw new NotImplementedException();
       }

      // الطريقة لحذف كتاب من قاعدة البيانات
       public Task<bool> DeleteBookAsync(Guid id)
       {
           throw new NotImplementedException();
       }

       // الطريقة للحصول على كتاب من قاعدة البيانات بواسطة معرفه

       public Task<BookResponse> GetBookByIdAsync(Guid id)
       {
           throw new NotImplementedException();
       }

      // الطريقة للحصول على جميع الكتب من قاعدة البيانات
       public Task<IEnumerable<BookResponse>> GetBooksAsync()
       {
           throw new NotImplementedException();
       }

       // الطريقة لتحديث كتاب في قاعدة البيانات
       public Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest updateBookRequest)
       {
           throw new NotImplementedException();
       }
   }
}

الآن يمكنك رؤية أن الأساليب في الواجهة تم تنفيذها في فئة BookService. سنقوم بتنفيذ منطق الأعمال لكل طريقة في الجزء التالي.

قبل أن نقوم بذلك، دعنا نضيف التبعيات اللازمة إلى فئة BookService. نحتاج إلى حقن تبعيات ApplicationContext و ILogger إلى فئة BookService. ApplicationContext تستخدم للتفاعل مع قاعدة البيانات، بينما ILogger تستخدم للتسجيل.

لحقن التبعيات، قم بتحديث فئة BookService كما يلي:


// Services/BookService.cs

// ...
 private readonly ApplicationContext _context; // سياق قاعدة البيانات
  private readonly ILogger<BookService> _logger; // سجل لتسجيل المعلومات والأخطاء

//..

نظرًا لأننا قمنا بإضافة التبعيات، نحتاج إلى تحديث مُنشئ BookService لقبول التبعيات. قم بتحديث مُنشئ BookService كما يلي:


// Services/BookService.cs

// ...

  // مُنشئ لتهيئة سياق قاعدة البيانات والسجل
 public BookService(ApplicationContext context, ILogger<BookService> logger)
 {
            _context = context;
            _logger = logger;
}

// ...

الآن بعد أن قمنا بإضافة التبعيات وتحديث المُنشئ، يمكننا تنفيذ منطق الأعمال لكل طريقة في فئة BookService.

لنقم بإنشاء البنية التحتية لعمليات الإنشاء والقراءة والتحديث والحذف في فئة BookService.

كيفية تنفيذ طريقة AddBookAsync

كما ذكرت سابقًا، سنستخدم طريقة AddBookAsync لإضافة كتاب جديد إلى قاعدة البيانات. في هذه الطريقة، سنقوم بإنشاء كيان كتاب جديد، ونقوم بتعيين البيانات من كائن CreateBookRequest إلى كيان الكتاب، وحفظ كيان الكتاب في قاعدة البيانات. كما سنقوم بإرجاع كيان الكتاب ككائن BookResponse.

قم بتحديث طريقة AddBookAsync في فئة BookService على النحو التالي:

// Services/BookService.cs

// ...
 /// <summary>
        /// إضافة كتاب جديد
        /// </summary>
        /// <param name="createBookRequest">طلب إضافة الكتاب</param>
        /// <returns>تفاصيل الكتاب المنشأ</returns>
        public async Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
        {
            try
            {
                var book = new BookModel
                {
                    Title = createBookRequest.Title,
                    Author = createBookRequest.Author,
                    Description = createBookRequest.Description,
                    Category = createBookRequest.Category,
                    Language = createBookRequest.Language,
                    TotalPages = createBookRequest.TotalPages
                };

                // إضافة الكتاب إلى قاعدة البيانات
                _context.Books.Add(book);
                await _context.SaveChangesAsync();
                _logger.LogInformation("Book added successfully.");

                // إرجاع تفاصيل الكتاب المنشأ
                return new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error adding book: {ex.Message}");
                throw;
            }
        }
// ...

في هذا الكود، نقوم بإنشاء كيان كتاب جديد من كائن CreateBookRequest، نقوم بتعيين البيانات من كائن CreateBookRequest إلى كيان الكتاب، ثم نقوم بحفظ كيان الكتاب في قاعدة البيانات، ونعيد كيان الكتاب ككائن BookResponse.

نقوم أيضًا بتسجيل المعلومات والأخطاء باستخدام وحدة الاعتماد ILogger. إذا حدث استثناء أثناء العملية، نقوم بتسجيل رسالة الخطأ وإعادة رمي الاستثناء.

الآن بعد أن قمنا بتنفيذ الطريقة AddBookAsync، لنقم بتنفيذ الطريقة GetBookByIdAsync.

كيفية تنفيذ الطريقة GetBookByIdAsync

تُستخدم الطريقة GetBookByIdAsync لاسترداد كتاب بواسطة معرفه من قاعدة البيانات. في هذه الطريقة، سنستعلم قاعدة البيانات عن الكتاب بالمعرف المحدد، ونعيد كيان الكتاب ككائن BookResponse.

قم بتحديث الطريقة GetBookByIdAsync في فئة BookService كما يلي:


// Services/BookService.cs

//... 

    /// <summary>
        /// الحصول على كتاب بواسطة معرفه
        /// </summary>
        /// <param name="id">معرف الكتاب</param>
        /// <returns>تفاصيل الكتاب</returns>
        public async Task<BookResponse>  GetBookByIdAsync(Guid id)
        {
            try
            {
                // العثور على الكتاب بواسطة معرفه
                var book = await _context.Books.FindAsync(id);
                if (book == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return null;
                }

                // إعادة تفاصيل الكتاب
                return new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error retrieving book: {ex.Message}");
                throw;
            }
        }

//...

في هذا الكود، نقوم بالاستعلام عن قاعدة البيانات للكتاب بالمعرف المحدد، وربط كيان الكتاب بكائن BookResponse، وإرجاع كائن BookResponse. كما نقوم بتسجيل المعلومات والأخطاء باستخدام تبعية ILogger.

إذا لم يتم العثور على الكتاب بالمعرف المحدد، فإننا نقوم بتسجيل رسالة تحذير وإرجاع قيمة فارغة. إذا حدث استثناء خلال العملية، نقوم بتسجيل رسالة الخطأ وإعادة رمي الاستثناء.

الآن بعد أن قمنا بتنفيذ طريقة GetBookByIdAsync، دعونا ننفذ طريقة GetBooksAsync.

كيفية تنفيذ طريقة GetBooksAsync

يتم استخدام طريقة GetBooksAsync لاسترداد جميع الكتب من قاعدة البيانات. في هذه الطريقة، سنستعلم قاعدة البيانات للحصول على جميع الكتب، نقوم بتعيين كل كيان كتاب إلى كائن BookResponse، ونعيد قائمة من كوائن BookResponse.

قم بتحديث الطريقة GetBooksAsync في فئة BookService على النحو التالي:



// Services/BookService.cs

//... 


  /// <summary>
        /// الحصول على جميع الكتب
        /// </summary>
        /// <returns>قائمة بجميع الكتب</returns>
        public async Task<IEnumerable<BookResponse>> GetBooksAsync()
        {
            try
            {
                // الحصول على جميع الكتب من قاعدة البيانات
                var books = await _context.Books.ToListAsync();

                // إعادة تفاصيل جميع الكتب
                return books.Select(book => new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                });
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error retrieving books: {ex.Message}");
                throw;
            }
        }
//...

هنا، نقوم باستعلام قاعدة البيانات للحصول على جميع الكتب، وتعيين كل كيان كتاب إلى كائن BookResponse، وإعادة قائمة من كوائن BookResponse. كما نقوم بتسجيل المعلومات والأخطاء باستخدام تبعية ILogger. إذا حدث استثناء أثناء العملية، نسجل رسالة الخطأ وإعادة رمي الاستثناء.

الآن بعد تنفيذ طريقة GetBooksAsync، دعنا ننفذ الطريقة UpdateBookAsync.

كيفية تنفيذ الطريقة UpdateBookAsync

يتم استخدام طريقة UpdateBookAsync لتحديث كتاب موجود في قاعدة البيانات. في هذه الطريقة، سنستعلم قاعدة البيانات عن الكتاب بالمعرف المحدد، ونحدث كيان الكتاب بالبيانات من كائن UpdateBookRequest، ثم نحفظ كيان الكتاب المحدث في قاعدة البيانات، ونعيد كيان الكتاب المحدث ككائن BookResponse.

قم بتحديث طريقة UpdateBookAsync في فئة BookService كما يلي:

// Services/BookService.cs
 //...
 /// <summary>
        /// تحديث كتاب موجود
        /// </summary>
        /// <param name="id">معرف الكتاب الذي سيتم تحديثه</param>
        /// <param name="book">نموذج الكتاب المحدث</param>
        /// <returns>تفاصيل الكتاب المحدث</returns>
        public async Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest book)
        {
            try
            {
                // العثور على الكتاب الموجود بواسطة معرفه
                var existingBook = await _context.Books.FindAsync(id);
                if (existingBook == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return null;
                }

                // تحديث تفاصيل الكتاب
                existingBook.Title = book.Title;
                existingBook.Author = book.Author;
                existingBook.Description = book.Description;
                existingBook.Category = book.Category;
                existingBook.Language = book.Language;
                existingBook.TotalPages = book.TotalPages;

                // حفظ التغييرات في قاعدة البيانات
                await _context.SaveChangesAsync();
                _logger.LogInformation("Book updated successfully.");

                // إعادة تفاصيل الكتاب المحدث
                return new BookResponse
                {
                    Id = existingBook.Id,
                    Title = existingBook.Title,
                    Author = existingBook.Author,
                    Description = existingBook.Description,
                    Category = existingBook.Category,
                    Language = existingBook.Language,
                    TotalPages = existingBook.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error updating book: {ex.Message}");
                throw;
            }
        }
//...

هنا، نقوم بالاستعلام عن قاعدة البيانات للكتاب بالمعرف المحدد، ثم نقوم بتحديث كيان الكتاب باستخدام البيانات من كائن UpdateBookRequest، وحفظ كيان الكتاب المحدث في قاعدة البيانات، وإرجاع كيان الكتاب المحدث ككائن BookResponse. كما نقوم بتسجيل معلومات وأخطاء باستخدام تبعية ILogger.

إذا لم يتم العثور على الكتاب بالمعرف المحدد، نقوم بتسجيل رسالة تحذير وإرجاع قيمة فارغة. إذا حدث استثناء أثناء العملية، نقوم بتسجيل رسالة الخطأ وإعادة الاستثناء.

الآن بعد تنفيذ طريقة UpdateBookAsync، دعنا ننفذ طريقة DeleteBookAsync.

كيفية تنفيذ طريقة DeleteBookAsync

تُستخدم طريقة DeleteBookAsync لحذف كتاب موجود من قاعدة البيانات. في هذه الطريقة، سنستعلم قاعدة البيانات عن الكتاب بالمعرف المحدد، ونقوم بإزالة كيان الكتاب من قاعدة البيانات، ونُعيد قيمة بوليانية تشير إلى ما إذا تم حذف الكتاب بنجاح.

قم بتحديث طريقة DeleteBookAsync في فئة BookService على النحو التالي:

// Services/BookService.cs

 //...


/// <summary>
        /// حذف كتاب بواسطة معرفه
        /// </summary>
        /// <param name="id">معرف الكتاب الذي سيتم حذفه</param>
        /// <returns>صحيح إذا تم حذف الكتاب، خطأ في حال عدم النجاح</returns>
        public async Task<bool> DeleteBookAsync(Guid id)
        {
            try
            {
                // العثور على الكتاب بواسطة معرفه
                var book = await _context.Books.FindAsync(id);
                if (book == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return false;
                }

                // إزالة الكتاب من قاعدة البيانات
                _context.Books.Remove(book);
                await _context.SaveChangesAsync();
                _logger.LogInformation($"Book with ID {id} deleted successfully.");
                return true;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error deleting book: {ex.Message}");
                throw;
            }
        }
//...

في هذا الكود، نقوم بالاستعلام عن قاعدة البيانات للكتاب بالمعرف المحدد، ثم نقوم بإزالة كيان الكتاب من قاعدة البيانات، ونعيد قيمة بوليانية تشير ما إذا تم حذف الكتاب بنجاح. كما نقوم بتسجيل معلومات وأخطاء باستخدام تبعية ILogger.

إذا لم يتم العثور على الكتاب بالمعرف المحدد، فإننا نسجل رسالة تحذير ونقوم بإرجاع قيمة خطأ. إذا حدث استثناء أثناء العملية، فإننا نسجل رسالة الخطأ ونعيد الاستثناء.

الآن لقد نفذت بنجاح منطق العمل لأساليب AddBookAsync, GetBookByIdAsync, GetBooksAsync, UpdateBookAsync, و DeleteBookAsync في فئة BookService. تتعامل هذه الأساليب مع عمليات CRUD للكتب، تقوم بالتحقق من صحة بيانات الكتاب، وتتعامل مع الاستثناءات. حتى الآن، يجب أن تبدو فئة BookService الخاصة بك بهذا الشكل:



using bookapi_minimal.AppContext;
using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;
using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;

namespace bookapi_minimal.Services
{
    public class BookService : IBookService
    {
          private readonly ApplicationContext _context; // سياق قاعدة البيانات
        private readonly ILogger<BookService> _logger; // مسجل لتسجيل المعلومات والأخطاء
          // مُعمل لتهيئة سياق قاعدة البيانات والمسجل
        public BookService(ApplicationContext context, ILogger<BookService> logger)
        {
            _context = context;
            _logger = logger;
        }

           /// إضافة كتاب جديد
        /// </ملخص>
        /// <param name="createBookRequest">طلب الكتاب الذي سيتم إضافته</param>
        /// <returns>تفاصيل الكتاب الذي تم إنشاؤه</returns>
        public async Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
        {
            try
            {
                var book = new BookModel
                {
                    Title = createBookRequest.Title,
                    Author = createBookRequest.Author,
                    Description = createBookRequest.Description,
                    Category = createBookRequest.Category,
                    Language = createBookRequest.Language,
                    TotalPages = createBookRequest.TotalPages
                };

                // إضافة الكتاب إلى قاعدة البيانات
                _context.Books.Add(book);
                await _context.SaveChangesAsync();
                _logger.LogInformation("Book added successfully.");

                // إرجاع تفاصيل الكتاب الذي تم إنشاؤه
                return new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error adding book: {ex.Message}");
                throw;
            }
        }

          /// <ملخص>
        /// الحصول على كتاب بواسطة معرفه
        /// </ملخص>
        /// <param name="id">معرف الكتاب</param>
        /// <returns>تفاصيل الكتاب</returns>
        public async Task<BookResponse>  GetBookByIdAsync(Guid id)
        {
            try
            {
                // العثور على الكتاب بواسطة معرفه
                var book = await _context.Books.FindAsync(id);
                if (book == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return null;
                }

                // إرجاع تفاصيل الكتاب
                return new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error retrieving book: {ex.Message}");
                throw;
            }
        }



  /// <ملخص>
        /// الحصول على جميع الكتب
        /// </ملخص>
        /// <returns>قائمة بجميع الكتب</returns>
        public async Task<IEnumerable<BookResponse>> GetBooksAsync()
        {
            try
            {
                // الحصول على جميع الكتب من قاعدة البيانات
                var books = await _context.Books.ToListAsync();

                // إرجاع تفاصيل جميع الكتب
                return books.Select(book => new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                });
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error retrieving books: {ex.Message}");
                throw;
            }
        }


         /// <ملخص>
        /// تحديث كتاب موجود
        /// </ملخص>
        /// <param name="id">معرف الكتاب الذي سيتم تحديثه</param>
        /// <param name="book">نموذج الكتاب المحدث</param>
        /// <returns>تفاصيل الكتاب المحدثة</returns>
        public async Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest book)
        {
            try
            {
                // العثور على الكتاب الموجود بواسطة معرفه
                var existingBook = await _context.Books.FindAsync(id);
                if (existingBook == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return null;
                }

                // تحديث تفاصيل الكتاب
                existingBook.Title = book.Title;
                existingBook.Author = book.Author;
                existingBook.Description = book.Description;
                existingBook.Category = book.Category;
                existingBook.Language = book.Language;
                existingBook.TotalPages = book.TotalPages;

                // حفظ التغييرات في قاعدة البيانات
                await _context.SaveChangesAsync();
                _logger.LogInformation("Book updated successfully.");

                // إرجاع تفاصيل الكتاب المحدثة
                return new BookResponse
                {
                    Id = existingBook.Id,
                    Title = existingBook.Title,
                    Author = existingBook.Author,
                    Description = existingBook.Description,
                    Category = existingBook.Category,
                    Language = existingBook.Language,
                    TotalPages = existingBook.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error updating book: {ex.Message}");
                throw;
            }
        }



        /// <ملخص>
        /// حذف كتاب بواسطة معرفه
        /// </ملخص>
        /// <param name="id">معرف الكتاب الذي سيتم حذفه</param>
        /// <returns>صحيح إذا تم حذف الكتاب، خطأ في حالة عدم الحذف</returns>
        public async Task<bool> DeleteBookAsync(Guid id)
        {
            try
            {
                // العثور على الكتاب بواسطة معرفه
                var book = await _context.Books.FindAsync(id);
                if (book == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return false;
                }

                // إزالة الكتاب من قاعدة البيانات
                _context.Books.Remove(book);
                await _context.SaveChangesAsync();
                _logger.LogInformation($"Book with ID {id} deleted successfully.");
                return true;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error deleting book: {ex.Message}");
                throw;
            }
        }

    }
}

تهانينا! لقد نفذت بنجاح منطق الأعمال لأساليب AddBookAsync, GetBookByIdAsync, GetBooksAsync, UpdateBookAsync, و DeleteBookAsync في فئة BookService.

هناك شيء واحد علينا القيام به: نحتاج إلى تسجيل الخدمة في طريقة التمديد الخاصة بنا. لنقم بذلك.

في ملف ServiceExtensions.cs الخاص بك، أضف الشيفرة التالية:


// Extensions/ServiceExtensions.cs

//..

 builder.Services.AddScoped<IBookService, BookService>();
//...

سيقوم هذا بتسجيل فئة BookService كخدمة مدى الحياة القصيرة. وهذا يعني أن الخدمة ستنشأ مرة واحدة لكل طلب وتتم التخلص منها بعد اكتمال الطلب.

الآن بعد أن لدينا الخدمة تعمل، لنقم بإنشاء فئات الاستثناءات.

كيفية إنشاء الاستثناءات

يعد التعامل السليم مع الاستثناءات أمرًا حاسمًا لضمان استقرار وموثوقية التطبيق. في سياق ASP.NET Core، هناك نوعان رئيسيان من الاستثناءات:

  • استثناءات النظام: وهذه هي الاستثناءات التي يتم رميها بواسطة .NET runtime أو النظام الأساسي.

  • استثناءات التطبيق: وهذه هي الاستثناءات التي يتم رميها بواسطة كود التطبيق للتعامل مع أخطاء أو حالات معينة.

في ASP.NET Core مع .NET 8، تم تقديم ميزة جديدة تسمى التعامل الاستثنائي العالمي. تتيح هذه الميزة لك التعامل مع الاستثناءات على نطاق عالمي في تطبيقك، مما يجعل من السهل إدارة الأخطاء وتوفير تجربة مستخدم متسقة.

في تطبيقنا، سنقوم بإنشاء فئات استثناء مخصصة للتعامل مع الأخطاء والظروف المحددة. سنستفيد أيضًا من ميزة التعامل الاستثنائي العالمي لإدارة الاستثناءات على نطاق عالمي، مما يضمن نهجًا موحدًا لمعالجة الأخطاء عبر التطبيق بأكمله.

سنقوم بإنشاء الفئات الاستثناء التالية:

  • استثناء_عدم_العثور_على_الكتاب: يتم طرحه عند عدم العثور على كتاب بالمعرف المحدد.

  • استثناء_عدم_وجود_الكتاب: يتم طرحه عند عدم وجود كتاب بالمعرف المحدد.

  • معالج_الاستثناءات_العالمي: يدير الاستثناءات على نطاق عالمي في التطبيق.

في مجلد الاستثناءات، قم بإنشاء ملف جديد ب اسم NoBookFoundException.cs وأضف الكود التالي:


// Exceptions/NoBookFoundException.cs

namespace bookapi_minimal.Exceptions
{

    public class NoBookFoundException : Exception
    {

        public NoBookFoundException() : base("No books found")
        {}
    }
}

في هذا الكود، نقوم بإنشاء فئة استثناء مخصصة تسمى NoBookFoundException التي ترث من الفئة Exception. تُستخدم فئة NoBookFoundException للتعامل مع الحالة التي لا توجد فيها كتب في قاعدة البيانات. كما نقوم بتوفير رسالة خطأ مخصصة للاستثناء.

في مجلد Exceptions، قم بإنشاء ملف جديد بعنوان BookDoesNotExistException.cs وأضف الكود التالي:

namespace bookapi_minimal.Exceptions
{
     public class BookDoesNotExistException : Exception
    {
        private int id { get; set; }

        public BookDoesNotExistException(int id) : base($"Book with id {id} does not exist")
        {
            this.id = id;
        } 

    }
}

في هذا الكود، نقوم بإنشاء فئة استثناء مخصصة تسمى BookDoesNotExistException التي ترث من الفئة Exception. تُستخدم فئة BookDoesNotExistException للتعامل مع الحالة التي لا يوجد فيها كتاب بالمعرف المحدد في قاعدة البيانات. كما نقوم بتوفير رسالة خطأ مخصصة للاستثناء.

في مجلد Exceptions، قم بإنشاء ملف جديد بعنوان GlobalExceptionHandler.cs وأضف الكود التالي:

// استثناءات / GlobalExceptionHandler.cs

using System.Net;
using bookapi_minimal.Contracts;
using Microsoft.AspNetCore.Diagnostics;

namespace bookapi_minimal.Exceptions
{

   // فئة معالج الاستثناءات العالمي تنفذ واجهة IExceptionHandler
    public class GlobalExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<GlobalExceptionHandler> _logger;

        // البناء لتهيئة سجل الأحداث
        public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
        {
            _logger = logger;
        }

        // طريقة لمعالجة الاستثناءات بشكل غير متزامن
        public async ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            // تسجيل تفاصيل الاستثناء
            _logger.LogError(exception, "An error occurred while processing your request");

            var errorResponse = new ErrorResponse
            {
                Message = exception.Message,
                Title = exception.GetType().Name
            };

            // تحديد رمز الحالة استنادًا إلى نوع الاستثناء
            switch (exception)
            {
                case BadHttpRequestException:
                    errorResponse.StatusCode = (int)HttpStatusCode.BadRequest;
                    break;

                case NoBookFoundException:
                case BookDoesNotExistException:
                    errorResponse.StatusCode = (int)HttpStatusCode.NotFound;
                    break;

                default:
                    errorResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
                    break;
            }

            // تعيين رمز حالة الاستجابة
            httpContext.Response.StatusCode = errorResponse.StatusCode;

            // كتابة رد الخطأ كـ JSON
            await httpContext.Response.WriteAsJsonAsync(errorResponse, cancellationToken);

            // إرجاع قيمة صحيحة للإشارة إلى أن الاستثناء تمت معالجته
            return true;
        }
    }
}

لنقوم بتفكيك الكود أعلاه:

  • نحدد فئة تسمى GlobalExceptionHandler تنفذ واجهة IExceptionHandler. تستخدم واجهة IExceptionHandler لمعالجة الاستثناءات بشكل عام في التطبيق.

  • تحتوي فئة GlobalExceptionHandler على بناء يهيئ تبعية ILogger<GlobalExceptionHandler>. تستخدم ILogger لتسجيل المعلومات والأخطاء.

  • تُستخدم طريقة TryHandleAsync للتعامل مع الاستثناءات بشكل غير متزامن. تقبل هذه الطريقة HttpContext وException وCancellationToken كمعلمات.

  • نقوم بتسجيل تفاصيل الاستثناء باستخدام اعتماد ILogger.

  • نقوم بإنشاء كائن ErrorResponse لتمثيل استجابة الخطأ التي ترجعها واجهة برمجة التطبيقات. يحتوي كائن ErrorResponse على رسالة الخطأ والعنوان ورمز الحالة.

  • نحدد رمز الحالة بناءً على نوع الاستثناء. إذا كان الاستثناء هو BadHttpRequestException، نقوم بتعيين رمز الحالة إلى BadRequest. إذا كان الاستثناء هو NoBookFoundException أو BookDoesNotExistException، نقوم بتعيين رمز الحالة إلى NotFound. خلاف ذلك، نقوم بتعيين رمز الحالة إلى InternalServerError.

  • نقوم بتعيين رمز حالة الاستجابة باستخدام خاصية httpContext.Response.StatusCode.

  • نقوم بكتابة استجابة الخطأ بصيغة JSON باستخدام طريقة httpContext.Response.WriteAsJsonAsync.

  • نقوم بإرجاع true للإشارة إلى أنه تم معالجة الاستثناء بنجاح.

الآن بعد أن قمنا بإنشاء فئات الاستثناء، دعنا نقوم بتسجيل GlobalExceptionHandler في حاوية حقن التبعية. بما أننا أنشأنا طريقة توسيع لتسجيل الخدمات في حاوية حقن التبعية، سنضيف GlobalExceptionHandler إلى فئة ServiceExtensions.

قم بتحديث فئة ServiceExtensions في مجلد Extensions على النحو التالي:


// Extensions/ServiceExtensions.cs
//...
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

builder.Services.AddProblemDetails();

//...

يسجل الطريقة AddExceptionHandler الـ GlobalExceptionHandler في حاوية حقن الاعتماد. تسجل الطريقة AddProblemDetails فئة ProblemDetails في حاوية حقن الاعتماد.

الآن بعد تسجيل الـ GlobalExceptionHandler في حاوية حقن الاعتماد، يمكننا استخدامه لمعالجة الاستثناءات بشكل عام في تطبيقنا. في القسم التالي، سنقوم بإنشاء نقاط نهاية واجهة برمجة التطبيقات(API) للتفاعل مع بيانات الكتاب.

كيفية إنشاء نقاط نهاية واجهة برمجة التطبيقات(API)

في سياق الواجهات البرمجية الدنيا في ASP.NET Core، هناك العديد من الطرق لإعداد نقاط النهاية الخاصة بك.

يمكنك تعريفها مباشرة في ملف Program.cs الخاص بك. ولكن كما ينمو مشروعك وتحتاج إلى إضافة المزيد من نقاط النهاية أو الوظائف، فإن تنظيم رمزك بشكل أفضل يكون مفيدًا. طريقة لتحقيق ذلك هي عن طريق إنشاء فئة منفصلة للتعامل مع جميع نقاط النهاية.

كما نوقش أعلاه، لا تستخدم الواجهات البرمجية الدنيا وحدات التحكم أو العروض مثل تطبيقات ASP.NET Core التقليدية. بدلاً من ذلك، يستخدمون طرقًا مثل MapGet، MapPost، MapPut، و MapDelete لتعريف طرق HTTP ومسارات لنقاط نهاية واجهة برمجة التطبيقات(API).

للبدء، انتقل إلى مجلد Endpoints وأنشئ ملفًا جديدًا باسم BookEndpoints.cs. أضف الكود التالي إلى الملف:


// نقاط النهاية / BookEndpoints.cs



namespace bookapi_minimal.Endpoints
{
     public static class BookEndPoint
    {
        public static IEndpointRouteBuilder MapBookEndPoint(this IEndpointRouteBuilder app)
        {


            return app;
        }
    }
}

تحتوي فئة BookEndpoints على طريقة MapBookEndPoint التي تعيد كائن IEndpointRouteBuilder. يتم استخدام كائن IEndpointRouteBuilder لتحديد طرق HTTP ومسارات نقاط نهاية API. في الأقسام التالية، سنحدد نقاط نهاية الـ API لـ إنشاء، قراءة، تحديث، و حذف الكتب.

كيفية إنشاء نقطة نهاية AddBookAsync Books

في هذا القسم، سنقوم بإنشاء نقطة نهاية AddBookAsync. ستقبل هذه النقطة نقطة Book كجسم JSON وسوف تضيفها إلى قاعدة البيانات. سنستخدم الطريقة MapPost لتحديد طريقة HTTP ومسار لهذه النقطة.

أضف الكود التالي إلى فئة BookEndpoints:


// نقاط النهاية / BookEndpoints.cs


//...
   // نقطة النهاية لإضافة كتاب جديد
      app.MapPost("/books", async (CreateBookRequest createBookRequest, IBookService bookService) =>
        {
        var result = await bookService.AddBookAsync(createBookRequest);
        return Results.Created($"/books/{result.Id}", result); 
        });


//...
  • تعريف المسار: تحدد طريقة MapPost المسار للنقطة النهاية كـ /books.

  • نموذج الطلب: تقبل النقطة كائن CreateBookRequest كجسم JSON. يحتوي الكائن CreateBookRequest على البيانات المطلوبة لإنشاء كتاب جديد.

  • نموذج الاستجابة: تعيد النقطة النهائية كائن Book كحمولة JSON. يحتوي كائن Book على البيانات للكتاب الجديد الذي تم إنشاؤه.

  • قيمة الإرجاع: تعيد النقطة النهائية نتيجة Created. تحتوي نتيجة Created على موقع الكتاب الجديد وكائن Book.

كيفية إنشاء نقطة النهاية GetBookAsync للكتاب

في هذا القسم، سنقوم بإنشاء نقطة النهاية GetBookAsync. ستقبل هذه النقطة النهائية معرف الكتاب كمعلمة استعلام وستعيد الكتاب بالمعرف المحدد. سنستخدم طريقة MapGet لتحديد طريقة HTTP والمسار لهذه النقطة النهائية.

أضف الكود التالي إلى فئة BookEndpoints:


// نقاط النهاية/BookEndpoints.cs

// ...
    // نقطة النهاية للحصول على جميع الكتب
    app.MapGet("/books", async (IBookService bookService) =>
     {
    var result = await bookService.GetBooksAsync();
    return Results.Ok(result);
});


//...
  • تعريف المسار: تحدد طريقة MapGet المسار لنقطة النهاية كـ /books.

  • نموذج الطلب: تقبل نقطة النهاية كائن Book كحمولة JSON. كائن Book يحتوي على البيانات المطلوبة لإنشاء كتاب جديد.

  • نموذج الاستجابة: ترجع نقطة النهاية كائن Book كحمولة JSON. كائن Book يحتوي على بيانات الكتاب الجديد.

  • قيمة العائد: ترجع نقطة النهاية نتيجة Ok. نتيجة Ok تحتوي على كائن Book.

كيفية إنشاء نقطة نهاية الكتابة الـ GetBookByIdAsync

في هذا القسم، سنقوم بإنشاء نقطة النهاية GetBookByIdAsync. ستقبل هذه النقطة معرف الكتاب كمعلمة مسار وستقوم بإرجاع الكتاب ذو الهوية المحددة. سنستخدم الطريقة MapGet لتحديد طريقة HTTP والمسار لهذه النقطة.

أضف الكود التالي إلى فئة BookEndpoints:


// Endpoints/BookEndpoints.cs
//...
// نقطة النهاية للحصول على كتاب بواسطة الهوية

  app.MapGet("/books/{id:guid}", async (Guid id, IBookService bookService) =>
  {
    var result = await bookService.GetBookByIdAsync(id);
    return result != null ? Results.Ok(result) : Results.NotFound();
});

//...
  • تعريف المسار: تحدد طريقة MapGet المسار للنقطة النهائية كـ /books/{id:guid}. يحدد المعلمة {id:guid} أن المعلمة id يجب أن تكون GUID.

  • نموذج الطلب: تقبل النقطة كائن Book كحمولة JSON. يحتوي كائن Book على البيانات المطلوبة لإنشاء كتاب جديد.

  • نموذج الاستجابة: تقوم النقطة بإرجاع كائن Book كحمولة JSON. يحتوي كائن Book على البيانات للكتاب الجديد المنشأ.

  • قيمة العودة: تُرجع نقطة النهاية نتيجة Ok إذا تم العثور على الكتاب. تُرجع نتيجة NotFound إذا لم يتم العثور على الكتاب.

كيفية إنشاء نقطة نهاية UpdateBookAsync للكتاب

في هذا القسم، سنقوم بإنشاء نقطة نهاية UpdateBookAsync. ستقبل هذه النقطة نصف كتاب كمعلمة مسار وكائن Book كحمولة JSON وستقوم بتحديث الكتاب بالمعرف المحدد. سنستخدم طريقة MapPut لتعريف طريقة HTTP ومسار لهذه النقطة النهائية.

أضف الشيفرة التالية إلى فئة BookEndpoints:


// Endpoints/BookEndpoints.cs

//...
   // نقطة نهاية لتحديث كتاب بواسطة المعرف
    app.MapPut("/books/{id:guid}", async (Guid id, UpdateBookRequest updateBookRequest, IBookService bookService) =>
 {
var result = await bookService.UpdateBookAsync(id, updateBookRequest);
return result != null ? Results.Ok(result) : Results.NotFound();
});

//...
  • تعريف المسار: تعرّف طريقة MapPut المسار لنقطة النهاية كـ /books/{id:guid}. المعلمة {id:guid} تحدد أن المعلمة id يجب أن تكون GUID.

  • نموذج الطلب: تقبل النقطة النهائية كائن Book كحمولة JSON. يحتوي كائن Book على البيانات المطلوبة لإنشاء كتاب جديد.

  • نموذج الاستجابة: ترجع النقطة النهائية كائن Book كحمولة JSON. يحتوي كائن Book على البيانات للكتاب الجديد الذي تم إنشاؤه.

  • قيمة الإرجاع: ترجع النقطة النهائية نتيجة Ok إذا تم العثور على الكتاب. تُرجع نتيجة NotFound إذا لم يتم العثور على الكتاب.

كيفية إنشاء نقطة الكتاب DeleteBookAsync

في هذا القسم، سنقوم بإنشاء نقطة النهاية DeleteBookAsync. ستقبل هذه النقطة الكتاب ID كمعلمة مسار وتحذف الكتاب بالمعرف المحدد. سنستخدم طريقة MapDelete لتحديد طريقة HTTP والمسار لهذه النقطة.

أضف الكود التالي إلى فئة BookEndpoints:


// Endpoints/BookEndpoints.cs

//...
   // نقطة النهاية لحذف كتاب بواسطة المعرف
 app.MapDelete("/books/{id:guid}", async (Guid id, IBookService bookService) =>
{
var result = await bookService.DeleteBookAsync(id);
   return result ? Results.NoContent() : Results.NotFound();
});


//...
  • تعريف المسار: تحدد طريقة MapDelete المسار للنقطة النهائية على أنه /books/{id:guid}. تحدد المعلمة {id:guid} أن المعلمة id يجب أن تكون GUID.

  • نموذج الطلب: تقبل النقطة النهائية كائن Book كتحميل JSON. يحتوي الكائن Book على البيانات المطلوبة لإنشاء كتاب جديد.

  • نموذج الاستجابة: تعيد النقطة النهائية كائن Book كاستجابة JSON. يحتوي الكائن Book على البيانات للكتاب الجديد الذي تم إنشاؤه.

  • قيمة الإرجاع: تُرجع النقطة النهائية نتيجة NoContent إذا تم حذف الكتاب بنجاح. يتم إرجاع نتيجة NotFound إذا لم يتم العثور على الكتاب.

الآن لقد قمت بتعريف جميع الطرق لنقاط نهاية الكتاب. لذا يجب أن يبدو صنف النقطة النهائية الخاص بك مثل هذا:

// Endpoints/BookEndpoints.cs
using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;

namespace bookapi_minimal.Endpoints
{
     public static class BookEndPoint
    {
        public static IEndpointRouteBuilder MapBookEndPoint(this IEndpointRouteBuilder app)
        {
            // حدد النقاط النهائية

            // نقطة نهائية لإضافة كتاب جديد
            app.MapPost("/books", async (CreateBookRequest createBookRequest, IBookService bookService) =>
            {
                var result = await bookService.AddBookAsync(createBookRequest);
                return Results.Created($"/books/{result.Id}", result); 
            });


               // نقطة نهائية للحصول على جميع الكتب
            app.MapGet("/books", async (IBookService bookService) =>
            {
                var result = await bookService.GetBooksAsync();
                return Results.Ok(result);
            });

            // نقطة نهائية للحصول على كتاب بواسطة الهوية
            app.MapGet("/books/{id:guid}", async (Guid id, IBookService bookService) =>
            {
                var result = await bookService.GetBookByIdAsync(id);
                return result != null ? Results.Ok(result) : Results.NotFound();
            });


            // نقطة نهائية لتحديث كتاب بواسطة الهوية
            app.MapPut("/books/{id:guid}", async (Guid id, UpdateBookRequest updateBookRequest, IBookService bookService) =>
            {
                var result = await bookService.UpdateBookAsync(id, updateBookRequest);
                return result != null ? Results.Ok(result) : Results.NotFound();
            });

            // نقطة نهائية لحذف كتاب بواسطة الهوية
            app.MapDelete("/books/{id:guid}", async (Guid id, IBookService bookService) =>
            {
                var result = await bookService.DeleteBookAsync(id);
                return result ? Results.NoContent() : Results.NotFound();
            });

            return app;
        }
    }
}

تهانينا! لقد قمت بإنشاء جميع النقاط النهائية لواجهة برمجة تطبيقات الكتب. تتعامل النقاط النهائية مع عمليات CRUD للكتب وتُرجع الاستجابات المناسبة استنادًا إلى الطلب والبيانات.

كيفية تسجيل النقاط النهائية

بعد تحديد نقاط نهاية واجهة برمجة التطبيقات API للكتب، الخطوة التالية هي تسجيل هذه النقاط النهائية في ملف Program.cs. سنستخدم طريقة MapBookEndpoints لتسجيل نقاط نهاية الكتب.

يجب أيضًا تنظيف فئة Program.cs لضمان بقائها منظمة وقابلة للصيانة.

// Program.cs

using System.Reflection;
using bookapi_minimal.Endpoints;
using bookapi_minimal.Services;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);


builder.AddApplicationServices();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c=>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "Mimal API", Version = "v1", Description = "Showing how you can build minimal " +
        "api with .net" });


    // تعيين مسار التعليقات لـ Swagger JSON وواجهة المستخدم.
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);

});
var app = builder.Build();

// تكوين خط أنابيب طلب HTTP.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseExceptionHandler();


app.MapGroup("/api/v1/")
   .WithTags(" Book endpoints")
   .MapBookEndPoint();

app.Run();

لنقسم مكونات الرئيسية لملف Program.cs:

  • AddApplicationServices: تسجيل الخدمات اللازمة للـ API. إنها طريقة تمديد أنشأناها سابقًا لإضافة الخدمات إلى حاوية حقن الإعتماد.

  • AddSwaggerGen: تسجيل مولد Swagger، الذي يستخدم لإنشاء وثائق Swagger للـ API. نحدد عنوانًا، إصدارًا، ووصفًا للـ API في وثيقة Swagger.

  • MapGroup: تقوم هذه الطريقة بتجميع نقاط النهاية. يأخذ مسارًا كمعلمة ويعيد كائنًا IEndpointRouteBuilder. نستخدم الطريقة WithTags لإضافة علامات إلى النقاط النهائية والطريقة MapBookEndpoints لتسجيل نقاط نهاية الكتب.

  • Run: تقوم هذه الطريقة ببدء التطبيق.

لتمكين وثائق Swagger، تحتاج إلى إضافة خاصية GenerateDocumentationFile إلى ملف .csproj الخاص بك. في هذا المثال، يُسمى الملف bookapi-minimal.csproj، ولكن يمكن أن يختلف الاسم استنادًا إلى مشروعك.

أضف السطر التالي إلى ملف .csproj الخاص بك:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

في النهاية، يجب أن يبدو ملف bookapi-minimal.csproj مثل هذا:


<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <RootNamespace>bookapi_minimal</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
   <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>

الآن بعد تسجيل نقاط نهاية الكتب في ملف Program.cs، يمكننا تشغيل التطبيق واختبار نقاط نهاية واجهة برمجة التطبيق باستخدام Swagger.

عند تشغيل التطبيق، يجب أن ترى توثيق Swagger على الرابط التالي: https://localhost:5001/swagger/index.html. توثيق Swagger يوفر معلومات حول نقاط نهاية واجهة برمجة التطبيقات، نماذج الطلب والاستجابة، ويسمح لك باختبار نقاط النهاية مباشرة من المتصفح. يجب أن ترى شيئًا مشابهًا لهذا:

تهانينا! لقد قمت بتنفيذ منطق الأعمال لخدمة الكتاب، وأنشأت استثناءات مخصصة، وحددت نقاط واجهة برمجة التطبيقات، وسجلت النقاط في ملف Program.cs. لقد قمت أيضًا بتمكين توثيق Swagger لاختبار نقاط واجهة برمجة التطبيقات.

كيفية إضافة بيانات بادئة إلى قاعدة البيانات

خطوة مهمة أخرى هي إضافة بيانات بادئة إلى قاعدة البيانات مع بدء تشغيل التطبيق. ستقوم هذه البيانات بالبدء في ملء قاعدة البيانات، مما يتيح لك اختبار نقاط واجهة برمجة التطبيقات بدون إضافة البيانات يدويًا.

لنضيف بعض بيانات بادئة قبل تنفيذ التحركات واختبار نقاط واجهة برمجة التطبيقات.

لتحقيق ذلك، سنقوم بإنشاء فئة جديدة في مجلد التكوين الخاص بنا تسمى BookTypeConfigurations وإضافة الكود التالي:



using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace bookapi_minimal.Configurations
{
    public class BookTypeConfigurations : IEntityTypeConfiguration<BookModel>
    {
        public void Configure(EntityTypeBuilder<BookModel> builder)
        {
            // تكوين اسم الجدول
            builder.ToTable("Books");

            // تكوين المفتاح الأساسي
            builder.HasKey(x => x.Id);

            // تكوين الخصائص
            builder.Property(x => x.Id).ValueGeneratedOnAdd();
            builder.Property(x => x.Title).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Author).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Description).IsRequired().HasMaxLength(500);
            builder.Property(x => x.Category).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Language).IsRequired().HasMaxLength(50);
            builder.Property(x => x.TotalPages).IsRequired();

            // بيانات بادئة
            builder.HasData(
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "The Alchemist",
                    Author = "Paulo Coelho",
                    Description = "The Alchemist follows the journey of an Andalusian shepherd",
                    Category = "Fiction",
                    Language = "English",
                    TotalPages = 208
                },
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "To Kill a Mockingbird",
                    Author = "Harper Lee",
                    Description = "A novel about the serious issues of rape and racial inequality.",
                    Category = "Fiction",
                    Language = "English",
                    TotalPages = 281
                },
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "1984",
                    Author = "George Orwell",
                    Description = "A dystopian social science fiction novel and cautionary tale about the dangers of totalitarianism. ",
                  Category = "Fiction",
                  Language = "English",
                  TotalPages = 328
                } 
            );
        }
    }
}

لنقسم الكود أعلاه:

في Entity Framework Core، يمكنك استخدام واجهة IEntityTypeConfiguration لتكوين نوع الكيان والبيانات الابتدائية لقاعدة البيانات. تقوم فئة BookTypeConfigurations بتنفيذ واجهة IEntityTypeConfiguration<BookModel> وتوفر التكوين لكيان BookModel.

  • طريقة التكوين: تُستخدم هذه الطريقة لتكوين نوع كيان BookModel. تعرف اسم الجدول والمفتاح الابتدائي والخصائص لكيان BookModel.

    • اسم الجدول: تُحدد طريقة ToTable اسم الجدول الذي سيتم إنشاؤه في قاعدة البيانات. في هذه الحالة، يتم تعيين اسم الجدول إلى “Books”.

    • المفتاح الابتدائي: تُحدد طريقة HasKey المفتاح الابتدائي لكيان BookModel. يتم تعيين المفتاح الابتدائي إلى خصائص Id.

    • الخصائص: تُعمل طريقة Property على تكوين خصائص كيان BookModel. تُحدد نوع البيانات والطول والقيود لكل خاصية.

  • بيانات البذرة: تبذر طريقة HasData قاعدة البيانات ببيانات أولية. تنشئ ثلاثة كائنات BookModel ببيانات عينية لاختبار نقاط نهاية واجهة برمجة التطبيقات.

الآن بعد إنشاء فئة BookTypeConfigurations، نحتاج إلى تسجيل هذا التكوين في فئة ApplicationContext. هذا يضمن تطبيق التكوين عند إنشاء قاعدة البيانات أو هجرتها.

نحن نقترب أخيرًا من الاختبار النهائي لواجهة برمجة التطبيقات API الخاصة بنا. ولكن قبل القيام بذلك، نحتاج إلى تنفيذ الترحيلات لإنشاء قاعدة البيانات وتطبيق بيانات البذرة.

تذكر أننا أضفنا سلسلة اتصال قاعدة البيانات في ملف appsettings.json؟ الآن دعونا ننفذ ترحيلًا ومن ثم تحديث قاعدة البيانات لكي يظهر تأثير الترحيل.

كيفية تنفيذ ترحيل

الترحيلات تسمح لك بتحديث بنية قاعدة البيانات بناءً على التغييرات التي تم إجراؤها على فئات النموذج الخاصة بك. في Entity Framework Core، يمكنك استخدام أمر dotnet ef migrations add لإنشاء ترحيل جديد يعكس هذه التغييرات.

لتنفيذ ترحيل، قم بتشغيل الأمر التالي في نافذة الطرفية:

dotnet ef migrations add InitialCreate

إذا نجح الأمر، يجب أن ترى إخراجًا مماثلًا لهذا:

Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'

سترى الآن مجلدًا جديدًا يسمى Migrations في مشروعك. يحتوي هذا المجلد على ملفات الترحيل التي تم إنشاؤها بناءً على التغييرات التي تم إجراؤها على فئات النموذج الخاصة بك. تتضمن ملفات الترحيل هذه الأوامر الخاصة بالـ SQL المطلوبة لتحديث بنية قاعدة البيانات.

كيفية تحديث قاعدة البيانات

بعد إنشاء الترحيل، تحتاج إلى تطبيق الترحيل لتحديث بنية قاعدة البيانات. يمكنك استخدام أمر dotnet ef database update لتطبيق الترحيل وتحديث قاعدة البيانات. تأكد من تشغيل خادم SQL.

قم بتشغيل الأمر التالي في نافذة الطرفية:


dotnet ef database update

سيقوم هذا بتحديث بنية قاعدة البيانات بناءً على التغييرات التي تم إجراؤها على فئات النموذج الخاصة بك. تأكد من عدم وجود أخطاء في سلسلة اتصال قاعدة البيانات الخاصة بك.

كيفية اختبار نقاط نهاية واجهة برمجة التطبيقات

الآن يمكننا اختبار نقاط النهاية الخاصة بنا باستخدام Swagger. للقيام بذلك، قم بتشغيل التطبيق عن طريق تنفيذ الأمر التالي في الطرفية:


dotnet run

سيتم تشغيل التطبيق الخاص بنا. يمكنك فتح متصفحك والانتقال إلى https://localhost:5001/swagger/index.html للوصول إلى وثائق Swagger. يجب أن ترى قائمة بنقاط نهاية الواجهة البرمجية، ونماذج الطلب والاستجابة، والقدرة على اختبار نقاط النهاية مباشرة من المتصفح.

إذا كان رقم المنفذ الخاص بك مختلفًا عن 5001، لا تقلق – سيعمل بشكل صحيح. قد يتغير المنفذ اعتمادًا على نوع الجهاز الذي تستخدمه، ولكن سيحقق نفس النتيجة.

كيفية اختبار نقطة النهاية الحصول على جميع الكتب

لاختبار نقطة نهاية الحصول على جميع الكتب، اتبع هذه الخطوات:

  1. في وثائق Swagger، انقر فوق نقطة النهاية GET /api/v1/books.

  2. انقر على زر جربها الآن.

  3. انقر على زر تنفيذ.

سيتم إرسال طلب إلى واجهة برمجة التطبيق لاسترداد جميع الكتب في قاعدة البيانات.

يجب عليك رؤية الاستجابة من واجهة برمجة التطبيق، التي ستتضمن قائمة الكتب التي تم زرعها في قاعدة البيانات.

توضح الصورة أدناه الاستجابة من واجهة برمجة التطبيق:

كيفية اختبار نقطة نهاية الحصول على الكتاب بواسطة الهوية

لاختبار نقطة نهاية الحصول على الكتاب بواسطة الهوية، اتبع هذه الخطوات:

  1. في وثائق Swagger، انقر فوق نقطة النهاية GET /api/v1/books/{id}.

  2. أدخل هوية كتاب في حقل id. يمكنك استخدام إحدى هويات الكتب التي تم زرعها في قاعدة البيانات.

  3. انقر على الزر جرب الآن.

سيتم إرسال طلب إلى واجهة برمجة التطبيقات لاسترداد الكتاب بالهوية المحددة. يجب أن تظهر الاستجابة من واجهة برمجة التطبيقات، التي ستتضمن الكتاب بالهوية المحددة.

الصورة أدناه تظهر الاستجابة من واجهة برمجة التطبيقات:

كيفية اختبار نقطة نهاية إضافة كتاب

لاختبار نقطة نهاية إضافة كتاب، اتبع هذه الخطوات:

  1. في وثائق Swagger، انقر فوق نقطة النهاية POST /api/v1/books.

  2. انقر على الزر جرب الآن.

  3. أدخل تفاصيل الكتاب في جسم الطلب.

  4. انقر على زر تنفيذ.

سيتم إرسال طلب إلى واجهة برمجة التطبيقات لإضافة كتاب جديد إلى قاعدة البيانات.

يجب أن ترى الاستجابة من واجهة برمجة التطبيقات، التي ستتضمن الكتاب الجديد الذي تم إنشاؤه.

الصورة أدناه توضح الاستجابة من واجهة برمجة التطبيقات:

كيفية اختبار نقطة النهاية تحديث الكتاب

لاختبار نقطة النهاية تحديث الكتاب، اتبع هذه الخطوات:

  1. في وثائق Swagger، انقر على نقطة النهاية PUT /api/v1/books/{id}.

  2. أدخل معرف الكتاب في حقل id. يمكنك استخدام معرف أحد الكتب التي أضفناها للتو.

  3. انقر على زر جرب الآن.

سيتم إرسال طلب إلى واجهة برمجة التطبيقات لتحديث الكتاب بالمعرف المحدد.

يجب أن ترى الاستجابة من واجهة برمجة التطبيقات، التي ستتضمن الكتاب المحدث.

الصورة أدناه توضح الاستجابة من واجهة برمجة التطبيقات:

كيفية اختبار نقطة النهاية حذف الكتاب

لاختبار نقطة النهاية حذف الكتاب، اتبع هذه الخطوات:

  1. في وثائق Swagger، انقر على نقطة النهاية DELETE /api/v1/books/{id}.

  2. أدخل معرف الكتاب في حقل id. يمكنك استخدام أيًا من المعرفات للكتب التي تمت إضافتها مؤخرًا أو البيانات المزروعة.

  3. انقر على زر جرب الآن.

سيتم إرسال طلب إلى واجهة برمجة التطبيقات API لحذف الكتاب بالمعرف المحدد.

الصورة أدناه تُظهر الاستجابة من واجهة برمجة التطبيقات API:

تهانينا! لقد قمت بتنفيذ جميع عمليات CRUD الخاصة بالكتب واختبار نقاط نهاية واجهة برمجة التطبيقات API باستخدام Swagger، مما يتحقق من عملها كما هو متوقع. يمكنك الآن بناء على هذه الأسس لإضافة المزيد من الميزات والوظائف إلى واجهة برمجة التطبيقات الخاصة بك.

الاستنتاج

استكشف هذا الدليل كيفية إنشاء واجهة برمجة تطبيقات API الحدية في ASP.NET Core مع .NET 8. قمنا ببناء واجهة برمجة تطبيقات API شاملة تدعم عمليات CRUD، ونفذنا استثناءات مخصصة، وحددنا وسجلنا نقاط نهاية API، وفعلنا توثيق Swagger لاختبار السهل.

بعد اتباع هذا البرنامج التعليمي، قد اكتسبت أساسًا قويًا لبناء واجهات برمجة تطبيقات حدية باستخدام ASP.NET Core. يمكنك الآن تطبيق هذه المعرفة وإنشاء واجهات برمجة تطبيقات قوية لمختلف المجالات والصناعات.

آمل أن تكون وجدت هذا البرنامج التعليمي مفيدًا وإيجابيًا. شكرًا لك على القراءة!

لا تتردد في التواصل معي على وسائل التواصل الاجتماعي: