استهلاك REST API في .NET MAUI: دليل عملي باستخدام HttpClient و IHttpClientFactory

دليل عملي لاستهلاك REST API في تطبيقات .NET MAUI باستخدام HttpClient و IHttpClientFactory، مع تغطية عمليات CRUD والمصادقة بـ JWT وأنماط المرونة مع Polly والتعامل مع حالة عدم الاتصال وأمثلة كاملة.

لماذا يُعدّ استهلاك REST API أساسياً في تطبيقات .NET MAUI؟

لنكن صريحين — الغالبية العظمى من تطبيقات الهاتف اليوم لا تعمل بمعزل عن الإنترنت. سواء كنت تبني تطبيق تجارة إلكترونية، أو شبكة اجتماعية، أو حتى أداة إنتاجية بسيطة، فإن التواصل مع واجهات REST API هو العمود الفقري لتطبيقك فعلياً.

في بيئة .NET MAUI، تُوفّر الفئة HttpClient الأداة الأساسية لهذا التواصل. لكن — وهنا المفاجأة لكثير من المطورين — استخدامها بالشكل الصحيح ليس بالبساطة التي يبدو عليها. يتطلب الأمر فهماً جيداً لأنماط الإدارة والمرونة وأمان الاتصال.

في هذا الدليل، سنبني معاً طبقة اتصال شبكي متكاملة لتطبيق .NET MAUI، من عمليات CRUD البسيطة وصولاً إلى أنماط المرونة المتقدمة مع IHttpClientFactory و Microsoft.Extensions.Http.Resilience. فلنبدأ!

إعداد المشروع وتثبيت الحزم المطلوبة

قبل أي شيء، تأكد من أنك تستخدم .NET 10 SDK مع أحدث إصدار من .NET MAUI. أنشئ مشروعاً جديداً وأضف الحزم التالية:

dotnet new maui -n MyMauiApp
cd MyMauiApp
dotnet add package Microsoft.Extensions.Http
dotnet add package Microsoft.Extensions.Http.Resilience
dotnet add package System.Text.Json

حزمة Microsoft.Extensions.Http تُوفّر IHttpClientFactory لإدارة دورة حياة HttpClient بكفاءة. أما حزمة Microsoft.Extensions.Http.Resilience فتُضيف طبقة المرونة المبنية على مكتبة Polly v8 — وهي واحدة من أفضل الإضافات التي يمكنك استخدامها في أي تطبيق إنتاجي.

فهم المشكلة: لماذا لا نُنشئ HttpClient مباشرة؟

صراحةً، هذا الخطأ شائع جداً حتى بين المطورين ذوي الخبرة. الكثيرون يُنشئون نسخة جديدة من HttpClient في كل طلب هكذا:

// ❌ لا تفعل هذا - يسبب استنزاف المقابس (Socket Exhaustion)
using var client = new HttpClient();
var response = await client.GetAsync("https://api.example.com/data");

المشكلة هنا أنه عند التخلص من كائن HttpClient، لا يتم تحرير المقبس (Socket) الأساسي فوراً. في التطبيقات التي تُجري طلبات متكررة، يؤدي هذا إلى نفاد المقابس المتاحة وفشل الاتصالات بالكامل.

وفي المقابل، استخدام نسخة واحدة طوال عمر التطبيق (Singleton) قد يتسبب في تجاهل تغييرات DNS. يعني ما في حل مثالي بالطريقة التقليدية!

الحل الأفضل هو استخدام IHttpClientFactory الذي يُدير مجموعة (Pool) من HttpMessageHandler ويُعيد استخدامها بذكاء، وهذا بالضبط ما سنعتمد عليه.

تسجيل HttpClient مع حقن التبعيات في MauiProgram.cs

الخطوة الأولى هي تسجيل HttpClient في حاوية حقن التبعيات (DI Container) داخل ملف MauiProgram.cs:

using Microsoft.Extensions.Http.Resilience;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // تسجيل HttpClient مُسمّى مع عنوان القاعدة
        builder.Services.AddHttpClient("ApiClient", client =>
        {
            client.BaseAddress = new Uri("https://api.example.com/");
            client.DefaultRequestHeaders.Add("Accept", "application/json");
            client.Timeout = TimeSpan.FromSeconds(30);
        })
        .AddStandardResilienceHandler(); // إضافة طبقة المرونة

        // تسجيل الخدمات و ViewModels
        builder.Services.AddSingleton<IApiService, ApiService>();
        builder.Services.AddTransient<MainViewModel>();
        builder.Services.AddTransient<MainPage>();

        return builder.Build();
    }
}

لاحظ كيف أن سطر واحد فقط (AddStandardResilienceHandler()) يمنحك طبقة مرونة كاملة. سنتحدث عن تفاصيلها لاحقاً.

نمط العميل المُحدَّد النوع (Typed Client Pattern)

للحصول على تنظيم أفضل للكود، استخدم نمط العميل المُحدَّد النوع الذي يُغلّف HttpClient داخل خدمة مخصصة. شخصياً، أفضّل هذا النمط في أي مشروع يتجاوز مرحلة النموذج الأولي:

public interface IApiService
{
    Task<List<Product>?> GetProductsAsync();
    Task<Product?> GetProductByIdAsync(int id);
    Task<Product?> CreateProductAsync(CreateProductRequest request);
    Task<bool> UpdateProductAsync(int id, UpdateProductRequest request);
    Task<bool> DeleteProductAsync(int id);
}

public class ApiService : IApiService
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializerOptions _jsonOptions;

    public ApiService(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient("ApiClient");
        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
    }
}

تنفيذ عمليات CRUD الكاملة

الآن نأتي للجزء الممتع — تنفيذ العمليات الأربع الأساسية.

عملية GET — جلب البيانات

public async Task<List<Product>?> GetProductsAsync()
{
    try
    {
        var response = await _httpClient.GetAsync("api/products");
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<List<Product>>(json, _jsonOptions);
    }
    catch (HttpRequestException ex)
    {
        Debug.WriteLine($"HTTP Error: {ex.StatusCode} - {ex.Message}");
        return null;
    }
}

public async Task<Product?> GetProductByIdAsync(int id)
{
    var response = await _httpClient.GetAsync($"api/products/{id}");

    if (response.StatusCode == HttpStatusCode.NotFound)
        return null;

    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<Product>(_jsonOptions);
}

لاحظ كيف نتعامل مع حالة 404 بشكل منفصل عن باقي الأخطاء — هذا مهم لأن "المنتج غير موجود" ليس خطأً تقنياً بل نتيجة متوقعة.

عملية POST — إنشاء بيانات جديدة

public async Task<Product?> CreateProductAsync(CreateProductRequest request)
{
    var content = new StringContent(
        JsonSerializer.Serialize(request, _jsonOptions),
        Encoding.UTF8,
        "application/json");

    var response = await _httpClient.PostAsync("api/products", content);
    response.EnsureSuccessStatusCode();

    return await response.Content.ReadFromJsonAsync<Product>(_jsonOptions);
}

عملية PUT — تحديث البيانات

public async Task<bool> UpdateProductAsync(int id, UpdateProductRequest request)
{
    var content = new StringContent(
        JsonSerializer.Serialize(request, _jsonOptions),
        Encoding.UTF8,
        "application/json");

    var response = await _httpClient.PutAsync($"api/products/{id}", content);
    return response.IsSuccessStatusCode;
}

عملية DELETE — حذف البيانات

public async Task<bool> DeleteProductAsync(int id)
{
    var response = await _httpClient.DeleteAsync($"api/products/{id}");
    return response.IsSuccessStatusCode;
}

عمليات PUT و DELETE واضحة ومباشرة — نُرسل الطلب ونتحقق من نجاحه فقط.

نماذج البيانات (Data Models)

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public string ImageUrl { get; set; } = string.Empty;
}

public class CreateProductRequest
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

public class UpdateProductRequest
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

المصادقة باستخدام رموز Bearer و JWT

معظم واجهات API الحقيقية تتطلب مصادقة، وهذا أمر لا مفر منه. الطريقة الأنظف لإضافة رموز Bearer تلقائياً لكل طلب هي استخدام DelegatingHandler مخصص:

public class AuthenticationHandler : DelegatingHandler
{
    private readonly ISecureStorage _secureStorage;

    public AuthenticationHandler(ISecureStorage secureStorage)
    {
        _secureStorage = secureStorage;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await _secureStorage.GetAsync("access_token");

        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token);
        }

        var response = await base.SendAsync(request, cancellationToken);

        // إذا انتهت صلاحية الرمز، حاول تجديده
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            var newToken = await RefreshTokenAsync();
            if (!string.IsNullOrEmpty(newToken))
            {
                request.Headers.Authorization =
                    new AuthenticationHeaderValue("Bearer", newToken);
                response = await base.SendAsync(request, cancellationToken);
            }
        }

        return response;
    }

    private async Task<string?> RefreshTokenAsync()
    {
        var refreshToken = await _secureStorage.GetAsync("refresh_token");
        if (string.IsNullOrEmpty(refreshToken))
            return null;

        // منطق تجديد الرمز هنا
        // ...
        return null;
    }
}

الجميل في هذا النمط أن باقي كود التطبيق لا يحتاج أن يعرف أي شيء عن المصادقة — كل شيء يحدث بشفافية تامة في الخلفية.

سجّل هذا المعالج في MauiProgram.cs:

builder.Services.AddTransient<AuthenticationHandler>();

builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
})
.AddHttpMessageHandler<AuthenticationHandler>()
.AddStandardResilienceHandler();

خدمة تسجيل الدخول والحصول على رمز JWT

public class AuthService
{
    private readonly HttpClient _httpClient;
    private readonly ISecureStorage _secureStorage;
    private readonly JsonSerializerOptions _jsonOptions;

    public AuthService(
        IHttpClientFactory httpClientFactory,
        ISecureStorage secureStorage)
    {
        _httpClient = httpClientFactory.CreateClient("ApiClient");
        _secureStorage = secureStorage;
        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
    }

    public async Task<bool> LoginAsync(string email, string password)
    {
        var loginRequest = new { Email = email, Password = password };
        var content = new StringContent(
            JsonSerializer.Serialize(loginRequest),
            Encoding.UTF8,
            "application/json");

        var response = await _httpClient.PostAsync("api/auth/login", content);

        if (!response.IsSuccessStatusCode)
            return false;

        var tokenResponse = await response.Content
            .ReadFromJsonAsync<TokenResponse>(_jsonOptions);

        if (tokenResponse is null)
            return false;

        // تخزين الرموز بشكل آمن
        await _secureStorage.SetAsync("access_token", tokenResponse.AccessToken);
        await _secureStorage.SetAsync("refresh_token", tokenResponse.RefreshToken);

        return true;
    }

    public async Task LogoutAsync()
    {
        _secureStorage.Remove("access_token");
        _secureStorage.Remove("refresh_token");
    }
}

public class TokenResponse
{
    public string AccessToken { get; set; } = string.Empty;
    public string RefreshToken { get; set; } = string.Empty;
    public int ExpiresIn { get; set; }
}

أنماط المرونة مع Microsoft.Extensions.Http.Resilience

هنا الأمور تصبح مثيرة حقاً. الاتصال بالشبكة في تطبيقات الهاتف غير مضمون أبداً — المستخدم قد يكون في مصعد، أو في منطقة ذات تغطية ضعيفة، أو ينتقل بين شبكة Wi-Fi وبيانات الهاتف.

حزمة Microsoft.Extensions.Http.Resilience (المبنية على Polly v8) تُوفّر خط أنابيب مرونة متكامل يتضمن خمس استراتيجيات:

  • Rate Limiter: يحدّ من عدد الطلبات المتزامنة لمنع إغراق الخادم
  • Total Request Timeout: مهلة إجمالية تشمل جميع محاولات إعادة المحاولة
  • Retry: إعادة المحاولة تلقائياً مع تأخير تصاعدي أسي (Exponential Backoff)
  • Circuit Breaker: إيقاف الطلبات مؤقتاً عند ارتفاع معدل الفشل (فكّر فيه كقاطع كهربائي يحمي النظام)
  • Attempt Timeout: مهلة لكل محاولة فردية

التكوين المخصص لطبقة المرونة

الإعدادات الافتراضية جيدة لمعظم الحالات، لكن يمكنك تخصيصها حسب احتياجاتك:

builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
})
.AddStandardResilienceHandler(options =>
{
    // تكوين إعادة المحاولة
    options.Retry.MaxRetryAttempts = 3;
    options.Retry.Delay = TimeSpan.FromSeconds(1);
    options.Retry.BackoffType = DelayBackoffType.Exponential;
    options.Retry.UseJitter = true;

    // تكوين قاطع الدائرة
    options.CircuitBreaker.FailureRatio = 0.3;
    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
    options.CircuitBreaker.MinimumThroughput = 5;
    options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);

    // تكوين المهلة الإجمالية
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);

    // تكوين مهلة كل محاولة
    options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10);
});

خيار UseJitter = true مهم بشكل خاص — يُضيف عشوائية صغيرة للتأخير بين المحاولات لتجنب ما يُعرف بـ "قطيع الرعد" (Thundering Herd) حيث تُعيد جميع الأجهزة المحاولة في نفس اللحظة.

التعامل مع حالة عدم الاتصال (Offline Mode)

تطبيقات الهاتف تحتاج أن تكون ذكية عند انقطاع الإنترنت. لا أحد يحب أن يرى شاشة خطأ فارغة! إليك نمطاً عملياً يجمع بين التخزين المؤقت والتحقق من حالة الشبكة:

public class CachedApiService : IApiService
{
    private readonly IApiService _apiService;
    private readonly IConnectivity _connectivity;
    private List<Product>? _cachedProducts;

    public CachedApiService(
        IApiService apiService,
        IConnectivity connectivity)
    {
        _apiService = apiService;
        _connectivity = connectivity;
    }

    public async Task<List<Product>?> GetProductsAsync()
    {
        if (_connectivity.NetworkAccess != NetworkAccess.Internet)
        {
            // إرجاع البيانات المخزنة مؤقتاً عند عدم الاتصال
            return _cachedProducts;
        }

        var products = await _apiService.GetProductsAsync();
        if (products is not null)
        {
            _cachedProducts = products;
        }

        return products ?? _cachedProducts;
    }

    // ... باقي العمليات
}

هذا نمط بسيط لكنه فعّال. في تطبيق إنتاجي حقيقي، قد تحتاج لتخزين البيانات في قاعدة بيانات محلية مثل SQLite بدلاً من الذاكرة فقط.

تكوين المنصات: Android و iOS

هذا القسم من التفاصيل التي قد تُضيع ساعات من وقتك إذا لم تكن على دراية بها مسبقاً.

Android — السماح بحركة HTTP غير المشفرة أثناء التطوير

بدءاً من Android 9 (API 28)، يتم حظر حركة HTTP غير المشفرة (Cleartext) افتراضياً. أثناء التطوير المحلي، ستحتاج للسماح بها لنطاقات محددة فقط.

أنشئ ملف Platforms/Android/Resources/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>

ثم أشر إليه في AndroidManifest.xml:

<application android:networkSecurityConfig="@xml/network_security_config">
    ...
</application>

تنبيه مهم: لا تستخدم أبداً android:usesCleartextTraffic="true" في بيئة الإنتاج. استخدم HTTPS دائماً للاتصالات الحقيقية.

iOS — إعدادات App Transport Security

على iOS الوضع مشابه. لتطوير محلي مع خادم HTTP، أضف الاستثناء في Platforms/iOS/Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

الربط مع ViewModel باستخدام نمط MVVM

حان الوقت لربط كل شيء معاً! لنوصل طبقة الشبكة مع واجهة المستخدم عبر ViewModel:

public partial class ProductsViewModel : ObservableObject
{
    private readonly IApiService _apiService;

    public ProductsViewModel(IApiService apiService)
    {
        _apiService = apiService;
    }

    [ObservableProperty]
    private ObservableCollection<Product> products = new();

    [ObservableProperty]
    private bool isLoading;

    [ObservableProperty]
    private string? errorMessage;

    [RelayCommand]
    private async Task LoadProductsAsync()
    {
        try
        {
            IsLoading = true;
            ErrorMessage = null;

            var result = await _apiService.GetProductsAsync();

            if (result is not null)
            {
                Products = new ObservableCollection<Product>(result);
            }
        }
        catch (HttpRequestException ex)
        {
            ErrorMessage = $"فشل تحميل البيانات: {ex.Message}";
        }
        finally
        {
            IsLoading = false;
        }
    }

    [RelayCommand]
    private async Task DeleteProductAsync(Product product)
    {
        var success = await _apiService.DeleteProductAsync(product.Id);
        if (success)
        {
            Products.Remove(product);
        }
    }
}

استخدام [ObservableProperty] و [RelayCommand] من MVVM Community Toolkit يوفّر عليك كتابة الكثير من الكود المتكرر — وهذا شيء أقدّره فعلاً في المشاريع الكبيرة.

عرض البيانات في صفحة XAML

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:MyMauiApp.ViewModels"
             x:DataType="vm:ProductsViewModel"
             x:Class="MyMauiApp.Views.ProductsPage">

    <RefreshView Command="{Binding LoadProductsCommand}"
                 IsRefreshing="{Binding IsLoading}">
        <CollectionView ItemsSource="{Binding Products}"
                        EmptyView="لا توجد منتجات">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:Product">
                    <SwipeView>
                        <SwipeView.RightItems>
                            <SwipeItems>
                                <SwipeItem Text="حذف"
                                           BackgroundColor="Red"
                                           Command="{Binding Source={RelativeSource AncestorType={x:Type vm:ProductsViewModel}}, Path=DeleteProductCommand}"
                                           CommandParameter="{Binding .}" />
                            </SwipeItems>
                        </SwipeView.RightItems>

                        <Grid Padding="16" ColumnDefinitions="60,*">
                            <Image Source="{Binding ImageUrl}"
                                   WidthRequest="50"
                                   HeightRequest="50"
                                   Aspect="AspectFill" />
                            <VerticalStackLayout Grid.Column="1" Spacing="4">
                                <Label Text="{Binding Name}"
                                       FontAttributes="Bold"
                                       FontSize="16" />
                                <Label Text="{Binding Price, StringFormat='{0:C}'}"
                                       TextColor="Gray" />
                            </VerticalStackLayout>
                        </Grid>
                    </SwipeView>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </RefreshView>

</ContentPage>

لاحظ استخدام RefreshView الذي يمنح المستخدم إمكانية السحب للتحديث — تجربة مستخدم مألوفة ومتوقعة في أي تطبيق هاتف حديث.

أفضل الممارسات والنصائح

بعد العمل مع .NET MAUI في عدة مشاريع، إليك أهم النصائح التي تعلّمتها:

  • استخدم دائماً IHttpClientFactory: لتجنب مشكلة استنزاف المقابس وضمان إدارة صحيحة لدورة حياة HttpClient
  • لا تخزّن رموز المصادقة في نص عادي: استخدم SecureStorage الذي يعتمد على Keychain في iOS و KeyStore في Android
  • فعّل طبقة المرونة: أضف AddStandardResilienceHandler() لكل عميل HTTP. سطر واحد يمنحك إعادة محاولة وقاطع دائرة تلقائياً
  • استخدم CancellationToken: مرّر رموز الإلغاء لكل طلب HTTP حتى يمكن إلغاؤه عند مغادرة المستخدم للصفحة — هذه نقطة يغفل عنها الكثيرون
  • تعامل مع الأخطاء بوضوح: ميّز بين أخطاء الشبكة (لا يوجد اتصال) وأخطاء الخادم (500) وأخطاء العميل (400) واعرض رسائل مناسبة لكل حالة
  • استخدم HTTPS دائماً في الإنتاج: إعدادات HTTP غير المشفر مخصصة فقط للتطوير المحلي، ولا مجال للتهاون في هذه النقطة
  • تجنب حظر الخيط الرئيسي: استخدم async/await لجميع استدعاءات الشبكة. لا شيء يُزعج المستخدم أكثر من واجهة متجمدة

الأسئلة الشائعة

ما الفرق بين HttpClient المُسمّى (Named) والمُحدَّد النوع (Typed) في .NET MAUI؟

العميل المُسمّى يُنشأ عبر IHttpClientFactory.CreateClient("name") ويُعطى اسماً نصياً، وهو مناسب للحالات البسيطة. أما العميل المُحدَّد النوع فيُغلّف HttpClient داخل فئة مخصصة تُحقن تلقائياً عبر DI، ويُوفّر تجريداً أنظف وأسهل في الاختبار. إذا كان مشروعك يكبر، فالنمط المُحدَّد النوع هو الخيار الأفضل بلا تردد.

كيف أتعامل مع انتهاء صلاحية رمز JWT تلقائياً؟

استخدم DelegatingHandler مخصص يعترض الاستجابات ذات الكود 401 (Unauthorized). عند اكتشاف انتهاء الصلاحية، يُرسل طلباً لتجديد الرمز (Refresh Token) ثم يُعيد الطلب الأصلي بالرمز الجديد تلقائياً. كما رأينا في المثال أعلاه، هذا النمط يجعل تجديد الرموز شفافاً تماماً بالنسبة لباقي كود التطبيق.

هل يعمل IHttpClientFactory بشكل صحيح على جميع منصات .NET MAUI؟

نعم، يعمل على جميع المنصات (Android و iOS و Windows و macOS). لكن انتبه إلى أن كل منصة تستخدم معالج HTTP أصلي مختلف — على Android يُستخدم AndroidMessageHandler وعلى iOS يُستخدم NSUrlSessionHandler افتراضياً. وهذا في الواقع شيء جيد لأنه يُوفّر أداءً أفضل من المعالج المُدار.

ما هو أفضل نهج لاختبار الوحدة مع HttpClient في .NET MAUI؟

لديك خياران رئيسيان: الأول هو استخدام نمط العميل المُحدَّد النوع مع واجهة (Interface) ثم محاكاة الواجهة في اختباراتك — وهذا الأبسط. الثاني هو استخدام MockHttpMessageHandler لمحاكاة استجابات HTTP محددة، مما يسمح باختبار تفاصيل الطلبات كالمسارات والترويسات. شخصياً، أبدأ عادةً بالنهج الأول وألجأ للثاني عند الحاجة لاختبارات أكثر دقة.

كيف أحسّن أداء طلبات HTTP في تطبيقات .NET MAUI؟

عدة أشياء تُحدث فرقاً كبيراً: استخدم IHttpClientFactory لإعادة استخدام الاتصالات، وفعّل ضغط gzip عبر HttpClientHandler.AutomaticDecompression، وقلّل حجم البيانات المنقولة باستخدام ترقيم الصفحات (Pagination) وفلترة الحقول. وأيضاً — وهذا مهم — استخدم System.Text.Json بدلاً من Newtonsoft.Json للحصول على أداء تسلسل أسرع واستهلاك ذاكرة أقل بشكل ملحوظ.

عن الكاتب Editorial Team

Our team of expert writers and editors.