// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Aspire.Cli.Bundles;
using Aspire.Cli.Configuration;
using Aspire.Cli.DotNet;
using Aspire.Cli.Layout;
using Aspire.Cli.NuGet;
using Aspire.Cli.Packaging;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;
namespace Aspire.Cli.Projects;
/// <summary>
/// Factory for creating AppHostServerProject instances with required dependencies.
/// </summary>
internal interface IAppHostServerProjectFactory
{
string GetInstanceIdentifier(string appPath);
Task<IAppHostServerProject> CreateAsync(string appPath, CancellationToken cancellationToken = default);
Task<IAppHostServerProject> CreateAsync(string appPath, FileInfo appHostFile, CancellationToken cancellationToken);
Task<IAppHostServerProject> CreateAsync(string appPath, string? restoreRootConfigDirectory, CancellationToken cancellationToken);
}
/// <summary>
/// Factory implementation that creates IAppHostServerProject instances.
/// Chooses between DotNetBasedAppHostServerProject (dev mode) and PrebuiltAppHostServer (bundle mode).
/// </summary>
internal sealed class AppHostServerProjectFactory(
IDotNetCliRunner dotNetCliRunner,
IPackagingService packagingService,
IBundleService bundleService,
BundleNuGetService bundleNuGetService,
IDotNetSdkInstaller sdkInstaller,
CliExecutionContext executionContext,
IEnvironment environment,
IProcessExecutionFactory processExecutionFactory,
ILoggerFactory loggerFactory) : IAppHostServerProjectFactory
{
public string GetInstanceIdentifier(string appPath)
{
if (IsNativeServerEnabled(appPath) || AspireRepositoryDetector.DetectRepositoryRoot(appPath) is null)
{
return appPath;
}
return DotNetBasedAppHostServerProject.GetDefaultInstanceIdentifier(appPath);
}
public Task<IAppHostServerProject> CreateAsync(string appPath, CancellationToken cancellationToken = default)
=> CreateAsync(
appPath,
restoreRootConfigDirectory: null,
workloadId: AppHostWorkloadId.Create(appPath),
cancellationToken: cancellationToken);
public Task<IAppHostServerProject> CreateAsync(
string appPath,
FileInfo appHostFile,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(appHostFile);
return CreateAsync(
appPath,
restoreRootConfigDirectory: null,
workloadId: AppHostWorkloadId.Create(appHostFile),
cancellationToken: cancellationToken);
}
public async Task<IAppHostServerProject> CreateAsync(string appPath, string? restoreRootConfigDirectory, CancellationToken cancellationToken)
=> await CreateAsync(
appPath,
restoreRootConfigDirectory,
AppHostWorkloadId.Create(appPath),
cancellationToken).ConfigureAwait(false);
private async Task<IAppHostServerProject> CreateAsync(
string appPath,
string? restoreRootConfigDirectory,
string workloadId,
CancellationToken cancellationToken)
{
if (IsNativeServerEnabled(appPath))
{
return await NativeAppHostServerProject.CreateAsync(appPath, bundleService, environment,
processExecutionFactory, loggerFactory.CreateLogger<NativeAppHostServerProject>(), cancellationToken).ConfigureAwait(false);
}
var socketPath = CliPathHelper.CreateGuestAppHostSocketPath("apphost.sock");
// Priority 1: Check for dev mode (ASPIRE_REPO_ROOT or running from Aspire source repo)
var repoRoot = AspireRepositoryDetector.DetectRepositoryRoot(appPath);
if (repoRoot is not null)
{
return new DotNetBasedAppHostServerProject(
appPath,
socketPath,
repoRoot,
dotNetCliRunner,
packagingService,
processExecutionFactory,
environment,
loggerFactory.CreateLogger<DotNetBasedAppHostServerProject>(),
logFilePath: executionContext.LogFilePath,
restoreRootConfigDirectory: restoreRootConfigDirectory);
}
// Priority 2: Ensure bundle is extracted and check for layout
var layoutLease = await bundleService.EnsureExtractedAndAcquireLayoutAsync("cli", "apphost-server", cancellationToken);
var layout = layoutLease?.Layout;
// Priority 3: Check if we have a bundle layout with a pre-built AppHost server
if (layout is not null && layout.GetManagedPath() is string serverPath && File.Exists(serverPath))
{
return CreatePrebuiltAppHostServer(appPath, socketPath, layout, layoutLease, workloadId);
}
layoutLease?.Dispose();
throw new InvalidOperationException(
"No Aspire AppHost server is available. Ensure the Aspire CLI is installed " +
"with a valid bundle layout, or reinstall using 'aspire setup --force'.");
}
internal PrebuiltAppHostServer CreatePrebuiltAppHostServer(
string appPath,
string socketPath,
LayoutConfiguration layout,
BundleLayoutLease? layoutLease,
string? workloadId = null)
{
try
{
return new PrebuiltAppHostServer(
appPath,
socketPath,
layout,
bundleNuGetService,
dotNetCliRunner,
sdkInstaller,
packagingService,
executionContext,
processExecutionFactory,
environment,
loggerFactory.CreateLogger<PrebuiltAppHostServer>(),
layoutLease,
workloadId);
}
catch
{
layoutLease?.Dispose();
throw;
}
}
private static bool IsNativeServerEnabled(string appPath)
{
var configDirectory = ConfigurationHelper.GetConfigRootDirectory(new DirectoryInfo(appPath));
return KnownFeatures.IsAppHostServerV2Enabled(AspireConfigFile.Load(configDirectory.FullName));
}
}