Skip to content

Commit bd38e53

Browse files
jfversluisradical
authored andcommitted
Add Aspire.Hosting.Maui (.NET MAUI) Mac Catalyst integration (#12342)
* Add Mac Catalyst * Better code sharing * Update README.md * Update public API file
1 parent d08c84d commit bd38e53

File tree

12 files changed

+656
-74
lines changed

12 files changed

+656
-74
lines changed

playground/AspireWithMaui/AspireWithMaui.AppHost/AppHost.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@
77
mauiapp.AddWindowsDevice()
88
.WithReference(weatherApi);
99

10+
mauiapp.AddMacCatalystDevice()
11+
.WithReference(weatherApi);
12+
1013
builder.Build().Run();

playground/AspireWithMaui/README.md

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,19 @@ After running the restore script with `-restore-maui`, you can build and run the
5656
## What's Included
5757

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

6464
## Features Demonstrated
6565

66-
### MAUI Windows Platform Support
67-
The playground demonstrates Aspire's ability to manage MAUI apps on Windows:
68-
- Configures the MAUI app with `.AddMauiWindows()`
69-
- Automatically detects the Windows target framework from the project file
66+
### MAUI Multi-Platform Support
67+
The playground demonstrates Aspire's ability to manage MAUI apps on multiple platforms:
68+
- **Windows**: Configures the MAUI app with `.AddWindowsDevice()`
69+
- **Mac Catalyst**: Configures the MAUI app with `.AddMacCatalystDevice()`
70+
- Automatically detects platform-specific target frameworks from the project file
71+
- Shows "Unsupported" state in dashboard when running on incompatible host OS
7072
- Sets up dev tunnels for MAUI app communication with backend services
7173

7274
### OpenTelemetry Integration
@@ -76,8 +78,8 @@ The MAUI client uses OpenTelemetry to send traces and metrics to the Aspire dash
7678
The MAUI app discovers and connects to backend services (WeatherApi) using Aspire's service discovery.
7779

7880
### Future Platform Support
79-
The architecture is designed to support additional platforms (Android, iOS, macCatalyst) through:
80-
- `.AddMauiAndroid()`, `.AddMauiIos()`, `.AddMauiMacCatalyst()` extension methods (coming in future updates)
81+
The architecture is designed to support additional platforms (Android, iOS) through:
82+
- `.AddAndroidDevice()`, `.AddIosDevice()` extension methods (coming in future updates)
8183
- Parallel extension patterns for each platform
8284

8385
## Troubleshooting
@@ -95,23 +97,25 @@ If you encounter build errors:
9597
3. Try running `dotnet build` from the repository root first
9698

9799
### Platform-Specific Issues
98-
- **Windows**: Requires Windows 10 build 19041 or higher for WinUI support
100+
- **Windows**: Requires Windows 10 build 19041 or higher for WinUI support. Mac Catalyst devices will show as "Unsupported" when running on Windows.
101+
- **Mac Catalyst**: Requires macOS to run. Windows devices will show as "Unsupported" when running on macOS.
99102
- **Android**: Not yet implemented in this playground (coming soon)
100-
- **iOS/macCatalyst**: Not yet implemented in this playground (coming soon)
103+
- **iOS**: Not yet implemented in this playground (coming soon)
101104

102105
## Current Status
103106

104107
**Implemented:**
105-
- Windows platform support via `AddMauiWindows()`
106-
- Automatic Windows TFM detection from project file
108+
- Windows platform support via `AddWindowsDevice()`
109+
- Mac Catalyst platform support via `AddMacCatalystDevice()`
110+
- Automatic platform-specific TFM detection from project file
111+
- Platform validation with "Unsupported" state for incompatible hosts
107112
- Dev tunnel configuration for MAUI-to-backend communication
108113
- Service discovery integration
109114
- OpenTelemetry integration
110115

111116
🚧 **Coming Soon:**
112-
- Android platform support
113-
- iOS platform support
114-
- macCatalyst platform support
117+
- Android platform support via `AddAndroidDevice()`
118+
- iOS platform support via `AddIosDevice()`
115119
- Multi-platform simultaneous debugging
116120

117121
## Learn More
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire.Hosting.Maui;
5+
6+
/// <summary>
7+
/// Marker interface for MAUI platform-specific resources (Windows, Android, iOS, Mac Catalyst).
8+
/// </summary>
9+
/// <remarks>
10+
/// This interface is used to identify resources that represent a specific platform instance
11+
/// of a MAUI application, allowing for common handling across all MAUI platforms.
12+
/// </remarks>
13+
internal interface IMauiPlatformResource
14+
{
15+
}

src/Aspire.Hosting.Maui/Lifecycle/UnsupportedPlatformEventSubscriber.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ namespace Aspire.Hosting.Maui.Lifecycle;
1212
/// Event subscriber that sets the "Unsupported" state for MAUI platform resources
1313
/// marked with <see cref="UnsupportedPlatformAnnotation"/>.
1414
/// </summary>
15+
/// <remarks>
16+
/// This subscriber handles all MAUI platform resources (Windows, Android, iOS, Mac Catalyst)
17+
/// by checking for the <see cref="IMauiPlatformResource"/> marker interface.
18+
/// </remarks>
1519
/// <param name="notificationService">The notification service for publishing resource state updates.</param>
1620
internal sealed class UnsupportedPlatformEventSubscriber(ResourceNotificationService notificationService) : IDistributedApplicationEventingSubscriber
1721
{
@@ -23,7 +27,7 @@ public Task SubscribeAsync(IDistributedApplicationEventing eventing, Distributed
2327
// Find all MAUI platform resources with the UnsupportedPlatformAnnotation
2428
foreach (var resource in @event.Model.Resources)
2529
{
26-
if (resource is MauiWindowsPlatformResource &&
30+
if (resource is IMauiPlatformResource &&
2731
resource.TryGetLastAnnotation<UnsupportedPlatformAnnotation>(out var annotation))
2832
{
2933
// Set the state to "Unsupported" with a warning style and the reason
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
using Aspire.Hosting.Maui;
6+
7+
namespace Aspire.Hosting;
8+
9+
/// <summary>
10+
/// Provides extension methods for adding Mac Catalyst platform resources to MAUI projects.
11+
/// </summary>
12+
public static class MauiMacCatalystExtensions
13+
{
14+
/// <summary>
15+
/// Adds a Mac Catalyst device resource to run the MAUI application on the macOS platform.
16+
/// </summary>
17+
/// <param name="builder">The MAUI project resource builder.</param>
18+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
19+
/// <remarks>
20+
/// This method creates a new Mac Catalyst platform resource that will run the MAUI application
21+
/// targeting the Mac Catalyst platform using <c>dotnet run</c>. The resource does not auto-start
22+
/// and must be explicitly started from the dashboard by clicking the start button.
23+
/// <para>
24+
/// The resource name will default to "{projectName}-maccatalyst".
25+
/// </para>
26+
/// </remarks>
27+
/// <example>
28+
/// Add a Mac Catalyst device to a MAUI project:
29+
/// <code lang="csharp">
30+
/// var builder = DistributedApplication.CreateBuilder(args);
31+
///
32+
/// var maui = builder.AddMauiProject("mauiapp", "../MyMauiApp/MyMauiApp.csproj");
33+
/// var macCatalystDevice = maui.AddMacCatalystDevice();
34+
///
35+
/// builder.Build().Run();
36+
/// </code>
37+
/// </example>
38+
public static IResourceBuilder<MauiMacCatalystPlatformResource> AddMacCatalystDevice(
39+
this IResourceBuilder<MauiProjectResource> builder)
40+
{
41+
ArgumentNullException.ThrowIfNull(builder);
42+
43+
var name = $"{builder.Resource.Name}-maccatalyst";
44+
return builder.AddMacCatalystDevice(name);
45+
}
46+
47+
/// <summary>
48+
/// Adds a Mac Catalyst device resource to run the MAUI application on the macOS platform with a specific name.
49+
/// </summary>
50+
/// <param name="builder">The MAUI project resource builder.</param>
51+
/// <param name="name">The name of the Mac Catalyst device resource.</param>
52+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
53+
/// <remarks>
54+
/// This method creates a new Mac Catalyst platform resource that will run the MAUI application
55+
/// targeting the Mac Catalyst platform using <c>dotnet run</c>. The resource does not auto-start
56+
/// and must be explicitly started from the dashboard by clicking the start button.
57+
/// <para>
58+
/// Multiple Mac Catalyst device resources can be added to the same MAUI project if needed, each with
59+
/// a unique name.
60+
/// </para>
61+
/// </remarks>
62+
/// <example>
63+
/// Add multiple Mac Catalyst devices to a MAUI project:
64+
/// <code lang="csharp">
65+
/// var builder = DistributedApplication.CreateBuilder(args);
66+
///
67+
/// var maui = builder.AddMauiProject("mauiapp", "../MyMauiApp/MyMauiApp.csproj");
68+
/// var macCatalystDevice1 = maui.AddMacCatalystDevice("maccatalyst-device-1");
69+
/// var macCatalystDevice2 = maui.AddMacCatalystDevice("maccatalyst-device-2");
70+
///
71+
/// builder.Build().Run();
72+
/// </code>
73+
/// </example>
74+
public static IResourceBuilder<MauiMacCatalystPlatformResource> AddMacCatalystDevice(
75+
this IResourceBuilder<MauiProjectResource> builder,
76+
[ResourceName] string name)
77+
{
78+
ArgumentNullException.ThrowIfNull(builder);
79+
ArgumentException.ThrowIfNullOrWhiteSpace(name);
80+
81+
// Check if a Mac Catalyst device with this name already exists in the application model
82+
var existingMacCatalystDevices = builder.ApplicationBuilder.Resources
83+
.OfType<MauiMacCatalystPlatformResource>()
84+
.FirstOrDefault(r => r.Parent == builder.Resource &&
85+
string.Equals(r.Name, name, StringComparisons.ResourceName));
86+
87+
if (existingMacCatalystDevices is not null)
88+
{
89+
throw new DistributedApplicationException(
90+
$"Mac Catalyst device with name '{name}' already exists on MAUI project '{builder.Resource.Name}'. " +
91+
$"Provide a unique name parameter when calling AddMacCatalystDevice() to add multiple Mac Catalyst devices.");
92+
}
93+
94+
// Get the absolute project path and working directory
95+
var (projectPath, workingDirectory) = MauiPlatformHelper.GetProjectPaths(builder);
96+
97+
var macCatalystResource = new MauiMacCatalystPlatformResource(name, builder.Resource);
98+
99+
var resourceBuilder = builder.ApplicationBuilder.AddResource(macCatalystResource)
100+
.WithAnnotation(new MauiProjectMetadata(projectPath))
101+
.WithAnnotation(new ExecutableAnnotation
102+
{
103+
Command = "dotnet",
104+
WorkingDirectory = workingDirectory
105+
});
106+
107+
// Configure the platform resource with common settings
108+
MauiPlatformHelper.ConfigurePlatformResource(
109+
resourceBuilder,
110+
projectPath,
111+
"maccatalyst",
112+
"Mac Catalyst",
113+
"net10.0-maccatalyst",
114+
OperatingSystem.IsMacOS,
115+
"Desktop",
116+
"-p:OpenArguments=-W");
117+
118+
return resourceBuilder;
119+
}
120+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
6+
namespace Aspire.Hosting.Maui;
7+
8+
/// <summary>
9+
/// Represents a Mac Catalyst platform instance of a .NET MAUI project.
10+
/// </summary>
11+
/// <param name="name">The name of the resource.</param>
12+
/// <param name="parent">The parent MAUI project resource.</param>
13+
/// <remarks>
14+
/// This resource represents a MAUI application running on the Mac Catalyst platform.
15+
/// The actual build and deployment happens when the resource is started, allowing for
16+
/// incremental builds during development without blocking AppHost startup.
17+
/// <para>
18+
/// Use <see cref="MauiMacCatalystExtensions.AddMacCatalystDevice(IResourceBuilder{MauiProjectResource}, string?)"/>
19+
/// to add this resource to a MAUI project.
20+
/// </para>
21+
/// </remarks>
22+
public class MauiMacCatalystPlatformResource(string name, MauiProjectResource parent)
23+
: ProjectResource(name), IResourceWithParent<MauiProjectResource>, IMauiPlatformResource
24+
{
25+
/// <summary>
26+
/// Gets the parent MAUI project resource.
27+
/// </summary>
28+
public MauiProjectResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent));
29+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
using Aspire.Hosting.Lifecycle;
6+
using Aspire.Hosting.Maui.Annotations;
7+
using Aspire.Hosting.Maui.Lifecycle;
8+
using Aspire.Hosting.Maui.Utilities;
9+
using Aspire.Hosting.Utils;
10+
11+
namespace Aspire.Hosting.Maui;
12+
13+
/// <summary>
14+
/// Helper methods for adding platform-specific MAUI device resources.
15+
/// </summary>
16+
internal static class MauiPlatformHelper
17+
{
18+
/// <summary>
19+
/// Gets the absolute project path and working directory from a MAUI project resource.
20+
/// </summary>
21+
/// <param name="builder">The MAUI project resource builder.</param>
22+
/// <returns>A tuple containing the absolute project path and working directory.</returns>
23+
internal static (string ProjectPath, string WorkingDirectory) GetProjectPaths(IResourceBuilder<MauiProjectResource> builder)
24+
{
25+
var projectPath = builder.Resource.ProjectPath;
26+
if (!Path.IsPathRooted(projectPath))
27+
{
28+
projectPath = PathNormalizer.NormalizePathForCurrentPlatform(
29+
Path.Combine(builder.ApplicationBuilder.AppHostDirectory, projectPath));
30+
}
31+
32+
var workingDirectory = Path.GetDirectoryName(projectPath)
33+
?? throw new InvalidOperationException($"Unable to determine directory from project path: {projectPath}");
34+
35+
return (projectPath, workingDirectory);
36+
}
37+
38+
/// <summary>
39+
/// Configures a platform resource with common settings and TFM validation.
40+
/// </summary>
41+
/// <typeparam name="T">The type of platform resource.</typeparam>
42+
/// <param name="resourceBuilder">The resource builder.</param>
43+
/// <param name="projectPath">The absolute path to the project file.</param>
44+
/// <param name="platformName">The platform name (e.g., "windows", "maccatalyst").</param>
45+
/// <param name="platformDisplayName">The display name for the platform (e.g., "Windows", "Mac Catalyst").</param>
46+
/// <param name="tfmExample">Example TFM for error messages (e.g., "net10.0-windows10.0.19041.0").</param>
47+
/// <param name="isSupported">Function to check if the platform is supported on the current host.</param>
48+
/// <param name="iconName">The icon name for the resource.</param>
49+
/// <param name="additionalArgs">Optional additional command-line arguments to pass to dotnet run.</param>
50+
internal static void ConfigurePlatformResource<T>(
51+
IResourceBuilder<T> resourceBuilder,
52+
string projectPath,
53+
string platformName,
54+
string platformDisplayName,
55+
string tfmExample,
56+
Func<bool> isSupported,
57+
string iconName = "Desktop",
58+
params string[] additionalArgs) where T : ProjectResource
59+
{
60+
// Check if the project has the platform TFM and get the actual TFM value
61+
var platformTfm = ProjectFileReader.GetPlatformTargetFramework(projectPath, platformName);
62+
63+
// Set the command line arguments with the detected TFM if available
64+
resourceBuilder.WithArgs(context =>
65+
{
66+
context.Args.Add("run");
67+
if (!string.IsNullOrEmpty(platformTfm))
68+
{
69+
context.Args.Add("-f");
70+
context.Args.Add(platformTfm);
71+
}
72+
// Add any additional platform-specific arguments
73+
foreach (var arg in additionalArgs)
74+
{
75+
context.Args.Add(arg);
76+
}
77+
});
78+
79+
resourceBuilder
80+
.WithOtlpExporter()
81+
.WithIconName(iconName)
82+
.WithExplicitStart();
83+
84+
// Validate the platform TFM when the resource is about to start
85+
resourceBuilder.OnBeforeResourceStarted((resource, eventing, ct) =>
86+
{
87+
// If we couldn't detect the TFM earlier, fail the resource start
88+
if (string.IsNullOrEmpty(platformTfm))
89+
{
90+
throw new DistributedApplicationException(
91+
$"Unable to detect {platformDisplayName} target framework in project '{projectPath}'. " +
92+
$"Ensure the project file contains a TargetFramework or TargetFrameworks element with a {platformDisplayName} target framework (e.g., {tfmExample}) " +
93+
$"or remove the Add{platformDisplayName.Replace(" ", "")}Device() call from your AppHost.");
94+
}
95+
96+
return Task.CompletedTask;
97+
});
98+
99+
// Check if platform is supported on the current host
100+
if (!isSupported())
101+
{
102+
var reason = $"{platformDisplayName} platform not available on this host";
103+
104+
// Mark as unsupported
105+
resourceBuilder.WithAnnotation(new UnsupportedPlatformAnnotation(reason), ResourceAnnotationMutationBehavior.Append);
106+
107+
// Add an event subscriber to set the "Unsupported" state after orchestrator initialization
108+
var appBuilder = resourceBuilder.ApplicationBuilder;
109+
appBuilder.Services.TryAddEventingSubscriber<UnsupportedPlatformEventSubscriber>();
110+
}
111+
}
112+
}

src/Aspire.Hosting.Maui/MauiProjectResource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Aspire.Hosting.Maui;
1212
/// <param name="projectPath">The path to the .NET MAUI project file.</param>
1313
/// <remarks>
1414
/// This resource serves as a parent for platform-specific MAUI resources (Windows, Android, iOS, macOS).
15-
/// Use extension methods like <c>AddWindowsDevice</c> to add platform-specific instances.
15+
/// Use extension methods like <c>AddWindowsDevice</c> or <c>AddMacCatalystDevice</c> to add platform-specific instances.
1616
/// <para>
1717
/// MAUI projects are built on-demand when the platform-specific resource is started, avoiding long
1818
/// AppHost startup times while still allowing incremental builds during development.

0 commit comments

Comments
 (0)