How to Integrate .NET MAUI with .NET Aspire: Service Discovery, Dev Tunnels, and Telemetry

Connect your .NET MAUI 10 app to backend services using .NET Aspire. Covers service discovery, Dev Tunnels for Android emulators and iOS simulators, OpenTelemetry observability, and working code examples for cloud-native mobile development.

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.2 for 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:

ProjectRole
AppHostOrchestrates all services, manages Dev Tunnels, wires up service discovery
MauiServiceDefaultsShared configuration for telemetry, resilience, and service discovery in your MAUI app
Your MAUI AppConsumes backend services through named HTTP clients
Your Web APIBackend 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:

  • AddMauiProject registers 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 localhost directly — 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:

  1. Your MAUI app resolves "weatherapi" using Aspire service discovery.
  2. On Android and iOS, the resolved URL points to a Dev Tunnel endpoint (something like https://abc123.devtunnels.ms).
  3. The Dev Tunnel routes traffic securely to your local machine where the API is running.
  4. 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 ILogger call 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 devtunnel CLI.
  • Preview NuGet package: The Aspire.Hosting.Maui package uses --prerelease versioning. 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.

About the Author Editorial Team

Our team of expert writers and editors.