If you've ever fought with Android's 10.0.2.2 workarounds, wrestled with iOS certificate validation, or juggled hardcoded localhost URLs across platforms, you know the pain. Connecting a .NET MAUI app to backend services during development has always been way more complicated than it should be.
.NET MAUI 10 finally fixes this with official .NET Aspire integration — and honestly, it changes everything about how you build mobile apps with backend dependencies.
In this guide, I'll walk you through setting up a .NET MAUI app with .NET Aspire from scratch. You'll get automatic service discovery, Dev Tunnels for emulators and physical devices, and OpenTelemetry observability — all without writing a single platform-specific networking hack.
What Is .NET Aspire and Why Should MAUI Developers Care?
.NET Aspire is Microsoft's opinionated stack for building cloud-native, distributed applications. It bundles orchestration, service discovery, pre-built integrations, and a developer dashboard into one cohesive experience. Until .NET 10, Aspire was primarily a backend concern — web APIs, microservices, worker services. Now, with the Aspire.Hosting.Maui package, your .NET MAUI app becomes a first-class participant in the Aspire ecosystem.
Here's what that actually gives you in practice:
- Automatic service discovery: No more hardcoded URLs. Your MAUI app resolves backend services by name, just like any other Aspire component.
- Dev Tunnels: Secure, automatic tunnels that route traffic from Android emulators, iOS simulators, and physical devices to services running on your dev machine.
- Unified telemetry: Logs, traces, and metrics from your mobile app flow into the same Aspire dashboard as your backend services.
- Platform-agnostic networking: No more
10.0.2.2for Android, no more ATS exceptions for iOS. Aspire just handles it.
Prerequisites
Before we start, make sure you have these installed:
- .NET 10 SDK (LTS release)
- Visual Studio 2026 or VS Code with the C# Dev Kit extension
- .NET Aspire workload:
dotnet workload install aspire - Android SDK and/or Xcode for mobile targets
- Docker Desktop (required for the Aspire dashboard)
You can verify your Aspire workload is installed by running:
dotnet workload list
You should see aspire in the output. If not, install it with:
dotnet workload install aspire
Solution Architecture
The integration adds two new projects to your solution alongside your MAUI app and backend API:
| Project | Role |
|---|---|
| AppHost | Orchestrates all services, manages Dev Tunnels, wires up service discovery |
| MauiServiceDefaults | Shared configuration for telemetry, resilience, and service discovery in your MAUI app |
| Your MAUI App | Consumes backend services through named HTTP clients |
| Your Web API | Backend service registered with Aspire for discovery |
The data flow is straightforward: the AppHost starts your API, registers it for discovery, creates Dev Tunnels for mobile platforms, and exposes the Aspire dashboard. Your MAUI app then uses the MauiServiceDefaults to resolve service URLs by name and send telemetry back through the tunnel. Pretty elegant, right?
Step 1: Create the Solution Structure
Start by creating a new solution with the MAUI app and a minimal API backend:
dotnet new sln -n WeatherApp
dotnet new maui -n WeatherApp.Mobile
dotnet new webapi -n WeatherApp.Api
dotnet sln add WeatherApp.Mobile/WeatherApp.Mobile.csproj
dotnet sln add WeatherApp.Api/WeatherApp.Api.csproj
Now add the Aspire App Host and the MAUI Service Defaults projects:
dotnet new aspire-apphost -n WeatherApp.AppHost
dotnet new maui-aspire-servicedefaults -n WeatherApp.MauiServiceDefaults
dotnet sln add WeatherApp.AppHost/WeatherApp.AppHost.csproj
dotnet sln add WeatherApp.MauiServiceDefaults/WeatherApp.MauiServiceDefaults.csproj
Wire up the project references:
dotnet add WeatherApp.Mobile/WeatherApp.Mobile.csproj reference WeatherApp.MauiServiceDefaults/WeatherApp.MauiServiceDefaults.csproj
dotnet add WeatherApp.AppHost/WeatherApp.AppHost.csproj reference WeatherApp.Api/WeatherApp.Api.csproj
Finally, add the Aspire MAUI hosting package to the App Host:
dotnet add WeatherApp.AppHost/WeatherApp.AppHost.csproj package Aspire.Hosting.Maui --prerelease
Step 2: Configure the App Host
The App Host is the brain of the operation. It registers your API for discovery, adds your MAUI project, and sets up Dev Tunnels for mobile platforms.
Open WeatherApp.AppHost/Program.cs and configure it:
var builder = DistributedApplication.CreateBuilder(args);
// Register the backend API
var weatherApi = builder.AddProject<Projects.WeatherApp_Api>("weatherapi");
// Create a public Dev Tunnel for mobile connectivity
var publicDevTunnel = builder.AddDevTunnel("weather-tunnel")
.WithPublicAccess();
// Add the MAUI project with platform targets
var mauiApp = builder.AddMauiProject("mobileapp",
@"..\WeatherApp.Mobile\WeatherApp.Mobile.csproj");
// Windows and Mac Catalyst use localhost directly
mauiApp.AddWindowsMachine()
.WithReference(weatherApi);
mauiApp.AddMacCatalystDevice()
.WithReference(weatherApi);
// iOS and Android need Dev Tunnels
mauiApp.AddiOSSimulator()
.WithOtlpDevTunnel()
.WithReference(weatherApi, publicDevTunnel);
mauiApp.AddAndroidEmulator()
.WithOtlpDevTunnel()
.WithReference(weatherApi, publicDevTunnel);
// Physical devices also use Dev Tunnels
mauiApp.AddiOSDevice()
.WithOtlpDevTunnel()
.WithReference(weatherApi, publicDevTunnel);
mauiApp.AddAndroidDevice()
.WithOtlpDevTunnel()
.WithReference(weatherApi, publicDevTunnel);
builder.Build().Run();
A few things worth noting here:
AddMauiProjectregisters your MAUI app with Aspire. Unlike regular Aspire project references, MAUI projects need this dedicated method.WithReference(weatherApi)makes the API discoverable to your MAUI app by the name"weatherapi".WithOtlpDevTunnel()creates a separate tunnel for OpenTelemetry data so your mobile app can send logs and traces back to the Aspire dashboard.- Windows and Mac Catalyst use
localhostdirectly — no tunnel needed.
Step 3: Configure the MAUI App with Service Defaults
Now let's connect your MAUI app to the Aspire service defaults. Open WeatherApp.Mobile/MauiProgram.cs:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WeatherApp.Mobile;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// Add Aspire service defaults (telemetry + service discovery)
builder.AddServiceDefaults();
// Register a typed HTTP client with service discovery
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
// "https+http://weatherapi" resolves via Aspire service discovery
// Prefers HTTPS, falls back to HTTP
client.BaseAddress = new Uri("https+http://weatherapi");
});
// Register the ViewModel
builder.Services.AddTransient<WeatherViewModel>();
builder.Services.AddTransient<MainPage>();
return builder.Build();
}
}
The key line here is builder.AddServiceDefaults(). That single call wires up:
- OpenTelemetry metrics and distributed tracing
- Structured logging with OTLP export
- Service discovery for named HTTP clients
- Resilience patterns (retries, circuit breakers) via Microsoft.Extensions.Http.Resilience
The "https+http://weatherapi" URI scheme tells the service discovery system to prefer HTTPS but fall back to HTTP. The name "weatherapi" matches what you registered in the App Host. No hardcoded ports, no platform-specific IP addresses. It just works.
Step 4: Create the Typed HTTP Client
Next, create a strongly-typed HTTP client to encapsulate your API calls. Add WeatherApiClient.cs to your MAUI project:
using System.Net.Http.Json;
namespace WeatherApp.Mobile;
public class WeatherApiClient(HttpClient httpClient)
{
public async Task<WeatherForecast[]?> GetForecastAsync(
CancellationToken cancellationToken = default)
{
return await httpClient.GetFromJsonAsync<WeatherForecast[]>(
"/weatherforecast", cancellationToken);
}
}
public record WeatherForecast(
DateOnly Date,
int TemperatureC,
string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Notice that the HttpClient gets injected via the primary constructor — it's already configured with the correct base address and all the resilience and telemetry middleware from the service defaults. You don't need to create or manage the HttpClient yourself.
Step 5: Build the ViewModel and UI
Create a simple ViewModel that uses the typed client:
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace WeatherApp.Mobile;
public partial class WeatherViewModel(WeatherApiClient apiClient) : ObservableObject
{
[ObservableProperty]
private bool isRefreshing;
[ObservableProperty]
private string? errorMessage;
public ObservableCollection<WeatherForecast> Forecasts { get; } = [];
[RelayCommand]
private async Task LoadWeatherAsync()
{
try
{
ErrorMessage = null;
IsRefreshing = true;
var forecasts = await apiClient.GetForecastAsync();
Forecasts.Clear();
if (forecasts is not null)
{
foreach (var forecast in forecasts)
{
Forecasts.Add(forecast);
}
}
}
catch (HttpRequestException ex)
{
ErrorMessage = $"Failed to load weather data: {ex.Message}";
}
finally
{
IsRefreshing = false;
}
}
}
And here's the corresponding XAML for MainPage.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:local="clr-namespace:WeatherApp.Mobile"
x:Class="WeatherApp.Mobile.MainPage"
x:DataType="local:WeatherViewModel"
Title="Weather Forecast">
<RefreshView IsRefreshing="{Binding IsRefreshing}"
Command="{Binding LoadWeatherCommand}">
<Grid RowDefinitions="Auto,*">
<Label Text="{Binding ErrorMessage}"
TextColor="Red"
IsVisible="{Binding ErrorMessage, Converter={StaticResource IsNotNullConverter}}"
Padding="16,8" />
<CollectionView Grid.Row="1"
ItemsSource="{Binding Forecasts}">
<CollectionView.EmptyView>
<VerticalStackLayout VerticalOptions="Center"
HorizontalOptions="Center">
<Label Text="Pull down to load weather data"
HorizontalTextAlignment="Center"
TextColor="Gray" />
</VerticalStackLayout>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:WeatherForecast">
<Grid ColumnDefinitions="*,Auto,Auto"
Padding="16,12"
ColumnSpacing="16">
<Label Text="{Binding Date, StringFormat='{0:ddd, MMM d}'}"
VerticalOptions="Center" />
<Label Grid.Column="1"
Text="{Binding TemperatureC, StringFormat='{0}°C'}"
FontAttributes="Bold"
VerticalOptions="Center" />
<Label Grid.Column="2"
Text="{Binding Summary}"
TextColor="Gray"
VerticalOptions="Center" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</RefreshView>
</ContentPage>
Step 6: Run Everything with Aspire
Start the Aspire App Host:
dotnet run --project WeatherApp.AppHost
Or use the Aspire CLI if you have it installed:
aspire run
The Aspire dashboard will open in your browser, showing your registered services. Your MAUI app doesn't launch automatically — you start each target manually. For example, to fire up the Android emulator target, use Visual Studio or run:
dotnet build WeatherApp.Mobile -t:Run -f net10.0-android
When the MAUI app starts, it receives the service discovery configuration from Aspire. The "weatherapi" name resolves to the correct URL — whether that's localhost:5001 on Windows, or a Dev Tunnel URL on Android and iOS. You don't need to change a single line of code between platforms.
How Dev Tunnels Work Under the Hood
Dev Tunnels are honestly the secret sauce that makes this whole thing work on mobile. Here's what happens when your Android emulator makes an HTTP request:
- Your MAUI app resolves
"weatherapi"using Aspire service discovery. - On Android and iOS, the resolved URL points to a Dev Tunnel endpoint (something like
https://abc123.devtunnels.ms). - The Dev Tunnel routes traffic securely to your local machine where the API is running.
- The response travels back through the tunnel to the emulator.
On Windows and Mac Catalyst, no tunnel is needed — the resolved URL points directly to localhost.
The WithOtlpDevTunnel() call creates a separate tunnel specifically for OpenTelemetry protocol traffic. This keeps telemetry data flowing even if your API tunnel configuration changes, and it ensures the Aspire dashboard receives logs and traces from mobile devices.
Monitoring Your MAUI App with the Aspire Dashboard
One of the most powerful aspects of this integration is unified observability. Once your app is running with service defaults, the Aspire dashboard shows:
- Structured logs: Every
ILoggercall in your MAUI app appears in the dashboard alongside your API logs. - Distributed traces: An HTTP call from your MAUI app to the API shows as a single trace spanning both services. You can see the full request lifecycle — from the button tap to the API response.
- Metrics: HTTP client metrics (request duration, success rate, connection pool stats) are collected automatically.
This is a game-changer for debugging. Instead of attaching multiple debuggers and correlating timestamps manually, you get the entire distributed call chain in one place. I've spent hours in the past trying to figure out why a mobile request was timing out, only to discover the issue was on the backend. With Aspire's unified traces, that kind of debugging takes seconds.
To add custom telemetry to your MAUI app, inject the standard .NET logging and activity APIs:
using System.Diagnostics;
using Microsoft.Extensions.Logging;
namespace WeatherApp.Mobile;
public partial class WeatherViewModel(
WeatherApiClient apiClient,
ILogger<WeatherViewModel> logger) : ObservableObject
{
private static readonly ActivitySource ActivitySource = new("WeatherApp.Mobile");
[RelayCommand]
private async Task LoadWeatherAsync()
{
using var activity = ActivitySource.StartActivity("LoadWeather");
logger.LogInformation("Loading weather forecast data");
try
{
var forecasts = await apiClient.GetForecastAsync();
activity?.SetTag("forecast.count", forecasts?.Length ?? 0);
logger.LogInformation("Loaded {Count} forecasts", forecasts?.Length ?? 0);
// ... update UI
}
catch (HttpRequestException ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
logger.LogError(ex, "Failed to load weather data");
// ... show error
}
}
}
These custom activities and log entries show up in the Aspire dashboard right alongside the automatic HTTP instrumentation.
The Old Way vs. the Aspire Way
To really appreciate what Aspire saves you, here's what connecting to a local API looked like before (and if this doesn't give you flashbacks, you haven't been doing MAUI long enough):
// The old painful way
public static class ApiConfig
{
public static string BaseUrl
{
get
{
#if ANDROID
// Android emulator uses 10.0.2.2 to reach the host
return "https://10.0.2.2:5001";
#elif IOS
// iOS simulator can use localhost, but physical devices cannot
return "https://localhost:5001";
#else
return "https://localhost:5001";
#endif
}
}
}
// Plus you needed:
// - Android network security config to allow cleartext
// - iOS ATS exceptions in Info.plist
// - Custom HttpClientHandler to bypass SSL validation in dev
// - Manual URL management when ports change
With Aspire, all of that collapses to:
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
client.BaseAddress = new Uri("https+http://weatherapi");
});
No conditional compilation. No platform-specific config files. No SSL hacks. It's genuinely refreshing.
Handling Production vs. Development
Aspire service discovery is a development tool. In production, your MAUI app connects to your actual deployed API. The recommended pattern is to use configuration to switch between environments:
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
// In development: resolves via Aspire service discovery
// In production: resolves via appsettings.json or environment variable
client.BaseAddress = new Uri(
builder.Configuration["ApiBaseUrl"] ?? "https+http://weatherapi");
});
In your production appsettings.json, set the actual URL:
{
"ApiBaseUrl": "https://api.yourapp.com"
}
During development, Aspire's service discovery intercepts the "https+http://weatherapi" URI and resolves it automatically. In production, the explicit URL from configuration takes precedence. Simple and clean.
Troubleshooting Common Issues
Dev Tunnel fails to connect on Android
Make sure Docker Desktop is running — the Aspire dashboard and Dev Tunnels depend on it. Also verify you're signed into your Microsoft account, since Dev Tunnels require authentication:
devtunnel user login
iOS simulator can't reach the API
Check that you used WithReference(weatherApi, publicDevTunnel) for iOS targets, not just WithReference(weatherApi). The Dev Tunnel reference is required for iOS simulator connectivity. This one tripped me up the first time too.
Telemetry isn't showing in the dashboard
Make sure WithOtlpDevTunnel() is configured for your mobile targets. Without it, telemetry data from iOS and Android simply can't reach the Aspire dashboard. Also double-check that AddServiceDefaults() is called in your MauiProgram.cs.
Service name doesn't resolve
The name you use in new Uri("https+http://weatherapi") must exactly match the name passed to AddProject in the App Host. Double-check casing — service names are case-sensitive.
Limitations and Preview Status
As of March 2026, .NET MAUI Aspire integration is still in preview. Here are the current limitations worth knowing about:
- Visual Studio 2026 integration is incomplete: The full IDE experience (launch profiles, integrated dashboard) is still being built out. Command-line workflows are more reliable for now.
- MAUI app doesn't auto-launch: Unlike web projects, your MAUI targets must be started manually after the App Host is running.
- Dev Tunnels require authentication: You need a Microsoft account signed in via the
devtunnelCLI. - Preview NuGet package: The
Aspire.Hosting.Mauipackage uses--prereleaseversioning. Pin your version in production projects.
Despite these limitations, the core functionality — service discovery, Dev Tunnels, and telemetry — works reliably across all platforms. The GA release is expected to ship alongside .NET 10 servicing updates later in 2026.
FAQ
Can I use .NET Aspire with an existing .NET MAUI app?
Yes. You can add Aspire integration to any existing .NET MAUI 10 app. Create the MauiServiceDefaults and AppHost projects, add the project references, call AddServiceDefaults() in your MauiProgram.cs, and update your HttpClient registrations to use service discovery URIs. Your existing code doesn't need to change beyond the startup configuration.
Does .NET Aspire integration work on physical iOS and Android devices?
Yes. Physical devices are supported through Dev Tunnels, just like emulators and simulators. In the App Host, use AddiOSDevice() and AddAndroidDevice() with WithOtlpDevTunnel() and WithReference() to enable connectivity.
What happens to service discovery in production?
Aspire service discovery is a development-time feature. In production, you set your API base URL through standard .NET configuration (appsettings.json, environment variables, or a remote config service). The recommended pattern is to provide a configuration fallback so the same code works in both environments.
Do I need Docker to use .NET Aspire with MAUI?
Docker Desktop is required for the Aspire dashboard, which runs as a container. If you only need service discovery without the dashboard, you can use the Aspire CLI (aspire run) which handles the dashboard lifecycle for you. Dev Tunnels themselves don't require Docker.
How does this differ from using Refit or a plain HttpClient?
Aspire doesn't replace your HTTP client library. You can use Refit, plain HttpClient, or any other HTTP abstraction on top of Aspire. What Aspire provides is the infrastructure layer beneath your HTTP clients: automatic URL resolution, Dev Tunnels for mobile connectivity, telemetry, and resilience patterns. Think of it as the plumbing that your HTTP client sits on top of.