Skip to content
Merged
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
3 changes: 3 additions & 0 deletions playground/AspireWithMaui/AspireWithMaui.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
mauiapp.AddWindowsDevice()
.WithReference(weatherApi);

mauiapp.AddMacCatalystDevice()
.WithReference(weatherApi);

builder.Build().Run();
32 changes: 18 additions & 14 deletions playground/AspireWithMaui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,19 @@ After running the restore script with `-restore-maui`, you can build and run the
## What's Included

- **AspireWithMaui.AppHost** - The Aspire app host that orchestrates all services
- **AspireWithMaui.MauiClient** - A .NET MAUI application that connects to the backend (Windows platform only in this playground)
- **AspireWithMaui.MauiClient** - A .NET MAUI application that connects to the backend (Windows and Mac Catalyst platforms)
- **AspireWithMaui.WeatherApi** - An ASP.NET Core Web API providing weather data
- **AspireWithMaui.ServiceDefaults** - Shared service defaults for non-MAUI projects
- **AspireWithMaui.MauiServiceDefaults** - Shared service defaults specific to MAUI projects

## Features Demonstrated

### MAUI Windows Platform Support
The playground demonstrates Aspire's ability to manage MAUI apps on Windows:
- Configures the MAUI app with `.AddMauiWindows()`
- Automatically detects the Windows target framework from the project file
### MAUI Multi-Platform Support
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()`
- 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
Expand All @@ -76,8 +78,8 @@ The MAUI client uses OpenTelemetry to send traces and metrics to the Aspire dash
The MAUI app discovers and connects to backend services (WeatherApi) using Aspire's service discovery.

### Future Platform Support
The architecture is designed to support additional platforms (Android, iOS, macCatalyst) through:
- `.AddMauiAndroid()`, `.AddMauiIos()`, `.AddMauiMacCatalyst()` extension methods (coming in future updates)
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

## Troubleshooting
Expand All @@ -95,23 +97,25 @@ 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
- **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/macCatalyst**: Not yet implemented in this playground (coming soon)
- **iOS**: Not yet implemented in this playground (coming soon)

## Current Status

✅ **Implemented:**
- Windows platform support via `AddMauiWindows()`
- Automatic Windows TFM detection from project file
- Windows platform support via `AddWindowsDevice()`
- Mac Catalyst platform support via `AddMacCatalystDevice()`
- 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
- iOS platform support
- macCatalyst platform support
- Android platform support via `AddAndroidDevice()`
- iOS platform support via `AddIosDevice()`
- Multi-platform simultaneous debugging

## Learn More
Expand Down
15 changes: 15 additions & 0 deletions src/Aspire.Hosting.Maui/IMauiPlatformResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.Maui;

/// <summary>
/// Marker interface for MAUI platform-specific resources (Windows, Android, iOS, Mac Catalyst).
/// </summary>
/// <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.
/// </remarks>
internal interface IMauiPlatformResource
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think make this derive from IResource or maybe even IResourceWithParent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit ce2c2ef

{
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ namespace Aspire.Hosting.Maui.Lifecycle;
/// Event subscriber that sets the "Unsupported" state for MAUI platform resources
/// marked with <see cref="UnsupportedPlatformAnnotation"/>.
/// </summary>
/// <remarks>
/// This subscriber handles all MAUI platform resources (Windows, Android, iOS, Mac Catalyst)
/// by checking for the <see cref="IMauiPlatformResource"/> marker interface.
/// </remarks>
/// <param name="notificationService">The notification service for publishing resource state updates.</param>
internal sealed class UnsupportedPlatformEventSubscriber(ResourceNotificationService notificationService) : IDistributedApplicationEventingSubscriber
{
Expand All @@ -23,7 +27,7 @@ public Task SubscribeAsync(IDistributedApplicationEventing eventing, Distributed
// Find all MAUI platform resources with the UnsupportedPlatformAnnotation
foreach (var resource in @event.Model.Resources)
{
if (resource is MauiWindowsPlatformResource &&
if (resource is IMauiPlatformResource &&
resource.TryGetLastAnnotation<UnsupportedPlatformAnnotation>(out var annotation))
{
// Set the state to "Unsupported" with a warning style and the reason
Expand Down
120 changes: 120 additions & 0 deletions src/Aspire.Hosting.Maui/MauiMacCatalystExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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.Maui;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding Mac Catalyst platform resources to MAUI projects.
/// </summary>
public static class MauiMacCatalystExtensions
{
/// <summary>
/// Adds a Mac Catalyst device resource to run the MAUI application on the macOS platform.
/// </summary>
/// <param name="builder">The MAUI project resource builder.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// This method creates a new Mac Catalyst platform resource that will run the MAUI application
/// targeting the Mac Catalyst platform using <c>dotnet run</c>. The resource does not auto-start
/// and must be explicitly started from the dashboard by clicking the start button.
/// <para>
/// The resource name will default to "{projectName}-maccatalyst".
/// </para>
/// </remarks>
/// <example>
/// Add a Mac Catalyst device to a MAUI project:
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// var maui = builder.AddMauiProject("mauiapp", "../MyMauiApp/MyMauiApp.csproj");
/// var macCatalystDevice = maui.AddMacCatalystDevice();
///
/// builder.Build().Run();
/// </code>
/// </example>
public static IResourceBuilder<MauiMacCatalystPlatformResource> AddMacCatalystDevice(
this IResourceBuilder<MauiProjectResource> builder)
{
ArgumentNullException.ThrowIfNull(builder);

var name = $"{builder.Resource.Name}-maccatalyst";
return builder.AddMacCatalystDevice(name);
}

/// <summary>
/// Adds a Mac Catalyst device resource to run the MAUI application on the macOS platform with a specific name.
/// </summary>
/// <param name="builder">The MAUI project resource builder.</param>
/// <param name="name">The name of the Mac Catalyst device resource.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// This method creates a new Mac Catalyst platform resource that will run the MAUI application
/// targeting the Mac Catalyst platform using <c>dotnet run</c>. The resource does not auto-start
/// and must be explicitly started from the dashboard by clicking the start button.
/// <para>
/// Multiple Mac Catalyst device resources can be added to the same MAUI project if needed, each with
/// a unique name.
/// </para>
/// </remarks>
/// <example>
/// Add multiple Mac Catalyst devices to a MAUI project:
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// var maui = builder.AddMauiProject("mauiapp", "../MyMauiApp/MyMauiApp.csproj");
/// var macCatalystDevice1 = maui.AddMacCatalystDevice("maccatalyst-device-1");
/// var macCatalystDevice2 = maui.AddMacCatalystDevice("maccatalyst-device-2");
///
/// builder.Build().Run();
/// </code>
/// </example>
public static IResourceBuilder<MauiMacCatalystPlatformResource> AddMacCatalystDevice(
this IResourceBuilder<MauiProjectResource> builder,
[ResourceName] string name)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrWhiteSpace(name);

// Check if a Mac Catalyst device with this name already exists in the application model
var existingMacCatalystDevices = builder.ApplicationBuilder.Resources
.OfType<MauiMacCatalystPlatformResource>()
.FirstOrDefault(r => r.Parent == builder.Resource &&
string.Equals(r.Name, name, StringComparisons.ResourceName));
Comment on lines +81 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't actually need to two this check (in fact the check is not quite right). You can just add the resource to the model and it will do a uniqueness check on the name. You cannot have two resources with the same name even if they differ by type. Change this for the Windows resource too - I didn't pick it up in review last time ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 669549a


if (existingMacCatalystDevices is not null)
{
throw new DistributedApplicationException(
$"Mac Catalyst device with name '{name}' already exists on MAUI project '{builder.Resource.Name}'. " +
$"Provide a unique name parameter when calling AddMacCatalystDevice() to add multiple Mac Catalyst devices.");
}

// Get the absolute project path and working directory
var (projectPath, workingDirectory) = MauiPlatformHelper.GetProjectPaths(builder);

var macCatalystResource = new MauiMacCatalystPlatformResource(name, builder.Resource);

var resourceBuilder = builder.ApplicationBuilder.AddResource(macCatalystResource)
.WithAnnotation(new MauiProjectMetadata(projectPath))
.WithAnnotation(new ExecutableAnnotation
{
Command = "dotnet",
WorkingDirectory = workingDirectory
});

// Configure the platform resource with common settings
MauiPlatformHelper.ConfigurePlatformResource(
resourceBuilder,
projectPath,
"maccatalyst",
"Mac Catalyst",
"net10.0-maccatalyst",
OperatingSystem.IsMacOS,
"Desktop",
"-p:OpenArguments=-W");

return resourceBuilder;
}
}
29 changes: 29 additions & 0 deletions src/Aspire.Hosting.Maui/MauiMacCatalystPlatformResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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>
/// Represents a Mac Catalyst platform instance of a .NET MAUI project.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="parent">The parent MAUI project resource.</param>
/// <remarks>
/// This resource represents a MAUI application running on the Mac Catalyst platform.
/// The actual build and deployment happens when the resource is started, allowing for
/// incremental builds during development without blocking AppHost startup.
/// <para>
/// Use <see cref="MauiMacCatalystExtensions.AddMacCatalystDevice(IResourceBuilder{MauiProjectResource}, string?)"/>
/// to add this resource to a MAUI project.
/// </para>
/// </remarks>
public class MauiMacCatalystPlatformResource(string name, MauiProjectResource parent)
: ProjectResource(name), IResourceWithParent<MauiProjectResource>, IMauiPlatformResource
{
/// <summary>
/// Gets the parent MAUI project resource.
/// </summary>
public MauiProjectResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent));
}
112 changes: 112 additions & 0 deletions src/Aspire.Hosting.Maui/MauiPlatformHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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.Lifecycle;
using Aspire.Hosting.Maui.Annotations;
using Aspire.Hosting.Maui.Lifecycle;
using Aspire.Hosting.Maui.Utilities;
using Aspire.Hosting.Utils;

namespace Aspire.Hosting.Maui;

/// <summary>
/// Helper methods for adding platform-specific MAUI device resources.
/// </summary>
internal static class MauiPlatformHelper
{
/// <summary>
/// Gets the absolute project path and working directory from a MAUI project resource.
/// </summary>
/// <param name="builder">The MAUI project resource builder.</param>
/// <returns>A tuple containing the absolute project path and working directory.</returns>
internal static (string ProjectPath, string WorkingDirectory) GetProjectPaths(IResourceBuilder<MauiProjectResource> builder)
{
var projectPath = builder.Resource.ProjectPath;
if (!Path.IsPathRooted(projectPath))
{
projectPath = PathNormalizer.NormalizePathForCurrentPlatform(
Path.Combine(builder.ApplicationBuilder.AppHostDirectory, projectPath));
}

var workingDirectory = Path.GetDirectoryName(projectPath)
?? throw new InvalidOperationException($"Unable to determine directory from project path: {projectPath}");

return (projectPath, workingDirectory);
}

/// <summary>
/// Configures a platform resource with common settings and TFM validation.
/// </summary>
/// <typeparam name="T">The type of platform resource.</typeparam>
/// <param name="resourceBuilder">The resource builder.</param>
/// <param name="projectPath">The absolute path to the project file.</param>
/// <param name="platformName">The platform name (e.g., "windows", "maccatalyst").</param>
/// <param name="platformDisplayName">The display name for the platform (e.g., "Windows", "Mac Catalyst").</param>
/// <param name="tfmExample">Example TFM for error messages (e.g., "net10.0-windows10.0.19041.0").</param>
/// <param name="isSupported">Function to check if the platform is supported on the current host.</param>
/// <param name="iconName">The icon name for the resource.</param>
/// <param name="additionalArgs">Optional additional command-line arguments to pass to dotnet run.</param>
internal static void ConfigurePlatformResource<T>(
IResourceBuilder<T> resourceBuilder,
string projectPath,
string platformName,
string platformDisplayName,
string tfmExample,
Func<bool> isSupported,
string iconName = "Desktop",
params string[] additionalArgs) where T : ProjectResource
{
// Check if the project has the platform TFM and get the actual TFM value
var platformTfm = ProjectFileReader.GetPlatformTargetFramework(projectPath, platformName);

// Set the command line arguments with the detected TFM if available
resourceBuilder.WithArgs(context =>
{
context.Args.Add("run");
if (!string.IsNullOrEmpty(platformTfm))
{
context.Args.Add("-f");
context.Args.Add(platformTfm);
}
// Add any additional platform-specific arguments
foreach (var arg in additionalArgs)
{
context.Args.Add(arg);
}
});

resourceBuilder
.WithOtlpExporter()
.WithIconName(iconName)
.WithExplicitStart();

// Validate the platform TFM when the resource is about to start
resourceBuilder.OnBeforeResourceStarted((resource, eventing, ct) =>
{
// If we couldn't detect the TFM earlier, fail the resource start
if (string.IsNullOrEmpty(platformTfm))
{
throw new DistributedApplicationException(
$"Unable to detect {platformDisplayName} target framework in project '{projectPath}'. " +
$"Ensure the project file contains a TargetFramework or TargetFrameworks element with a {platformDisplayName} target framework (e.g., {tfmExample}) " +
$"or remove the Add{platformDisplayName.Replace(" ", "")}Device() call from your AppHost.");
}

return Task.CompletedTask;
});

// Check if platform is supported on the current host
if (!isSupported())
{
var reason = $"{platformDisplayName} platform not available on this host";

// Mark as unsupported
resourceBuilder.WithAnnotation(new UnsupportedPlatformAnnotation(reason), ResourceAnnotationMutationBehavior.Append);

// Add an event subscriber to set the "Unsupported" state after orchestrator initialization
var appBuilder = resourceBuilder.ApplicationBuilder;
appBuilder.Services.TryAddEventingSubscriber<UnsupportedPlatformEventSubscriber>();
}
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.Maui/MauiProjectResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Aspire.Hosting.Maui;
/// <param name="projectPath">The path to the .NET MAUI project file.</param>
/// <remarks>
/// This resource serves as a parent for platform-specific MAUI resources (Windows, Android, iOS, macOS).
/// Use extension methods like <c>AddWindowsDevice</c> to add platform-specific instances.
/// Use extension methods like <c>AddWindowsDevice</c> or <c>AddMacCatalystDevice</c> to add platform-specific instances.
/// <para>
/// MAUI projects are built on-demand when the platform-specific resource is started, avoiding long
/// AppHost startup times while still allowing incremental builds during development.
Expand Down
Loading