Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions playground/AspireWithMaui/AspireWithMaui.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

var weatherApi = builder.AddProject("webapi", @"../AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj");

var publicDevTunnel = builder.AddDevTunnel("devtunnel-public")
.WithAnonymousAccess() // All ports on this tunnel default to allowing anonymous access
.WithReference(weatherApi.GetEndpoint("https"));

var mauiapp = builder.AddMauiProject("mauiapp", @"../AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj");

mauiapp.AddWindowsDevice()
Expand All @@ -10,4 +14,20 @@
mauiapp.AddMacCatalystDevice()
.WithReference(weatherApi);

// Add iOS simulator with default simulator (uses running or default simulator)
mauiapp.AddiOSSimulator()
.WithOtlpDevTunnel() // Needed to get the OpenTelemetry data to "localhost"
.WithReference(weatherApi, publicDevTunnel); // Needs a dev tunnel to reach "localhost"

// Add iOS device (requires device provisioning)
// Uncomment to use with a physical iOS device:
// mauiapp.AddiOSDevice()
// .WithOtlpDevTunnel() // Needed to get the OpenTelemetry data to "localhost"
// .WithReference(weatherApi, publicDevTunnel); // Needs a dev tunnel to reach "localhost"

// Add Android emulator with default emulator (uses running or default emulator)
mauiapp.AddAndroidEmulator()
.WithOtlpDevTunnel() // Needed to get the OpenTelemetry data to "localhost"
.WithReference(weatherApi, publicDevTunnel); // Needs a dev tunnel to reach "localhost"

builder.Build().Run();
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ private void LoadAspireEnvironmentVariables()
var variables = Environment.GetEnvironmentVariables()
.Cast<DictionaryEntry>()
.Select(entry => new KeyValuePair<string, string>(entry.Key?.ToString() ?? string.Empty, DecodeValue(entry.Value?.ToString())))
.Where(item => IsAspireVariable(item.Key))
.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase);

foreach (var variable in variables)
Expand Down Expand Up @@ -62,18 +61,4 @@ private static string DecodeValue(string? value)
return value;
}
}

private static bool IsAspireVariable(string key)
=> key.StartsWith("services__", StringComparison.OrdinalIgnoreCase)
|| key.StartsWith("connectionstrings__", StringComparison.OrdinalIgnoreCase)
|| key.StartsWith("ASPIRE_", StringComparison.OrdinalIgnoreCase)
|| key.StartsWith("AppHost__", StringComparison.OrdinalIgnoreCase)
|| key.StartsWith("OTEL_", StringComparison.OrdinalIgnoreCase)
|| key.StartsWith("LOGGING__CONSOLE", StringComparison.OrdinalIgnoreCase)
|| key.Equals("ASPNETCORE_ENVIRONMENT", StringComparison.OrdinalIgnoreCase)
|| key.Equals("ASPNETCORE_URLS", StringComparison.OrdinalIgnoreCase)
|| key.Equals("DOTNET_ENVIRONMENT", StringComparison.OrdinalIgnoreCase)
|| key.Equals("DOTNET_URLS", StringComparison.OrdinalIgnoreCase)
|| key.Equals("DOTNET_LAUNCH_PROFILE", StringComparison.OrdinalIgnoreCase)
|| key.Equals("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", StringComparison.OrdinalIgnoreCase);
}
122 changes: 112 additions & 10 deletions playground/AspireWithMaui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,110 @@ After running the restore script with `-restore-maui`, you can build and run the
The playground demonstrates Aspire's ability to manage MAUI apps on multiple platforms:
- **Windows**: Configures the MAUI app with `.AddWindowsDevice()`
- **Mac Catalyst**: Configures the MAUI app with `.AddMacCatalystDevice()`
- **iOS Device**: Configures the MAUI app with `.AddiOSDevice()` to run on physical iOS devices
- Requires device provisioning before deployment (see https://learn.microsoft.com/dotnet/maui/ios/device-provisioning)
- Use `.AddiOSDevice()` to target the only attached device (default, requires exactly one device)
- Use `.AddiOSDevice("device-name", "00008030-001234567890123A")` to target a specific device by UDID
- Find device UDID in Xcode under Window > Devices and Simulators > Devices tab, or right-click device and select "Copy Identifier"
- Requires macOS to run (iOS development is macOS-only)
- Use `.WithOtlpDevTunnel()` to send telemetry to the dashboard (iOS devices cannot reach localhost)
- **iOS Simulator**: Configures the MAUI app with `.AddiOSSimulator()` to run on iOS simulators
- Use `.AddiOSSimulator()` to target the default simulator
- Use `.AddiOSSimulator("simulator-name", "E25BBE37-69BA-4720-B6FD-D54C97791E79")` to target a specific simulator by UDID
- Find simulator UDIDs in Xcode under Window > Devices and Simulators > Simulators tab, or use `/Applications/Xcode.app/Contents/Developer/usr/bin/simctl list`
- Requires macOS to run (iOS development is macOS-only)
- Use `.WithOtlpDevTunnel()` to send telemetry to the dashboard (simulators cannot reach localhost)
- **Android Device**: Configures the MAUI app with `.AddAndroidDevice()` to run on physical Android devices
- Use `.AddAndroidDevice()` to target the only attached device (default, requires exactly one device)
- Use `.AddAndroidDevice("device-name", "abc12345")` to target a specific device by serial number or IP
- Works with USB-connected devices and WiFi debugging (e.g., "192.168.1.100:5555")
- Get device IDs from `adb devices` command
- Use `.WithOtlpDevTunnel()` to send telemetry to the dashboard (Android cannot reach localhost)
- **Android Emulator**: Configures the MAUI app with `.AddAndroidEmulator()` to run on Android emulators
- Use `.AddAndroidEmulator()` to target the only running emulator (default)
- Use `.AddAndroidEmulator("emulator-name", "Pixel_5_API_33")` to target a specific emulator by AVD name
- Can also use emulator serial number like "emulator-5554"
- Get emulator names from `adb devices` or `emulator -list-avds` command
- Use `.WithOtlpDevTunnel()` to send telemetry to the dashboard (emulators cannot reach localhost)
- Automatically detects platform-specific target frameworks from the project file
- Shows "Unsupported" state in dashboard when running on incompatible host OS
- Sets up dev tunnels for MAUI app communication with backend services

### OpenTelemetry Integration
The MAUI client uses OpenTelemetry to send traces and metrics to the Aspire dashboard via dev tunnels.
The MAUI client uses OpenTelemetry to send traces and metrics to the Aspire dashboard. For mobile platforms that cannot reach `localhost`, the playground demonstrates using dev tunnels to expose the dashboard's OTLP endpoint:

```csharp
// Android devices and emulators need dev tunnel for OTLP
mauiapp.AddAndroidEmulator()
.WithOtlpDevTunnel() // Automatically creates and configures a dev tunnel for telemetry
.WithReference(weatherApi, publicDevTunnel); // Dev tunnel for API communication

// iOS simulators and devices also need dev tunnel for OTLP
mauiapp.AddiOSSimulator()
.WithOtlpDevTunnel() // Automatically creates and configures a dev tunnel for telemetry
.WithReference(weatherApi, publicDevTunnel); // Dev tunnel for API communication
```

The `.WithOtlpDevTunnel()` method:
- Automatically resolves the dashboard's OTLP endpoint from configuration
- Creates a dev tunnel for the OTLP endpoint
- Configures the MAUI platform to send telemetry through the tunnel
- Handles all service discovery and environment variable setup

**Requirements for dev tunnels:**
- Dev tunnel CLI must be installed (automatic prompt if missing)
- User must be logged in to dev tunnel service (automatic prompt if needed)

### Service Discovery
The MAUI app discovers and connects to backend services (WeatherApi) using Aspire's service discovery.

### Environment Variables

All MAUI platform resources support environment variables using the standard `.WithEnvironment()` method. Environment variables are automatically forwarded to the MAUI application regardless of platform:

```csharp
// For Windows and Mac Catalyst, environment variables are passed directly:
mauiapp.AddWindowsDevice()
.WithEnvironment("DEBUG_MODE", "true")
.WithEnvironment("API_TIMEOUT", "30");

// For Android, environment variables are passed via an intermediate MSBuild targets file, but the syntax is identical:
mauiapp.AddAndroidDevice("my-device", "abc12345")
.WithEnvironment("DEBUG_MODE", "true")
.WithEnvironment("API_TIMEOUT", "30")
.WithEnvironment("LOG_LEVEL", "Debug");

mauiapp.AddAndroidEmulator("my-emulator", "Pixel_5_API_33")
.WithEnvironment("CUSTOM_VAR", "value")
.WithReference(weatherApi); // Service discovery environment variables also forwarded

// For iOS, environment variables are also passed via an intermediate MSBuild targets file:
mauiapp.AddiOSSimulator("my-simulator", "E25BBE37-69BA-4720-B6FD-D54C97791E79")
.WithEnvironment("DEBUG_MODE", "true")
.WithEnvironment("API_TIMEOUT", "30")
.WithReference(weatherApi); // Service discovery environment variables also forwarded
```

#### What Gets Forwarded

**ALL Aspire-managed environment variables** are automatically forwarded to MAUI applications:
- **Custom variables**: Set via `.WithEnvironment(key, value)`
- **Service discovery**: Connection strings and endpoints from `.WithReference(service)`
- **OpenTelemetry**: OTEL configuration from `.WithOtlpExporter()`
- **Resource metadata**: Automatically added by Aspire

#### Platform-Specific Implementation

- **Windows & Mac Catalyst**: Environment variables are passed directly through the process environment when launching via `dotnet run`.
- **Android**: Due to Android platform limitations, environment variables are written to a temporary MSBuild targets file that gets imported during the build. The targets file is generated automatically before each build and cleaned up after 24 hours (when a next build happens). Environment variable names are normalized to UPPERCASE (Android requirement), and semicolons are encoded as `%3B`.
- **iOS**: Due to iOS platform limitations, environment variables are written to a temporary MSBuild targets file that gets imported during the build. The targets file is generated automatically before each build and cleaned up after 24 hours. Environment variables are passed via the `--setenv` argument to `mtouch`/`mlaunch`.

Environment variables are available in your MAUI app code regardless of platform through standard .NET environment APIs (`Environment.GetEnvironmentVariable()`).

### Future Platform Support
The architecture is designed to support additional platforms (Android, iOS) through:
- `.AddAndroidDevice()`, `.AddIosDevice()` extension methods (coming in future updates)
- Parallel extension patterns for each platform
The architecture is designed to support additional platforms:
- Android support: `.AddAndroidDevice()` for physical devices, `.AddAndroidEmulator()` for emulators (implemented)
- iOS support: `.AddiOSDevice()` for physical devices, `.AddiOSSimulator()` for simulators (implemented)

## Troubleshooting

Expand All @@ -97,25 +187,37 @@ If you encounter build errors:
3. Try running `dotnet build` from the repository root first

### Platform-Specific Issues
- **Windows**: Requires Windows 10 build 19041 or higher for WinUI support. Mac Catalyst devices will show as "Unsupported" when running on Windows.
- **Mac Catalyst**: Requires macOS to run. Windows devices will show as "Unsupported" when running on macOS.
- **Android**: Not yet implemented in this playground (coming soon)
- **iOS**: Not yet implemented in this playground (coming soon)
- **Windows**: Requires Windows 10 build 19041 or higher for WinUI support. Mac Catalyst and iOS devices will show as "Unsupported" when running on Windows.
- **Mac Catalyst**: Requires macOS to run. Windows, Android, and iOS devices will show as "Unsupported" when running on non-macOS platforms.
- **iOS Device**: Requires macOS and a physical iOS device connected via USB. Device must be provisioned before deployment (https://learn.microsoft.com/dotnet/maui/ios/device-provisioning). Find device UDID in Xcode under Window > Devices and Simulators.
- **iOS Simulator**: Requires macOS and Xcode with iOS simulator runtimes installed. To target a specific simulator:
1. List available simulators: `/Applications/Xcode.app/Contents/Developer/usr/bin/simctl list`
2. Or find in Xcode: Window > Devices and Simulators > Simulators tab
3. Right-click simulator and select "Copy Identifier" for UDID
4. Use in code: `.AddiOSSimulator(simulatorId: "E25BBE37-69BA-4720-B6FD-D54C97791E79")`
- **Android Device**: Requires a physical Android device connected via USB/WiFi debugging. Ensure the device is visible via `adb devices`. Works on Windows, macOS, and Linux.
- **Android Emulator**: Requires an Android emulator running and visible via `adb devices`. To target a specific emulator:
1. List available emulators: `adb devices` (shows emulator IDs like "emulator-5554")
2. Or list AVDs: `emulator -list-avds` (shows AVD names like "Pixel_5_API_33")
3. Use either ID format in code: `.AddAndroidEmulator(emulatorId: "Pixel_5_API_33")` or `.AddAndroidEmulator(emulatorId: "emulator-5554")`
4. Works on Windows, macOS, and Linux.

## Current Status

✅ **Implemented:**
- Windows platform support via `AddWindowsDevice()`
- Mac Catalyst platform support via `AddMacCatalystDevice()`
- iOS device support via `AddiOSDevice()`
- iOS simulator support via `AddiOSSimulator()`
- Android device support via `AddAndroidDevice()`
- Android emulator support via `AddAndroidEmulator()`
- Automatic platform-specific TFM detection from project file
- Platform validation with "Unsupported" state for incompatible hosts
- Dev tunnel configuration for MAUI-to-backend communication
- Service discovery integration
- OpenTelemetry integration

🚧 **Coming Soon:**
- Android platform support via `AddAndroidDevice()`
- iOS platform support via `AddIosDevice()`
- Multi-platform simultaneous debugging

## Learn More
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.DevTunnels;
using Aspire.Hosting.Maui.Otlp;

namespace Aspire.Hosting.Maui.Annotations;

/// <summary>
/// Annotation that stores the OTLP dev tunnel configuration for a MAUI project.
/// This allows sharing a single dev tunnel infrastructure across multiple platform resources.
/// </summary>
internal sealed class OtlpDevTunnelConfigurationAnnotation : IResourceAnnotation
{
/// <summary>
/// The OTLP loopback stub resource that acts as the service discovery target.
/// </summary>
public OtlpLoopbackResource OtlpStub { get; }

/// <summary>
/// The resource builder for the OTLP stub (used for WithReference calls).
/// </summary>
public IResourceBuilder<OtlpLoopbackResource> OtlpStubBuilder { get; }

/// <summary>
/// The dev tunnel resource that tunnels the OTLP endpoint.
/// </summary>
public IResourceBuilder<DevTunnelResource> DevTunnel { get; }

public OtlpDevTunnelConfigurationAnnotation(
OtlpLoopbackResource otlpStub,
IResourceBuilder<OtlpLoopbackResource> otlpStubBuilder,
IResourceBuilder<DevTunnelResource> devTunnel)
{
OtlpStub = otlpStub;
OtlpStubBuilder = otlpStubBuilder;
DevTunnel = devTunnel;
}
}
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.Maui/Aspire.Hosting.Maui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\IConfigurationExtensions.cs" Link="Shared\IConfigurationExtensions.cs" />
<Compile Include="..\Shared\KnownConfigNames.cs" Link="Shared\KnownConfigNames.cs" />
<Compile Include="..\Shared\OtlpEndpointResolver.cs" Link="Shared\OtlpEndpointResolver.cs" />
<Compile Include="..\Shared\PathNormalizer.cs" Link="Shared\PathNormalizer.cs" />
<Compile Include="..\Shared\StringComparers.cs" Link="Shared\StringComparers.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire.Hosting\Aspire.Hosting.csproj" />
<ProjectReference Include="..\Aspire.Hosting.DevTunnels\Aspire.Hosting.DevTunnels.csproj" />
</ItemGroup>
</Project>
5 changes: 4 additions & 1 deletion src/Aspire.Hosting.Maui/IMauiPlatformResource.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.Maui;

/// <summary>
Expand All @@ -9,7 +11,8 @@ namespace Aspire.Hosting.Maui;
/// <remarks>
/// This interface is used to identify resources that represent a specific platform instance
/// of a MAUI application, allowing for common handling across all MAUI platforms.
/// All MAUI platform resources have a parent <see cref="MauiProjectResource"/>.
/// </remarks>
internal interface IMauiPlatformResource
public interface IMauiPlatformResource : IResourceWithParent<MauiProjectResource>
{
}
20 changes: 20 additions & 0 deletions src/Aspire.Hosting.Maui/MauiAndroidDeviceResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.Maui;

/// <summary>
/// A resource that represents an Android physical device for running a .NET MAUI application.
/// </summary>
/// <param name="name">The name of the Android device resource.</param>
/// <param name="parent">The parent MAUI project resource.</param>
public sealed class MauiAndroidDeviceResource(string name, MauiProjectResource parent)
: ProjectResource(name), IMauiPlatformResource
{
/// <summary>
/// Gets the parent MAUI project resource.
/// </summary>
public MauiProjectResource Parent { get; } = parent;
}
20 changes: 20 additions & 0 deletions src/Aspire.Hosting.Maui/MauiAndroidEmulatorResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.Maui;

/// <summary>
/// A resource that represents an Android emulator for running a .NET MAUI application.
/// </summary>
/// <param name="name">The name of the Android emulator resource.</param>
/// <param name="parent">The parent MAUI project resource.</param>
public sealed class MauiAndroidEmulatorResource(string name, MauiProjectResource parent)
: ProjectResource(name), IMauiPlatformResource
{
/// <summary>
/// Gets the parent MAUI project resource.
/// </summary>
public MauiProjectResource Parent { get; } = parent;
}
Loading