AgentSkillsCN

maui-rest-api

.NET MAUI 应用中消费 REST API 的指南。涵盖 HttpClient 的 System.Text.Json 配置、DI 注册、服务接口/实现模式、完整的 CRUD 操作(GET、POST、PUT、DELETE)、错误处理、平台特定的明文流量配置,以及 async/await 的最佳实践。适用于在 MAUI 项目中添加 API 调用、创建数据服务,或对接 HttpClient 时使用。

SKILL.md
--- frontmatter
name: maui-rest-api
description: >
  Guidance for consuming REST APIs in .NET MAUI apps. Covers HttpClient setup
  with System.Text.Json, DI registration, service interface/implementation
  pattern, full CRUD operations (GET, POST, PUT, DELETE), error handling,
  platform-specific clear-text traffic configuration, and async/await best
  practices. Use when adding API calls, creating data services, or wiring up
  HttpClient in a MAUI project.

Consuming REST APIs in .NET MAUI

HttpClient & JSON setup

Always configure a shared JsonSerializerOptions with camel-case naming:

csharp
private static readonly JsonSerializerOptions _jsonOptions = new()
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    PropertyNameCaseInsensitive = true
};

DI registration

Register HttpClient as a singleton or use IHttpClientFactory. Set BaseAddress once:

csharp
// MauiProgram.cs
builder.Services.AddSingleton(sp => new HttpClient
{
    BaseAddress = new Uri("https://api.example.com")
});
builder.Services.AddSingleton<IMyApiService, MyApiService>();

For more control, use the factory pattern:

csharp
builder.Services.AddHttpClient<IMyApiService, MyApiService>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
});

Service interface + implementation

Define a clean interface for each API resource:

csharp
public interface IMyApiService
{
    Task<List<Item>> GetItemsAsync();
    Task<Item?> GetItemAsync(int id);
    Task<Item?> CreateItemAsync(Item item);
    Task<bool> UpdateItemAsync(Item item);
    Task<bool> DeleteItemAsync(int id);
}

Implement the interface, injecting HttpClient:

csharp
public class MyApiService : IMyApiService
{
    private readonly HttpClient _httpClient;

    private static readonly JsonSerializerOptions _jsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true
    };

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

CRUD operations

GET (list)

csharp
    public async Task<List<Item>> GetItemsAsync()
    {
        var response = await _httpClient.GetAsync("api/items");
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<List<Item>>(content, _jsonOptions) ?? [];
    }

GET (single)

csharp
    public async Task<Item?> GetItemAsync(int id)
    {
        var response = await _httpClient.GetAsync($"api/items/{id}");
        if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
            return null;
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<Item>(content, _jsonOptions);
    }

POST (create)

csharp
    public async Task<Item?> CreateItemAsync(Item item)
    {
        var json = JsonSerializer.Serialize(item, _jsonOptions);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync("api/items", content);
        if (!response.IsSuccessStatusCode)
            return null;
        var responseBody = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<Item>(responseBody, _jsonOptions);
    }

PUT (update)

csharp
    public async Task<bool> UpdateItemAsync(Item item)
    {
        var json = JsonSerializer.Serialize(item, _jsonOptions);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _httpClient.PutAsync($"api/items/{item.Id}", content);
        return response.IsSuccessStatusCode;
    }

DELETE

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

Error handling

Check IsSuccessStatusCode or call EnsureSuccessStatusCode() depending on the scenario:

  • Use EnsureSuccessStatusCode() when failure is unexpected (throws HttpRequestException).
  • Use IsSuccessStatusCode when you need to branch on specific status codes.

Wrap calls in try/catch for network-level failures:

csharp
try
{
    var items = await _apiService.GetItemsAsync();
}
catch (HttpRequestException ex)
{
    // Network error or non-success status code
}
catch (JsonException ex)
{
    // Deserialization failure
}

Common HTTP response codes

CodeMeaningTypical use
200OKSuccessful GET or PUT
201CreatedSuccessful POST (resource created)
204No ContentSuccessful DELETE or PUT (no body)
400Bad RequestValidation error in request body
404Not FoundResource does not exist
409ConflictDuplicate or state conflict

Platform-specific: local development with HTTP clear-text

Emulators and simulators block clear-text HTTP by default. When targeting a local dev server over http://:

Android — add a network security config in Platforms/Android/Resources/xml/network_security_config.xml:

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>

Reference it in AndroidManifest.xml:

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

iOS / Mac Catalyst — add an NSAppTransportSecurity exception in Info.plist:

xml
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsLocalNetworking</key>
  <true/>
</dict>

Note: Android emulators reach the host machine at 10.0.2.2. iOS simulators use localhost directly.

Rules

  • Always use async/await; never block with .Result or .Wait().
  • Register HttpClient once (singleton or factory); do not create per-request instances.
  • Set BaseAddress in DI; use relative URIs in service methods.
  • Apply JsonSerializerOptions consistently with CamelCase naming policy.
  • Check IsSuccessStatusCode before deserializing response bodies.
  • Wrap API calls in try/catch for HttpRequestException and JsonException.
  • Use the service interface pattern so view models depend on abstractions.