REST API .NET MAUI -sovelluksessa: HttpClient, DI ja MVVM käytännössä

Rakenna REST API -integraatio .NET MAUI 10 -sovellukseen alusta loppuun. Opas kattaa HttpClientin, IHttpClientFactoryn, palvelukerroksen ja MVVM-arkkitehtuurin käytännön koodiesimerkein.

Käytännössä jokainen moderni mobiilisovellus tarvitsee yhteyttä ulkoisiin palveluihin. Käyttäjätietojen haku, tuoteluettelon lataaminen, tilausten lähettäminen — REST API -integraatio on se perusta, jonka päälle kaikki muu rakentuu.

Ja hyvä uutinen: .NET MAUI tarjoaa tähän oikeasti hyvät työkalut. Sisäänrakennettu riippuvuusinjektio, HttpClient-luokka ja natiivi verkkotuki jokaiselle alustalle. Kaikki valmiina käytettäväksi.

Tässä oppaassa rakennetaan REST API -integraatio alusta loppuun. Käydään läpi HttpClientin oikea käyttö, IHttpClientFactoryn hyödyt, palvelukerroksen rakentaminen ja lopuksi yhdistetään kaikki MVVM-arkkitehtuuriin CommunityToolkit.Mvvm-kirjastolla. Esimerkit on testattu .NET 10:n ja .NET MAUI 10:n kanssa, eli koodi on ajan tasalla.

HttpClient-luokan perusteet .NET MAUIssa

HttpClient on .NET-ekosysteemin keskeinen luokka HTTP-pyyntöjen tekemiseen. Se tukee kaikkia tavallisia HTTP-metodeja — GET, POST, PUT, PATCH ja DELETE — ja palauttaa vastaukset HttpResponseMessage-olioina.

Yksi .NET MAUIn parhaista puolista on se, että HttpClient käyttää automaattisesti kunkin alustan natiivia verkkokerrosta:

  • Android — AndroidClientHandler, joka delegoi pyynnöt natiiville Java-koodille
  • iOS/Mac Catalyst — NSUrlSessionHandler, joka hyödyntää Applen NSUrlSession-rajapintaa
  • Windows — WinHttpHandler

Eli käytännössä saat ikään kuin ilmaiseksi alustakohtaiset optimoinnit: HTTP/2-tuki, TLS-salaus ja sertifikaattien hallinta tulevat kaikki natiivikerroksesta. Ei tarvitse huolehtia niistä erikseen.

Yksinkertainen GET-pyyntö

Aloitetaan perusesimerkillä. Haetaan lista käyttäjistä JSON-muotoisesta API:sta:

using System.Net.Http.Json;

public class UserService
{
    private readonly HttpClient _httpClient;

    public UserService()
    {
        _httpClient = new HttpClient
        {
            BaseAddress = new Uri("https://api.example.com/")
        };
    }

    public async Task<List<User>?> GetUsersAsync()
    {
        return await _httpClient.GetFromJsonAsync<List<User>>("users");
    }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

GetFromJsonAsync on kätevä laajennusmetodi, joka hoitaa sekä HTTP-pyynnön että JSON-deserialisoinnin yhdellä kutsulla. Taustalla pyörii System.Text.Json, joka on huomattavasti nopeampi kuin vanhempi Newtonsoft.Json.

Miksi suoraa HttpClient-luomista kannattaa välttää

Yllä oleva esimerkki toimii kyllä ihan hyvin — mutta siinä piilee vakava ongelma. HttpClient luodaan suoraan konstruktorissa, ja tämä johtaa kahteen klassiseen ongelmaan:

  • Socket exhaustion — Jokainen HttpClient-instanssi varaa TCP-yhteyden. Kun instanssi hävitetään, soketti ei vapaudu heti. Jos luot ja hävität instansseja tiuhaan tahtiin, sokettivarasto tyhjenee ennen pitkää.
  • DNS-välimuistiongelma — Singleton-HttpClient ei päivitä DNS-resoluutiota. Jos palvelimen IP vaihtuu, sovellus jatkaa vanhaan osoitteeseen.

Olen itse törmännyt socket exhaustion -ongelmaan tuotannossa kerran, ja se oli todella turhauttava debugattava. Sovellus toimi moitteettomasti testeissä, mutta kaatui kuorman alla. Se oppi jäi kyllä mieleen.

.NET MAUIssa suositeltu ratkaisu on IHttpClientFactory tai HttpClientin rekisteröinti singleton-palveluna riippuvuusinjektiolla. Mobiilisovelluksissa, joissa puhutaan tyypillisesti yhden tai muutaman palvelimen kanssa, singleton-lähestymistapa on usein riittävä ja yksinkertaisin vaihtoehto.

Riippuvuusinjektio ja palvelurekisteröinti

.NET MAUIssa riippuvuusinjektio on sisäänrakennettu — ihan samalla tavalla kuin ASP.NET Coressa. Palvelut rekisteröidään MauiProgram.cs-tiedostossa.

Projektin NuGet-paketit

Asenna ensin nämä paketit:

dotnet add package CommunityToolkit.Mvvm
dotnet add package Microsoft.Extensions.Http

MauiProgram.cs-konfiguraatio

Sitten rekisteröidään palvelut, ViewModelit ja sivut:

using Microsoft.Extensions.Logging;

namespace MyMauiApp;

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

        // HttpClient-rekisteröinti IHttpClientFactoryn kautta
        builder.Services.AddHttpClient("ApiClient", client =>
        {
            client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
            client.DefaultRequestHeaders.Add("Accept", "application/json");
            client.Timeout = TimeSpan.FromSeconds(30);
        });

        // Palvelut
        builder.Services.AddSingleton<IPostService, PostService>();

        // ViewModelit
        builder.Services.AddTransient<PostsViewModel>();
        builder.Services.AddTransient<PostDetailViewModel>();

        // Sivut
        builder.Services.AddTransient<PostsPage>();
        builder.Services.AddTransient<PostDetailPage>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

Tässä muutama huomionarvoinen asia:

  • AddHttpClient rekisteröi nimetyn HttpClient-instanssin IHttpClientFactoryn kautta. Sokettipooli ja DNS-päivitykset hoituvat automaattisesti — sinun ei siis tarvitse miettiä niitä lainkaan.
  • IPostService on Singleton, koska se on tilaton palvelu jonka voi jakaa turvallisesti.
  • ViewModelit ja sivut ovat Transientteja, joten jokainen navigointikerta saa puhtaan instanssin.

Tietomallin määrittely

Määritellään Post-malli vastaamaan API:n JSON-rakennetta. JsonPropertyName-attribuutit varmistavat oikean mappauksen:

using System.Text.Json.Serialization;

namespace MyMauiApp.Models;

public class Post
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("userId")]
    public int UserId { get; set; }

    [JsonPropertyName("title")]
    public string Title { get; set; } = string.Empty;

    [JsonPropertyName("body")]
    public string Body { get; set; } = string.Empty;
}

public class CreatePostRequest
{
    [JsonPropertyName("title")]
    public string Title { get; set; } = string.Empty;

    [JsonPropertyName("body")]
    public string Body { get; set; } = string.Empty;

    [JsonPropertyName("userId")]
    public int UserId { get; set; }
}

API-palvelukerroksen rakentaminen

Palvelukerros eristää kaikki HTTP-kutsut omaan luokkaansa. Tämä on rehellisesti sanottuna yksi niistä arkkitehtuuripäätöksistä, joita ei koskaan kaduta — kun API:n rakenne muuttuu (ja usko pois, se muuttuu jossain vaiheessa), muutokset tehdään yhteen paikkaan eikä viiteen eri tiedostoon.

Rajapinnan määrittely

namespace MyMauiApp.Services;

public interface IPostService
{
    Task<List<Post>> GetPostsAsync();
    Task<Post?> GetPostAsync(int id);
    Task<Post?> CreatePostAsync(CreatePostRequest request);
    Task<bool> UpdatePostAsync(int id, Post post);
    Task<bool> DeletePostAsync(int id);
}

Palvelun toteutus

using System.Net.Http.Json;
using MyMauiApp.Models;

namespace MyMauiApp.Services;

public class PostService : IPostService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public PostService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    private HttpClient CreateClient() =>
        _httpClientFactory.CreateClient("ApiClient");

    public async Task<List<Post>> GetPostsAsync()
    {
        using var client = CreateClient();
        var posts = await client.GetFromJsonAsync<List<Post>>("posts");
        return posts ?? [];
    }

    public async Task<Post?> GetPostAsync(int id)
    {
        using var client = CreateClient();
        return await client.GetFromJsonAsync<Post>($"posts/{id}");
    }

    public async Task<Post?> CreatePostAsync(CreatePostRequest request)
    {
        using var client = CreateClient();
        var response = await client.PostAsJsonAsync("posts", request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<Post>();
    }

    public async Task<bool> UpdatePostAsync(int id, Post post)
    {
        using var client = CreateClient();
        var response = await client.PutAsJsonAsync($"posts/{id}", post);
        return response.IsSuccessStatusCode;
    }

    public async Task<bool> DeletePostAsync(int id)
    {
        using var client = CreateClient();
        var response = await client.DeleteAsync($"posts/{id}");
        return response.IsSuccessStatusCode;
    }
}

Jokainen metodi käyttää IHttpClientFactoryn luomaa instanssia. GetFromJsonAsync, PostAsJsonAsync ja PutAsJsonAsync hoitavat JSON-sarjallistamisen automaattisesti, joten manuaalista serialisointia ei tarvita. Koodia tulee yllättävän vähän.

ViewModel MVVM-arkkitehtuurilla

CommunityToolkit.Mvvm on rehellisesti sanottuna aika mahtava kirjasto. Se vähentää ViewModel-luokkien boilerplate-koodia dramaattisesti — lähdekoodigeneraattorit hoitavat INotifyPropertyChanged-toteutuksen, komennot ja kaiken muun toistuvan koodin puolestasi. Aika kätevää.

PostsViewModel — listan hallinta

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MyMauiApp.Models;
using MyMauiApp.Services;
using System.Collections.ObjectModel;

namespace MyMauiApp.ViewModels;

public partial class PostsViewModel : ObservableObject
{
    private readonly IPostService _postService;

    public PostsViewModel(IPostService postService)
    {
        _postService = postService;
    }

    public ObservableCollection<Post> Posts { get; } = [];

    [ObservableProperty]
    private bool _isRefreshing;

    [ObservableProperty]
    private bool _isLoading;

    [ObservableProperty]
    private string? _errorMessage;

    [RelayCommand]
    private async Task LoadPostsAsync()
    {
        if (IsLoading) return;

        try
        {
            IsLoading = true;
            ErrorMessage = null;

            var posts = await _postService.GetPostsAsync();

            Posts.Clear();
            foreach (var post in posts)
            {
                Posts.Add(post);
            }
        }
        catch (HttpRequestException ex)
        {
            ErrorMessage = $"Verkkovirhe: {ex.Message}";
        }
        catch (TaskCanceledException)
        {
            ErrorMessage = "Pyyntö aikakatkaistiin. Tarkista verkkoyhteys.";
        }
        finally
        {
            IsLoading = false;
            IsRefreshing = false;
        }
    }

    [RelayCommand]
    private async Task NavigateToDetailAsync(Post post)
    {
        await Shell.Current.GoToAsync(
            "postdetail",
            new Dictionary<string, object>
            {
                { "Post", post }
            });
    }

    [RelayCommand]
    private async Task DeletePostAsync(Post post)
    {
        var confirmed = await Shell.Current.DisplayAlert(
            "Vahvista poisto",
            $"Haluatko varmasti poistaa artikkelin \"{post.Title}\"?",
            "Poista", "Peruuta");

        if (!confirmed) return;

        var success = await _postService.DeletePostAsync(post.Id);
        if (success)
        {
            Posts.Remove(post);
        }
    }
}

PostDetailViewModel — yksittäisen artikkelin näkymä

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MyMauiApp.Models;

namespace MyMauiApp.ViewModels;

[QueryProperty(nameof(Post), "Post")]
public partial class PostDetailViewModel : ObservableObject
{
    [ObservableProperty]
    private Post? _post;

    [RelayCommand]
    private async Task GoBackAsync()
    {
        await Shell.Current.GoToAsync("..");
    }
}

[QueryProperty]-attribuutti toimii yhdessä Shell-navigoinnin kanssa. Kun navigoit postdetail-reitille ja välität Post-olion parametrina, Shell asettaa sen automaattisesti ViewModelin ominaisuuteen. Toimii siis käytännössä itsestään, mikä on aina mukavaa.

Käyttöliittymän rakentaminen XAML:lla

Nyt päästään siihen osaan, joka oikeasti näkyy käyttäjälle. Käyttöliittymä sidotaan ViewModeliin data bindingin avulla, ja tässä kannattaa käyttää compiled bindingia (x:DataType). Se parantaa suorituskykyä ja — mikä vielä tärkeämpää — havaitsee sitomisvirheet jo käännösaikana eikä vasta ajon aikana.

PostsPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:MyMauiApp.ViewModels"
             xmlns:models="clr-namespace:MyMauiApp.Models"
             x:Class="MyMauiApp.Views.PostsPage"
             x:DataType="vm:PostsViewModel"
             Title="Artikkelit">

    <Grid RowDefinitions="Auto,*">

        <!-- Virheilmoitus -->
        <Border Grid.Row="0"
                BackgroundColor="#FFF3CD"
                Stroke="#FFC107"
                StrokeShape="RoundRectangle 8"
                Padding="12"
                Margin="16,8"
                IsVisible="{Binding ErrorMessage,
                    Converter={StaticResource IsNotNullConverter}}">
            <Label Text="{Binding ErrorMessage}"
                   TextColor="#856404" />
        </Border>

        <!-- Artikkelelista -->
        <RefreshView Grid.Row="1"
                     IsRefreshing="{Binding IsRefreshing}"
                     Command="{Binding LoadPostsCommand}">
            <CollectionView ItemsSource="{Binding Posts}"
                            SelectionMode="None">
                <CollectionView.EmptyView>
                    <VerticalStackLayout
                        VerticalOptions="Center"
                        HorizontalOptions="Center"
                        Spacing="8">
                        <Label Text="Ei artikkeleita"
                               FontSize="18"
                               HorizontalTextAlignment="Center" />
                        <Button Text="Lataa uudelleen"
                                Command="{Binding LoadPostsCommand}" />
                    </VerticalStackLayout>
                </CollectionView.EmptyView>

                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="models:Post">
                        <SwipeView>
                            <SwipeView.RightItems>
                                <SwipeItems>
                                    <SwipeItem Text="Poista"
                                               BackgroundColor="Red"
                                               Command="{Binding Source={RelativeSource
                                                   AncestorType={x:Type vm:PostsViewModel}},
                                                   Path=DeletePostCommand}"
                                               CommandParameter="{Binding .}" />
                                </SwipeItems>
                            </SwipeView.RightItems>

                            <Border Stroke="#E0E0E0"
                                    StrokeShape="RoundRectangle 8"
                                    Padding="16"
                                    Margin="16,4">
                                <Border.GestureRecognizers>
                                    <TapGestureRecognizer
                                        Command="{Binding Source={RelativeSource
                                            AncestorType={x:Type vm:PostsViewModel}},
                                            Path=NavigateToDetailCommand}"
                                        CommandParameter="{Binding .}" />
                                </Border.GestureRecognizers>

                                <VerticalStackLayout Spacing="4">
                                    <Label Text="{Binding Title}"
                                           FontSize="16"
                                           FontAttributes="Bold"
                                           LineBreakMode="TailTruncation"
                                           MaxLines="2" />
                                    <Label Text="{Binding Body}"
                                           FontSize="13"
                                           TextColor="Gray"
                                           LineBreakMode="TailTruncation"
                                           MaxLines="3" />
                                </VerticalStackLayout>
                            </Border>
                        </SwipeView>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </RefreshView>

        <!-- Latausindikaattori -->
        <ActivityIndicator Grid.Row="1"
                           IsRunning="{Binding IsLoading}"
                           IsVisible="{Binding IsLoading}"
                           VerticalOptions="Center"
                           HorizontalOptions="Center" />
    </Grid>

</ContentPage>

PostsPage.xaml.cs — code-behind

namespace MyMauiApp.Views;

public partial class PostsPage : ContentPage
{
    private readonly PostsViewModel _viewModel;

    public PostsPage(PostsViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = _viewModel = viewModel;
    }

    protected override async void OnAppearing()
    {
        base.OnAppearing();

        if (_viewModel.Posts.Count == 0)
        {
            await _viewModel.LoadPostsCommand.ExecuteAsync(null);
        }
    }
}

Pieni mutta tärkeä yksityiskohta tässä: OnAppearingissa ladataan data vain jos lista on tyhjä. Tämä estää turhat API-kutsut, kun käyttäjä palaa sivulle back-navigoinnilla. Pieneltä tuntuva optimointi, mutta käyttäjäkokemus kiittää.

Virheenkäsittely ja verkkoyhteyden tarkistus

Mobiilisovelluksissa verkkoyhteys on asia, johon ei voi koskaan luottaa sataprosenttisesti. Käyttäjä saattaa olla tunnelissa, lentotilassa tai yksinkertaisesti hitaalla yhteydellä. Näihin tilanteisiin on syytä varautua.

.NET MAUI tarjoaa Connectivity-luokan yhteyden tilan tarkistamiseen.

Verkkoyhteyden tarkistus ennen API-kutsua

public async Task<List<Post>> GetPostsAsync()
{
    if (Connectivity.Current.NetworkAccess != NetworkAccess.Internet)
    {
        throw new InvalidOperationException(
            "Ei internet-yhteyttä. Tarkista verkkoyhteys ja yritä uudelleen.");
    }

    using var client = CreateClient();

    try
    {
        var posts = await client.GetFromJsonAsync<List<Post>>("posts");
        return posts ?? [];
    }
    catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
    {
        throw new UnauthorizedAccessException(
            "Istunto on vanhentunut. Kirjaudu sisään uudelleen.", ex);
    }
    catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
    {
        return [];
    }
}

Yhteyden tilan seuranta reaaliaikaisesti

Voit myös kuunnella yhteyden muutoksia sovelluksen elinkaaren aikana. Tämä on hyödyllistä etenkin silloin, jos haluat näyttää offline-bannerin automaattisesti:

public partial class PostsViewModel : ObservableObject
{
    // ... muut kentät ...

    [ObservableProperty]
    private bool _isOffline;

    public PostsViewModel(IPostService postService)
    {
        _postService = postService;

        Connectivity.Current.ConnectivityChanged += OnConnectivityChanged;
        IsOffline = Connectivity.Current.NetworkAccess != NetworkAccess.Internet;
    }

    private void OnConnectivityChanged(object? sender, ConnectivityChangedEventArgs e)
    {
        IsOffline = e.NetworkAccess != NetworkAccess.Internet;
    }
}

Autentikointi ja turvallisuus

Useimmat tuotanto-API:t vaativat autentikointia. Yleisin tapa on Bearer-token Authorization-headerissa. Tämän voi toteuttaa siististi DelegatingHandler-luokalla, ja tämä on ehdottomasti se tapa jota itse suosin:

public class AuthenticationHandler : DelegatingHandler
{
    private readonly ISecureStorageService _secureStorage;

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

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

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

        return await base.SendAsync(request, cancellationToken);
    }
}

// MauiProgram.cs — rekisteröinti
builder.Services.AddTransient<AuthenticationHandler>();
builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
})
.AddHttpMessageHandler<AuthenticationHandler>();

DelegatingHandler istuu HttpClientin middleware-ketjussa. Jokainen HTTP-pyyntö kulkee automaattisesti tämän käsittelijän läpi, ja Authorization-header lisätään ilman että palveluluokan tarvitsee tietää siitä mitään. Puhdasta arkkitehtuuria.

Suorituskyvyn optimointi

REST API -kutsujen suorituskyky vaikuttaa suoraan käyttäjäkokemukseen. Hidas API-kutsu tuntuu käyttäjästä bugiselta sovellukselta — vaikka itse sovellus olisi kuinka hyvin rakennettu. Tässä muutama tärkeä optimointivinkki.

1. Käytä CancellationTokenia

Pitkäkestoiset pyynnöt pitää voida peruuttaa. Tämä on erityisen tärkeää silloin, kun käyttäjä navigoi pois sivulta kesken latauksen — ilman peruutusta pyyntö jatkuu taustalla ihan turhaan:

[RelayCommand]
private async Task LoadPostsAsync(CancellationToken cancellationToken = default)
{
    try
    {
        var posts = await _postService.GetPostsAsync(cancellationToken);
        // ...
    }
    catch (OperationCanceledException)
    {
        // Pyyntö peruutettiin — ei toimenpiteitä
    }
}

2. Vältä tarpeetonta datan lataamista

  • Käytä sivutusta (pagination) isoissa tietomäärissä
  • Välimuistita harvoin muuttuvaa dataa
  • Lataa vain tarvittavat kentät, jos API tukee kenttävalintaa

3. Käytä System.Text.Json oikein

// Määritä JsonSerializerOptions kerran ja käytä uudelleen
private static readonly JsonSerializerOptions _jsonOptions = new()
{
    PropertyNameCaseInsensitive = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

public async Task<List<Post>> GetPostsAsync(CancellationToken ct = default)
{
    using var client = CreateClient();
    using var stream = await client.GetStreamAsync("posts", ct);
    var posts = await JsonSerializer.DeserializeAsync<List<Post>>(
        stream, _jsonOptions, ct);
    return posts ?? [];
}

Stream-pohjainen deserialisointi on yllättävän iso juttu suorituskyvyn kannalta. Koko JSON-vastausta ei tarvitse ladata muistiin ensin, vaan deserialisointi tapahtuu sitä mukaa kun dataa saapuu. Isolla datamäärällä ero on merkittävä — ja muistinkäyttö pysyy kurissa.

Paikallisen kehitysympäristön konfigurointi

Jos kehität API:a paikallisesti ASP.NET Corella, tulet todennäköisesti törmäämään siihen, ettei emulaattori löydä localhost-osoitetta. Tämä on klassinen "miksi tämä ei toimi" -hetki, joka hämmentää monia kehittäjiä ensimmäisellä kerralla (itseäni mukaan lukien).

Selitys on kuitenkin yksinkertainen: Android-emulaattori tulkitsee localhostin omaksi verkko-osoitteekseen, ei isäntäkoneen osoitteeksi.

  • Android-emulaattori — Käytä osoitetta 10.0.2.2 localhostin sijaan
  • iOS-simulaattorilocalhost toimii suoraan

Android-sovelluksessa täytyy myös sallia selkokielinen HTTP-liikenne kehitysympäristöä varten:

<!-- 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-config>
</network-security-config>

Usein kysytyt kysymykset

Pitäisikö käyttää IHttpClientFactorya vai singleton-HttpClientia .NET MAUIssa?

IHttpClientFactory on suositeltu tapa, koska se hoitaa sokettipoolin ja DNS-päivitykset automaattisesti. Mobiilisovelluksissa, joissa kommunikoidaan tyypillisesti vain muutaman palvelimen kanssa, myös singleton-HttpClient toimii hyvin. Oleellisinta on, ettei HttpClient-instansseja luoda ja hävitetä jokaisessa API-kutsussa.

Miten käsitellään JSON-deserialisointi, kun API:n kenttänimet eroavat C#-malleista?

Käytä JsonPropertyName-attribuuttia mallien ominaisuuksissa tai konfiguroi JsonSerializerOptions-olion PropertyNamingPolicy esimerkiksi CamelCase-asetuksella. Vaihtoehtoisesti PropertyNameCaseInsensitive = true ohittaa kirjainkoon erot kokonaan.

Miten testataan API-palvelukerros yksikkötesteillä?

Koska palvelukerros käyttää IPostService-rajapintaa, mock-toteutuksen luominen testeissä on suoraviivaista. Moq tai NSubstitute toimivat hyvin rajapinnan mockkaamiseen. HttpClient-kutsuja varten MockHttpMessageHandler palauttaa ennalta määritellyt vastaukset ilman oikeaa verkkoliikennettä.

Miten .NET MAUI -sovellus käyttäytyy, kun verkkoyhteys katkeaa kesken API-kutsun?

HttpClient heittää HttpRequestException- tai TaskCanceledException-poikkeuksen. Tarkista verkkoyhteys Connectivity.Current.NetworkAccess-ominaisuudella ennen kutsua, käsittele poikkeukset siististi ja näytä käyttäjälle selkeä virheilmoitus. Kukaan ei halua nähdä stack tracea.

Voinko käyttää Refit-kirjastoa .NET MAUIssa?

Ehdottomasti. Refit toimii loistavasti .NET MAUIssa ja generoi HttpClient-kutsut automaattisesti rajapintamäärittelyn perusteella. Asenna Refit.HttpClientFactory-paketti ja rekisteröi rajapinta AddRefitClient-metodilla MauiProgram.cs-tiedostossa. Se vähentää boilerplate-koodia merkittävästi.

Tietoa Kirjoittajasta Editorial Team

Our team of expert writers and editors.