Previous MAUI-Cancellation-Token MAUI-Developer-Tools Next

Complete REST API Consumer in .NET MAUI

🌐 Complete REST API Consumer in .NET MAUI

CRUD operations, search, retry logic with exponential backoff, and cancellation support. This gives you a single, complete reference implementation for consuming REST APIs in .NET MAUI.

📂 Project Structure

MyMauiApp/
│
├── Models/
│   └── Post.cs
│
├── Services/
│   └── ApiService.cs
│
├── ViewModels/
│   └── PostViewModel.cs
│
├── Views/
│   └── PostPage.xaml
│   └── PostPage.xaml.cs
│
├── MauiProgram.cs
└── App.xaml / App.xaml.cs

🧩 Code Walkthrough

1. Models/Post.cs

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}

2. Services/ApiService.cs (CRUD + retry + cancellation)

using System.Net.Http.Json;

public class ApiService
{
    private readonly HttpClient _httpClient;

    public ApiService(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    }

    private async Task<T> ExecuteWithRetry<T>(Func<Task<T>> operation, int maxRetries = 3, CancellationToken token = default)
    {
        int delay = 1000;
        for (int attempt = 1; attempt <= maxRetries; attempt++)
        {
            try
            {
                return await operation();
            }
            catch (HttpRequestException) when (attempt < maxRetries)
            {
                await Task.Delay(delay, token);
                delay *= 2;
            }
        }
        throw new Exception("Operation failed after retries.");
    }

    public async Task<List<Post>> GetPostsAsync(CancellationToken token) =>
    await ExecuteWithRetry(async () =>
    await _httpClient.GetFromJsonAsync<List<Post>>("posts", token), token: token);

    public async Task<Post> CreatePostAsync(Post newPost, CancellationToken token) =>
    await ExecuteWithRetry(async () =>
    {
        var response = await _httpClient.PostAsJsonAsync("posts", newPost, token);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<Post>(cancellationToken: token);
    }, token: token);

    public async Task<Post> UpdatePostAsync(Post updatedPost, CancellationToken token) =>
    await ExecuteWithRetry(async () =>
    {
        var response = await _httpClient.PutAsJsonAsync($"posts/{updatedPost.Id}", updatedPost, token);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<Post>(cancellationToken: token);
    }, token: token);

    public async Task DeletePostAsync(int id, CancellationToken token) =>
    await ExecuteWithRetry(async () =>
    {
        var response = await _httpClient.DeleteAsync($"posts/{id}", token);
        response.EnsureSuccessStatusCode();
        return true;
    }, token: token);
}

3. ViewModels/PostViewModel.cs

using System.Collections.ObjectModel;

public class PostViewModel : BindableObject
{
    private readonly ApiService _api;
    private CancellationTokenSource _cts;

    public ObservableCollection<Post> Posts { get; } = new();
    private string _errorMessage;
    public string ErrorMessage
    {
        get => _errorMessage;
        set { _errorMessage = value; OnPropertyChanged(); }
    }

    public PostViewModel(ApiService api)
    {
        _api = api;
        LoadPosts();
    }

    public async Task LoadPosts()
    {
        _cts = new CancellationTokenSource();
        try
        {
            var posts = await _api.GetPostsAsync(_cts.Token);
            Posts.Clear();
            foreach (var post in posts)
                Posts.Add(post);
            ErrorMessage = string.Empty;
        }
        catch (OperationCanceledException)
        {
            ErrorMessage = "Request canceled.";
        }
        catch (Exception ex)
        {
            ErrorMessage = $"Could not load posts: {ex.Message}";
        }
    }

    public async Task AddPost(string title, string body)
    {
        _cts = new CancellationTokenSource();
        try
        {
            await _api.CreatePostAsync(new Post { Title = title, Body = body }, _cts.Token);
            await LoadPosts();
        }
        catch (Exception ex)
        {
            ErrorMessage = $"Failed to add post: {ex.Message}";
        }
    }

    public async Task DeletePost(Post post)
    {
        _cts = new CancellationTokenSource();
        try
        {
            await _api.DeletePostAsync(post.Id, _cts.Token);
            await LoadPosts();
        }
        catch (Exception ex)
        {
            ErrorMessage = $"Failed to delete post: {ex.Message}";
        }
    }

    public void CancelRequest() => _cts?.Cancel();
}

4. Views/PostPage.xaml

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             x:Class="MyMauiApp.Views.PostPage"
             Title="Posts">

    <VerticalStackLayout Padding="20">
        <!-- Add new post -->
        <Entry x:Name="TitleEntry" Placeholder="Title" />
        <Entry x:Name="BodyEntry" Placeholder="Body" />
        <Button Text="Add Post" Clicked="OnAddClicked" />

        <!-- Cancel -->
        <Button Text="Cancel Request" Clicked="OnCancelClicked" />

        <!-- Error message -->
        <Label Text="{Binding ErrorMessage}" TextColor="Red" />

        <!-- List of posts -->
        <CollectionView ItemsSource="{Binding Posts}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <StackLayout Padding="5">
                        <Label Text="{Binding Title}" FontAttributes="Bold" />
                        <Label Text="{Binding Body}" />
                        <Button Text="Delete" CommandParameter="{Binding .}" />
                    </StackLayout>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </VerticalStackLayout>
</ContentPage>

5. Views/PostPage.xaml.cs

public partial class PostPage : ContentPage
{
    private readonly PostViewModel _vm;

    public PostPage(PostViewModel vm)
    {
        InitializeComponent();
        BindingContext = _vm = vm;
    }

    private async void OnAddClicked(object sender, EventArgs e)
    {
        if (!string.IsNullOrWhiteSpace(TitleEntry.Text) && !string.IsNullOrWhiteSpace(BodyEntry.Text))
        {
            await _vm.AddPost(TitleEntry.Text, BodyEntry.Text);
            TitleEntry.Text = string.Empty;
            BodyEntry.Text = string.Empty;
        }
    }

    private void OnCancelClicked(object sender, EventArgs e)
    {
        _vm.CancelRequest();
    }
}

6. MauiProgram.cs

builder.Services.AddHttpClient();
builder.Services.AddTransient();
builder.Services.AddTransient();

✅ Features in This Demo

  • CRUD: Create, Read, Update, Delete posts.
  • Search: Filter posts (can be added easily with a search method).
  • Retry Logic: Automatic retries with exponential backoff.
  • Cancellation: Cancel ongoing requests with a button.
  • Error Handling: User-friendly messages for failures.

This is now a complete reference implementation you can drop into your MAUI solution.


➕ Additional Enhancements You Can Add

  • 🔍 Live Search using Entry TextChanged event
  • 📶 Offline Detection using IConnectivity
  • 💾 SQLite caching for offline-first architecture
  • 📊 Logging with ILogger
  • 🧠 Polly for retry + timeout + circuit breaker policies
  • ⚡ Background sync using MAUI Essentials

🚀 Enterprise-Grade Improvements

  • MVVM Toolkit integration
  • API response caching
  • Secure token-based authentication
  • Centralized error handling middleware
  • Automatic retry + fallback strategy
Back to Index
Previous MAUI-Cancellation-Token MAUI-Developer-Tools Next
*