maui-rest-api
npx skills add https://github.com/davidortinau/maui-skills --skill maui-rest-api
Agent 安装分布
Skill 文档
Consuming REST APIs in .NET MAUI
HttpClient & JSON setup
Always configure a shared JsonSerializerOptions with camel-case naming:
private static readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
DI registration
Register HttpClient as a singleton or use IHttpClientFactory. Set BaseAddress once:
// 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:
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:
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:
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)
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)
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)
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)
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
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 (throwsHttpRequestException). - Use
IsSuccessStatusCodewhen you need to branch on specific status codes.
Wrap calls in try/catch for network-level failures:
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
| Code | Meaning | Typical use |
|---|---|---|
| 200 | OK | Successful GET or PUT |
| 201 | Created | Successful POST (resource created) |
| 204 | No Content | Successful DELETE or PUT (no body) |
| 400 | Bad Request | Validation error in request body |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate 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 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:
<application android:networkSecurityConfig="@xml/network_security_config" ... />
iOS / Mac Catalyst â add an NSAppTransportSecurity exception in Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
Note: Android emulators reach the host machine at
10.0.2.2. iOS simulators uselocalhostdirectly.
Rules
- Always use
async/await; never block with.Resultor.Wait(). - Register
HttpClientonce (singleton or factory); do not create per-request instances. - Set
BaseAddressin DI; use relative URIs in service methods. - Apply
JsonSerializerOptionsconsistently withCamelCasenaming policy. - Check
IsSuccessStatusCodebefore deserializing response bodies. - Wrap API calls in try/catch for
HttpRequestExceptionandJsonException. - Use the service interface pattern so view models depend on abstractions.