اتصال به REST API در .NET MAUI: راهنمای جامع با HttpClient، Refit و Polly

آموزش اتصال اپلیکیشن .NET MAUI به REST API با HttpClient، IHttpClientFactory، Refit و Polly. مدیریت Socket Exhaustion، احراز هویت، خطاهای شبکه و بهینه‌سازی عملکرد — همه با مثال‌های عملی و معماری MVVM.

اگه مقاله‌های قبلی ما درباره معماری MVVM و ذخیره‌سازی محلی با SQLite رو خونده باشید، احتمالاً الان یه اپلیکیشن .NET MAUI دارید که ساختار تمیزی داره و داده‌ها رو لوکال ذخیره می‌کنه. ولی خب، واقعیت اینه که اکثر اپ‌های موبایل مدرن باید با یه سرور حرف بزنن — از گرفتن لیست محصولات گرفته تا لاگین کردن کاربر و سینک کردن دیتا.

توی این مقاله قراره با هم یاد بگیریم چطوری اپ .NET MAUI رو به یه REST API وصل کنیم. از HttpClient ساده شروع می‌کنیم، بعد با IHttpClientFactory حرفه‌ای‌ترش می‌کنیم، با Refit کدهای تکراری رو حذف می‌کنیم، و آخرش با Polly اپ رو در برابر قطعی‌های شبکه مقاوم می‌کنیم. همه چیز هم با مثال عملی و سازگار با معماری MVVM هست.

پیش‌نیازها و ابزارهای مورد نیاز

قبل از شروع، مطمئن بشید اینا رو دارید:

  • .NET 10 SDK یا بالاتر (نسخه .NET 10.0.200 یا جدیدتر توصیه می‌شه)
  • Visual Studio 2022 v17.14+ یا JetBrains Rider 2025.3+
  • آشنایی اولیه با معماری MVVM و تزریق وابستگی در .NET MAUI
  • یه REST API آزمایشی (مثل JSONPlaceholder یا هر API شخصی که دم دست دارید)

چرا مدیریت درست HTTP در موبایل مهم‌تر از وب هست؟

وقتی داریم اپ موبایل می‌سازیم، شرایط شبکه با اپ تحت وب زمین تا آسمون فرق داره. کاربر ممکنه توی مترو باشه و نت هر چند ثانیه قطع و وصل بشه. یا داره از 4G به Wi-Fi سوییچ می‌کنه.

به همین دلیل چند تا نکته خیلی مهم می‌شه:

  • مصرف سوکت باید مدیریت بشه وگرنه Socket Exhaustion اتفاق میفته
  • خطاهای گذرا (Transient Faults) باید اتوماتیک هندل بشن
  • وضعیت اتصال باید قبل از ارسال درخواست چک بشه
  • تایم‌اوت‌ها باید متناسب با شبکه موبایل تنظیم بشن

روش اول: استفاده مستقیم از HttpClient

خب، ساده‌ترین راه برای وصل شدن به REST API استفاده از کلاس HttpClient هست. بیاید یه سرویس ساده بسازیم:

// Models/TodoItem.cs
public class TodoItem
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public string Title { get; set; } = string.Empty;
    public bool Completed { get; set; }
}
// Services/ITodoService.cs
public interface ITodoService
{
    Task<List<TodoItem>> GetAllAsync();
    Task<TodoItem?> GetByIdAsync(int id);
    Task<TodoItem> CreateAsync(TodoItem item);
    Task<bool> UpdateAsync(TodoItem item);
    Task<bool> DeleteAsync(int id);
}
// Services/TodoService.cs
using System.Net.Http.Json;

public class TodoService : ITodoService
{
    private readonly HttpClient _httpClient;

    public TodoService()
    {
        _httpClient = new HttpClient
        {
            BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
        };
        _httpClient.DefaultRequestHeaders.Accept.Add(
            new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
    }

    public async Task<List<TodoItem>> GetAllAsync()
    {
        var response = await _httpClient.GetAsync("todos");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<List<TodoItem>>() ?? [];
    }

    public async Task<TodoItem?> GetByIdAsync(int id)
    {
        return await _httpClient.GetFromJsonAsync<TodoItem>($"todos/{id}");
    }

    public async Task<TodoItem> CreateAsync(TodoItem item)
    {
        var response = await _httpClient.PostAsJsonAsync("todos", item);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<TodoItem>()
            ?? throw new InvalidOperationException("Failed to deserialize response.");
    }

    public async Task<bool> UpdateAsync(TodoItem item)
    {
        var response = await _httpClient.PutAsJsonAsync($"todos/{item.Id}", item);
        return response.IsSuccessStatusCode;
    }

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

مشکل Socket Exhaustion: چرا این روش خطرناکه؟

کد بالا کار می‌کنه، ولی یه مشکل جدی داره که خیلیا ازش غافلن. اگه هر بار یه نمونه جدید از HttpClient بسازید (مثلاً توی هر صفحه یا هر سرویس)، کلی سوکت باز می‌شه و بسته نمی‌شه. این همون مشکل معروف Socket Exhaustion هست که می‌تونه اپ رو کاملاً فلج کنه.

حتی اگه HttpClient رو Dispose کنید، سوکت زیرین فوری آزاد نمی‌شه و ممکنه تا ۴ دقیقه توی وضعیت TIME_WAIT بمونه. راستش من خودم یه بار توی یه پروژه به این مشکل برخوردم و دیباگ کردنش خیلی وقت‌گیر بود.

روش دوم: استفاده از IHttpClientFactory (روش توصیه‌شده)

IHttpClientFactory مشکل Socket Exhaustion رو با مدیریت یه Pool از HttpMessageHandler ها حل می‌کنه. بیاید سرویس‌مون رو ارتقا بدیم:

// Services/TodoService.cs (بهبود یافته با IHttpClientFactory)
using System.Net.Http.Json;

public class TodoService : ITodoService
{
    private readonly HttpClient _httpClient;

    public TodoService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<List<TodoItem>> GetAllAsync()
    {
        return await _httpClient.GetFromJsonAsync<List<TodoItem>>("todos") ?? [];
    }

    public async Task<TodoItem?> GetByIdAsync(int id)
    {
        return await _httpClient.GetFromJsonAsync<TodoItem>($"todos/{id}");
    }

    public async Task<TodoItem> CreateAsync(TodoItem item)
    {
        var response = await _httpClient.PostAsJsonAsync("todos", item);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<TodoItem>()
            ?? throw new InvalidOperationException("خطا در دریافت پاسخ سرور");
    }

    public async Task<bool> UpdateAsync(TodoItem item)
    {
        var response = await _httpClient.PutAsJsonAsync($"todos/{item.Id}", item);
        return response.IsSuccessStatusCode;
    }

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

ثبت سرویس در MauiProgram.cs

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

        // ثبت Typed HttpClient برای TodoService
        builder.Services.AddHttpClient<ITodoService, TodoService>(client =>
        {
            client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
            client.DefaultRequestHeaders.Accept.Add(
                new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
            client.Timeout = TimeSpan.FromSeconds(30);
        });

        // ثبت ViewModels و Pages
        builder.Services.AddTransient<TodoListViewModel>();
        builder.Services.AddTransient<TodoListPage>();

        return builder.Build();
    }
}

با این روش، IHttpClientFactory مدیریت lifecycle سوکت‌ها رو خودش انجام می‌ده و دیگه لازم نیست نگران Socket Exhaustion باشید. یه تغییر کوچیک، ولی تأثیرش خیلی بزرگه.

روش سوم: استفاده از Refit برای حذف کد تکراری

اگه یه نگاه به سرویس بالا بندازید، می‌بینید که کلی کد تکراری برای serialize و deserialize کردن JSON نوشتیم. اینجاست که Refit وارد بازی می‌شه — یه کتابخانه فوق‌العاده‌ست که با تعریف یه interface ساده، تمام کد HTTP رو خودش تولید می‌کنه.

صادقانه بگم، از وقتی با Refit آشنا شدم دیگه برنگشتم سراغ نوشتن دستی کدهای HTTP.

نصب پکیج Refit

dotnet add package Refit.HttpClientFactory

تعریف API Interface

// Services/ITodoApi.cs
using Refit;

public interface ITodoApi
{
    [Get("/todos")]
    Task<List<TodoItem>> GetAllAsync();

    [Get("/todos/{id}")]
    Task<TodoItem> GetByIdAsync(int id);

    [Post("/todos")]
    Task<TodoItem> CreateAsync([Body] TodoItem item);

    [Put("/todos/{id}")]
    Task<ApiResponse<TodoItem>> UpdateAsync(int id, [Body] TodoItem item);

    [Delete("/todos/{id}")]
    Task DeleteAsync(int id);
}

ببینید چقدر تمیزه! فقط یه interface با چند تا attribute و تمام. Refit بقیه کار رو انجام می‌ده.

ثبت Refit Client در DI Container

// MauiProgram.cs
using Refit;

builder.Services
    .AddRefitClient<ITodoApi>()
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
        client.Timeout = TimeSpan.FromSeconds(30);
    });

حالا می‌تونید ITodoApi رو مستقیماً توی ViewModel تزریق کنید:

// ViewModels/TodoListViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

public partial class TodoListViewModel : ObservableObject
{
    private readonly ITodoApi _todoApi;

    public TodoListViewModel(ITodoApi todoApi)
    {
        _todoApi = todoApi;
    }

    [ObservableProperty]
    public partial ObservableCollection<TodoItem> Todos { get; set; } = [];

    [ObservableProperty]
    public partial bool IsLoading { get; set; }

    [ObservableProperty]
    public partial string? ErrorMessage { get; set; }

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

            var items = await _todoApi.GetAllAsync();
            Todos = new ObservableCollection<TodoItem>(items.Take(20));
        }
        catch (HttpRequestException ex)
        {
            ErrorMessage = $"خطا در اتصال به سرور: {ex.Message}";
        }
        catch (Refit.ApiException ex)
        {
            ErrorMessage = $"خطای API: {ex.StatusCode} - {ex.Message}";
        }
        finally
        {
            IsLoading = false;
        }
    }

    [RelayCommand]
    private async Task DeleteTodoAsync(TodoItem item)
    {
        try
        {
            await _todoApi.DeleteAsync(item.Id);
            Todos.Remove(item);
        }
        catch (Exception ex)
        {
            ErrorMessage = $"خطا در حذف: {ex.Message}";
        }
    }
}

مدیریت احراز هویت با DelegatingHandler

خب تا اینجا همه چیز خوب پیش رفت. ولی توی دنیای واقعی، اکثر APIها نیاز به توکن احراز هویت دارن. بهترین روش برای هندل کردن این قضیه، استفاده از DelegatingHandler هست — چیزی شبیه یه middleware که قبل از هر درخواست اجرا می‌شه:

// Handlers/AuthHeaderHandler.cs
public class AuthHeaderHandler : DelegatingHandler
{
    private readonly ISecureStorageService _secureStorage;

    public AuthHeaderHandler(ISecureStorageService secureStorage)
    {
        _secureStorage = secureStorage;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await _secureStorage.GetTokenAsync();
        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}
// ثبت در MauiProgram.cs
builder.Services.AddTransient<AuthHeaderHandler>();

builder.Services
    .AddRefitClient<ITodoApi>()
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("https://your-api.com");
    })
    .AddHttpMessageHandler<AuthHeaderHandler>();

با این روش توکن اتوماتیک به هر درخواست اضافه می‌شه و نیازی نیست توی هر متد دستی هدر رو ست کنید. تمیز و قابل نگهداری.

افزودن مقاومت با Polly: وقتی شبکه قاطی می‌کنه

شبکه موبایل ذاتاً غیرقابل اعتماده. اتصال ممکنه هر لحظه قطع بشه و برگرده. Polly بهتون اجازه می‌ده سیاست‌های مقاومتی تعریف کنید تا اپ به صورت خودکار خطاهای موقتی رو مدیریت کنه.

این یکی از اون تکنیک‌هاییه که بعد از استفاده ازش می‌گید «چرا زودتر این کار رو نکردم؟»

نصب پکیج

dotnet add package Microsoft.Extensions.Http.Resilience

پیکربندی استراتژی مقاومت

// MauiProgram.cs
builder.Services
    .AddRefitClient<ITodoApi>()
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("https://your-api.com");
    })
    .AddHttpMessageHandler<AuthHeaderHandler>()
    .AddStandardResilienceHandler(options =>
    {
        // تنظیم Retry: 3 بار تلاش مجدد با backoff نمایی
        options.Retry.MaxRetryAttempts = 3;
        options.Retry.Delay = TimeSpan.FromSeconds(1);
        options.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
        options.Retry.UseJitter = true;

        // تنظیم تایم‌اوت کلی
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);

        // تنظیم Circuit Breaker
        options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(60);
        options.CircuitBreaker.FailureRatio = 0.5;
        options.CircuitBreaker.MinimumThroughput = 10;
        options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
    });

بذارید ساده توضیح بدم هر کدوم از اینا چیکار می‌کنن:

  • Retry: اگه درخواست شکست بخوره، تا ۳ بار با فاصله نمایی (تقریباً 1، 2 و 4 ثانیه) دوباره تلاش می‌کنه
  • Jitter: یه مقدار تصادفی به فاصله بین تلاش‌ها اضافه می‌شه تا اگه هزار تا کلاینت همزمان ریکوئست بزنن، همشون با هم retry نکنن (مشکل معروف Thundering Herd)
  • Circuit Breaker: اگه بیش از ۵۰٪ درخواست‌ها fail بشن، مدار قطع می‌شه و ۳۰ ثانیه اصلاً درخواستی ارسال نمی‌شه — این از فشار بیخودی به سرور جلوگیری می‌کنه
  • Timeout: هر درخواست حداکثر ۳۰ ثانیه فرصت داره

بررسی وضعیت اتصال شبکه

قبل از ارسال هر درخواست HTTP، عاقلانه‌ست که اول وضعیت اینترنت رو چک کنید. .NET MAUI یه API داخلی به اسم Connectivity داره که دقیقاً همین کار رو می‌کنه:

// Services/IConnectivityService.cs
public interface IConnectivityService
{
    bool IsConnected { get; }
    event EventHandler<ConnectivityChangedEventArgs> ConnectivityChanged;
}

// Services/ConnectivityService.cs
public class ConnectivityService : IConnectivityService
{
    public bool IsConnected =>
        Connectivity.Current.NetworkAccess == NetworkAccess.Internet;

    public event EventHandler<ConnectivityChangedEventArgs>? ConnectivityChanged
    {
        add => Connectivity.Current.ConnectivityChanged += value;
        remove => Connectivity.Current.ConnectivityChanged -= value;
    }
}
// استفاده در ViewModel
[RelayCommand]
private async Task LoadTodosAsync()
{
    if (!_connectivityService.IsConnected)
    {
        ErrorMessage = "اتصال اینترنت برقرار نیست. لطفاً اتصال خود را بررسی کنید.";
        return;
    }

    try
    {
        IsLoading = true;
        var items = await _todoApi.GetAllAsync();
        Todos = new ObservableCollection<TodoItem>(items);
    }
    catch (HttpRequestException)
    {
        ErrorMessage = "خطا در برقراری ارتباط با سرور.";
    }
    finally
    {
        IsLoading = false;
    }
}

نکته مهم: Connectivity.Current فقط وضعیت فعلی رو بهتون می‌گه. ممکنه بین چک کردن و ارسال درخواست، اتصال قطع بشه. پس همیشه try-catch هم داشته باشید.

استفاده از Stream برای Deserialization بهینه

وقتی حجم داده‌ها زیاده (مثلاً لیست هزاران آیتم)، بجای خوندن کل response به صورت string و بعد deserialize کردن، از Stream استفاده کنید. اینطوری مصرف حافظه خیلی کمتر می‌شه:

// روش بهینه با Stream
public async Task<List<TodoItem>> GetAllOptimizedAsync()
{
    var response = await _httpClient.GetAsync("todos",
        HttpCompletionOption.ResponseHeadersRead);

    response.EnsureSuccessStatusCode();

    await using var stream = await response.Content.ReadAsStreamAsync();
    return await JsonSerializer.DeserializeAsync<List<TodoItem>>(stream,
        new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        }) ?? [];
}

این تکنیک مخصوصاً وقتی لیست‌های بزرگ از سرور می‌گیرید اهمیت پیدا می‌کنه. بجای لود کردن کل JSON توی RAM، داده‌ها به صورت stream پردازش می‌شن و حافظه خیلی کمتری مصرف می‌شه.

نکات مهم برای اتصال به API محلی از شبیه‌ساز

اگه دارید با یه API محلی کار می‌کنید (مثلاً ASP.NET Core Web API روی localhost)، باید بدونید که شبیه‌سازها مستقیماً به localhost دسترسی ندارن. این یکی از اون مشکلاتیه که معمولاً تازه‌کارها رو سردرگم می‌کنه:

  • Android Emulator: بجای localhost از 10.0.2.2 استفاده کنید
  • iOS Simulator: خود localhost کار می‌کنه، ولی HTTPS رو باید درست کانفیگ کنید
  • برای توسعه، شاید لازم باشه clear-text HTTP رو فعال کنید (البته فقط توی حالت دیباگ!)
// تنظیم platform-specific BaseAddress
#if ANDROID
    private const string BaseUrl = "https://10.0.2.2:5001";
#elif IOS
    private const string BaseUrl = "https://localhost:5001";
#else
    private const string BaseUrl = "https://localhost:5001";
#endif

ساختار نهایی پروژه

وقتی تمام اجزا رو کنار هم بذارید، ساختار پروژه باید چیزی شبیه این باشه:

MyMauiApp/
├── Models/
│   └── TodoItem.cs
├── Services/
│   ├── ITodoApi.cs          (Refit interface)
│   ├── IConnectivityService.cs
│   └── ConnectivityService.cs
├── Handlers/
│   └── AuthHeaderHandler.cs
├── ViewModels/
│   └── TodoListViewModel.cs
├── Views/
│   └── TodoListPage.xaml(.cs)
├── MauiProgram.cs
└── App.xaml(.cs)

مقایسه سه روش: کدوم رو انتخاب کنم؟

ویژگیHttpClient مستقیمIHttpClientFactoryRefit + Factory
حجم کدزیادمتوسطکم
Type Safetyدستیدستیخودکار
مدیریت سوکت❌ خطرناک✅ خودکار✅ خودکار
پشتیبانی از Polly❌ دستی✅ یکپارچه✅ یکپارچه
مناسب براینمونه‌های سادهپروژه‌های متوسطپروژه‌های حرفه‌ای
سازگاری با NativeAOT✅ (از Refit 7+)

توصیه من: اگه پروژه جدید شروع می‌کنید، بدون فکر برید سراغ ترکیب Refit + IHttpClientFactory + Polly. این ترکیب بهترین تعادل بین کد تمیز، پرفورمنس بالا و مقاومت در برابر خطا رو بهتون می‌ده. توی پروژه‌های واقعی هم اثبات شده و مشکلی باهاش نداشتم.

سوالات متداول

آیا استفاده از IHttpClientFactory در .NET MAUI ضروری است؟

بله، به شدت توصیه می‌شه. اگه مستقیماً HttpClient بسازید و dispose کنید، با مشکل Socket Exhaustion مواجه می‌شید. IHttpClientFactory با مدیریت Pool از Handler ها این مشکل رو حل می‌کنه. تنها استثنا اپ‌های خیلی ساده‌ست که فقط چند تا درخواست HTTP دارن و بس.

تفاوت Refit و HttpClient چیست و کی از کدام استفاده کنم؟

Refit در واقع یه لایه انتزاع (abstraction) روی HttpClient هست. با تعریف یه interface، کل کد HTTP شامل ساخت URL، serialize/deserialize و مدیریت header به صورت خودکار تولید می‌شه. وقتی API مشخص و ساختارمندی دارید از Refit استفاده کنید. اگه نیاز به کنترل دقیق‌تر روی درخواست‌ها دارید (مثلاً streaming یا custom content type) از HttpClient مستقیم استفاده کنید.

چطور از اپلیکیشن MAUI به API محلی روی localhost متصل بشم؟

شبیه‌ساز Android مستقیماً به localhost دسترسی نداره. بجاش از آدرس 10.0.2.2 استفاده کنید که به localhost ماشین میزبان ریدایرکت می‌شه. برای iOS Simulator می‌تونید از localhost استفاده کنید. یادتون باشه که ممکنه لازم بشه clear-text HTTP رو توی تنظیمات Android فعال کنید.

آیا Polly برای اپلیکیشن موبایل واقعاً لازمه؟

صد در صد، حتی بیشتر از اپ‌های سمت سرور! شبکه موبایل ذاتاً ناپایداره و کاربر انتظار داره اپ بدون مشکل کار کنه. Polly با retry خودکار، circuit breaker و timeout مانع تجربه کاربری بد می‌شه. بدون Polly باید تمام این منطق رو خودتون بنویسید — که هم زمان‌بره و هم احتمال باگ داره.

چطور می‌تونم درخواست‌های HTTP رو در .NET MAUI دیباگ کنم؟

چند تا راه هست: ۱) از HttpClientHandler با فعال کردن logging استفاده کنید، ۲) از ابزار Charles Proxy یا mitmproxy برای مانیتور کردن ترافیک شبکه استفاده کنید، ۳) یه DelegatingHandler سفارشی بنویسید که request و response رو log کنه. روش سوم از نظر من بهترین انتخابه چون کاملاً توی pipeline خودتون قرار می‌گیره.

درباره نویسنده Editorial Team

Our team of expert writers and editors.