Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b5fda4a
Add project reference support for polyglot apphost integrations
davidfowl Mar 1, 2026
b5ae562
Remove GetAllPackages, migrate tests to GetIntegrationReferences
davidfowl Mar 1, 2026
769da44
Use single dotnet publish for PrebuiltAppHostServer integration resol…
davidfowl Mar 1, 2026
b3d5a35
Only use dotnet publish when project references are present
davidfowl Mar 1, 2026
ea28690
Use dotnet build with CopyLocalLockFileAssemblies instead of publish
davidfowl Mar 1, 2026
7da892d
Add SDK check for project refs and isolate from parent MSBuild imports
davidfowl Mar 1, 2026
32c0df9
Remove DotNetBasedAppHostServerProject casts from SDK commands
davidfowl Mar 1, 2026
66a3ab9
Use CLI version for SDK commands, let project deps resolve transitively
davidfowl Mar 1, 2026
c7753cd
Add unit tests for IntegrationReference and synthetic project generation
davidfowl Mar 1, 2026
25aa82a
Add E2E test for project reference integration support
davidfowl Mar 1, 2026
5a49b29
Simplify E2E test: use ExecuteCallback for file creation
davidfowl Mar 1, 2026
5778892
Address PR review feedback
davidfowl Mar 1, 2026
d2b702e
Use using directive for System.Xml.Linq
davidfowl Mar 1, 2026
8317809
Improve project reference E2E test with describe/wait verification
davidfowl Mar 1, 2026
26b23bc
Fix E2E test: use redis image instead of nonexistent myservice
davidfowl Mar 1, 2026
35ba2cc
Fix E2E test: correct project reference path and force codegen regene…
davidfowl Mar 1, 2026
babcde2
Fix: write nuget.config with channel sources for project ref builds
davidfowl Mar 1, 2026
431f132
Fix CheckAsync tuple deconstruction after release/13.2 rebase
davidfowl Mar 1, 2026
60ae7b9
Add build output logging for integration project failures
davidfowl Mar 2, 2026
b213eca
Dump child log on E2E test failure for CI debugging
davidfowl Mar 2, 2026
c31efdf
Fix: use RestoreConfigFile to ensure project references resolve from …
davidfowl Mar 2, 2026
7754985
Fix E2E test: write nuget.config in workspace for project reference r…
davidfowl Mar 2, 2026
5202e22
Address JamesNK review feedback
davidfowl Mar 3, 2026
1903c3d
Use RestoreAdditionalProjectSources in DotNetBasedAppHostServerProject
davidfowl Mar 3, 2026
3af7787
Address JamesNK and sebastienros review feedback
davidfowl Mar 4, 2026
b682686
Resolve project ref assembly names from MSBuild build output
davidfowl Mar 4, 2026
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
43 changes: 19 additions & 24 deletions src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,39 +122,34 @@ private async Task<int> DumpCapabilitiesAsync(

try
{
// TODO: Support bundle mode by using DLL references instead of project references.
// In bundle mode, we'd need to add integration DLLs to the probing path rather than
// using additionalProjectReferences. For now, SDK dump only works with .NET SDK.
var appHostServerProjectInterface = await _appHostServerProjectFactory.CreateAsync(tempDir, cancellationToken);
if (appHostServerProjectInterface is not DotNetBasedAppHostServerProject appHostServerProject)
var appHostServerProject = await _appHostServerProjectFactory.CreateAsync(tempDir, cancellationToken);

// Build integrations list - optional integration project reference
var integrations = new List<IntegrationReference>();

if (integrationProject is not null)
{
InteractionService.DisplayError("SDK dump is only available with .NET SDK installed.");
return ExitCodeConstants.FailedToBuildArtifacts;
integrations.Add(IntegrationReference.FromProject(
Path.GetFileNameWithoutExtension(integrationProject.FullName),
integrationProject.FullName));
}

// Build packages list - empty since we only need core capabilities + optional integration
var packages = new List<(string Name, string Version)>();

_logger.LogDebug("Building AppHost server for capability scanning");

// Create project files with the integration project reference if specified
var additionalProjectRefs = integrationProject is not null
? new[] { integrationProject.FullName }
: null;
var prepareResult = await appHostServerProject.PrepareAsync(
VersionHelper.GetDefaultTemplateVersion(),
integrations,
cancellationToken);

await appHostServerProject.CreateProjectFilesAsync(
packages,
cancellationToken,
additionalProjectReferences: additionalProjectRefs);

var (buildSuccess, buildOutput) = await appHostServerProject.BuildAsync(cancellationToken);

if (!buildSuccess)
if (!prepareResult.Success)
{
InteractionService.DisplayError("Failed to build capability scanner.");
foreach (var (_, line) in buildOutput.GetLines())
if (prepareResult.Output is not null)
{
InteractionService.DisplayMessage(KnownEmojis.Wrench, line);
foreach (var (_, line) in prepareResult.Output.GetLines())
{
InteractionService.DisplayMessage(KnownEmojis.Wrench, line);
}
}
return ExitCodeConstants.FailedToBuildArtifacts;
}
Expand Down
42 changes: 20 additions & 22 deletions src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,42 +120,40 @@ private async Task<int> GenerateSdkAsync(

try
{
// TODO: Support bundle mode by using DLL references instead of project references.
// In bundle mode, we'd need to add integration DLLs to the probing path rather than
// using additionalProjectReferences. For now, SDK generation only works with .NET SDK.
var appHostServerProjectInterface = await _appHostServerProjectFactory.CreateAsync(tempDir, cancellationToken);
if (appHostServerProjectInterface is not DotNetBasedAppHostServerProject appHostServerProject)
{
InteractionService.DisplayError("SDK generation is only available with .NET SDK installed.");
return ExitCodeConstants.FailedToBuildArtifacts;
}
var appHostServerProject = await _appHostServerProjectFactory.CreateAsync(tempDir, cancellationToken);

// Get code generation package for the target language
var codeGenPackage = await _languageDiscovery.GetPackageForLanguageAsync(languageInfo.LanguageId, cancellationToken);

// Build packages list - include the code generator
var packages = new List<(string Name, string Version)>();
// Build integrations list — the integration project brings Aspire.Hosting transitively;
// we only need to add the codegen package and the project reference itself.
var integrations = new List<IntegrationReference>();
if (codeGenPackage is not null)
{
packages.Add((codeGenPackage, DotNetBasedAppHostServerProject.DefaultSdkVersion));
integrations.Add(IntegrationReference.FromPackage(codeGenPackage, VersionHelper.GetDefaultTemplateVersion()));
}

_logger.LogDebug("Building AppHost server for SDK generation");
// Add the integration project as a project reference
integrations.Add(IntegrationReference.FromProject(
Path.GetFileNameWithoutExtension(integrationProject.FullName),
integrationProject.FullName));

// Create project files with the integration project reference
await appHostServerProject.CreateProjectFilesAsync(
packages,
cancellationToken,
additionalProjectReferences: [integrationProject.FullName]);
_logger.LogDebug("Building AppHost server for SDK generation");

var (buildSuccess, buildOutput) = await appHostServerProject.BuildAsync(cancellationToken);
var prepareResult = await appHostServerProject.PrepareAsync(
VersionHelper.GetDefaultTemplateVersion(),
integrations,
cancellationToken);

if (!buildSuccess)
if (!prepareResult.Success)
{
InteractionService.DisplayError("Failed to build SDK generation server.");
foreach (var (_, line) in buildOutput.GetLines())
if (prepareResult.Output is not null)
{
InteractionService.DisplayMessage(KnownEmojis.Wrench, line);
foreach (var (_, line) in prepareResult.Output.GetLines())
{
InteractionService.DisplayMessage(KnownEmojis.Wrench, line);
}
}
return ExitCodeConstants.FailedToBuildArtifacts;
}
Expand Down
51 changes: 30 additions & 21 deletions src/Aspire.Cli/Configuration/AspireJsonConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,24 +172,27 @@ public string GetEffectiveSdkVersion(string defaultSdkVersion)
}

/// <summary>
/// Gets all package references including the base Aspire.Hosting package.
/// Empty package versions in settings are resolved to the effective SDK version.
/// Gets all integration references (both NuGet packages and project references)
/// including the base Aspire.Hosting package.
/// A value ending in ".csproj" is treated as a project reference; otherwise as a NuGet version.
/// Empty package versions are resolved to the effective SDK version.
/// </summary>
/// <param name="defaultSdkVersion">Default SDK version to use when not configured.</param>
/// <returns>Enumerable of (PackageName, Version) tuples.</returns>
public IEnumerable<(string Name, string Version)> GetAllPackages(string defaultSdkVersion)
/// <param name="settingsDirectory">The directory containing .aspire/settings.json, used to resolve relative project paths.</param>
/// <returns>Enumerable of IntegrationReference objects.</returns>
public IEnumerable<IntegrationReference> GetIntegrationReferences(string defaultSdkVersion, string settingsDirectory)
{
var sdkVersion = GetEffectiveSdkVersion(defaultSdkVersion);

// Base package always included (Aspire.Hosting.AppHost is an SDK, not a runtime DLL)
yield return ("Aspire.Hosting", sdkVersion);
// Base package always included
yield return IntegrationReference.FromPackage("Aspire.Hosting", sdkVersion);

if (Packages is null)
{
yield break;
}

foreach (var (packageName, version) in Packages)
foreach (var (packageName, value) in Packages)
{
// Skip base packages and SDK-only packages
if (string.Equals(packageName, "Aspire.Hosting", StringComparison.OrdinalIgnoreCase) ||
Expand All @@ -198,21 +201,27 @@ public string GetEffectiveSdkVersion(string defaultSdkVersion)
continue;
}

yield return (packageName, string.IsNullOrWhiteSpace(version) ? sdkVersion : version);
var trimmedValue = value?.Trim();

if (string.IsNullOrEmpty(trimmedValue))
{
// NuGet package reference with no explicit version — fall back to the SDK version
yield return IntegrationReference.FromPackage(packageName, sdkVersion);
continue;
}

if (trimmedValue.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
{
// Project reference — resolve relative path to absolute
var absolutePath = Path.GetFullPath(Path.Combine(settingsDirectory, trimmedValue));
yield return IntegrationReference.FromProject(packageName, absolutePath);
}
else
{
// NuGet package reference with explicit version
yield return IntegrationReference.FromPackage(packageName, trimmedValue);
}
}
}

/// <summary>
/// Gets all package references including the base Aspire.Hosting packages.
/// Uses the SdkVersion for base packages.
/// Note: Aspire.Hosting.AppHost is an SDK package (not a runtime DLL) and is excluded.
/// </summary>
/// <returns>Enumerable of (PackageName, Version) tuples.</returns>
public IEnumerable<(string Name, string Version)> GetAllPackages()
{
var sdkVersion = !string.IsNullOrWhiteSpace(SdkVersion)
? SdkVersion
: throw new InvalidOperationException("SdkVersion must be set to a non-empty value before calling GetAllPackages. Use LoadOrCreate to ensure it's set.");
return GetAllPackages(sdkVersion);
}
}
63 changes: 63 additions & 0 deletions src/Aspire.Cli/Configuration/IntegrationReference.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Cli.Configuration;

/// <summary>
/// Represents a reference to an Aspire hosting integration, which can be either
/// a NuGet package (with a version) or a local project reference (with a path to a .csproj).
/// Exactly one of <see cref="Version"/> or <see cref="ProjectPath"/> must be non-null.
/// </summary>
internal sealed class IntegrationReference
{
/// <summary>
/// Gets the package or assembly name (e.g., "Aspire.Hosting.Redis").
/// </summary>
public required string Name { get; init; }

/// <summary>
/// Gets the NuGet package version, or null for project references.
/// </summary>
public string? Version { get; init; }

/// <summary>
/// Gets the absolute path to the .csproj file, or null for NuGet packages.
/// </summary>
public string? ProjectPath { get; init; }

/// <summary>
/// Returns true if this is a project reference (has a .csproj path).
/// </summary>
public bool IsProjectReference => ProjectPath is not null;

/// <summary>
/// Returns true if this is a NuGet package reference (has a version).
/// </summary>
public bool IsPackageReference => Version is not null;

/// <summary>
/// Creates a NuGet package reference.
/// </summary>
/// <param name="name">The package name.</param>
/// <param name="version">The NuGet package version.</param>
public static IntegrationReference FromPackage(string name, string version)
{
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentException.ThrowIfNullOrEmpty(version);

return new IntegrationReference { Name = name, Version = version };
}

/// <summary>
/// Creates a local project reference.
/// </summary>
/// <param name="name">The assembly name.</param>
/// <param name="projectPath">The absolute path to the .csproj file.</param>
public static IntegrationReference FromProject(string name, string projectPath)
{
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentException.ThrowIfNullOrEmpty(projectPath);

return new IntegrationReference { Name = name, ProjectPath = projectPath };
}
}
3 changes: 3 additions & 0 deletions src/Aspire.Cli/Projects/AppHostServerProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal sealed class AppHostServerProjectFactory(
IConfigurationService configurationService,
IBundleService bundleService,
BundleNuGetService bundleNuGetService,
IDotNetSdkInstaller sdkInstaller,
ILoggerFactory loggerFactory) : IAppHostServerProjectFactory
{
public async Task<IAppHostServerProject> CreateAsync(string appPath, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -83,6 +84,8 @@ public async Task<IAppHostServerProject> CreateAsync(string appPath, Cancellatio
socketPath,
layout,
bundleNuGetService,
dotNetCliRunner,
sdkInstaller,
packagingService,
configurationService,
loggerFactory.CreateLogger<PrebuiltAppHostServer>());
Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Cli/Projects/AppHostServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Aspire.Cli.Configuration;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -100,15 +101,15 @@ public AppHostServerSessionFactory(
public async Task<AppHostServerSessionResult> CreateAsync(
string appHostPath,
string sdkVersion,
IEnumerable<(string PackageId, string Version)> packages,
IEnumerable<IntegrationReference> integrations,
Dictionary<string, string>? launchSettingsEnvVars,
bool debug,
CancellationToken cancellationToken)
{
var appHostServerProject = await _projectFactory.CreateAsync(appHostPath, cancellationToken);

// Prepare the server (create files + build for dev mode, restore packages for prebuilt mode)
var prepareResult = await appHostServerProject.PrepareAsync(sdkVersion, packages, cancellationToken);
var prepareResult = await appHostServerProject.PrepareAsync(sdkVersion, integrations, cancellationToken);
if (!prepareResult.Success)
{
return new AppHostServerSessionResult(
Expand Down
Loading
Loading