File: TestServices\TestAppHostProjectFactory.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// 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.Projects;
 
namespace Aspire.Cli.Tests.TestServices;
 
/// <summary>
/// Test implementation of IAppHostProjectFactory that simulates .NET project detection.
/// </summary>
internal sealed class TestAppHostProjectFactory : IAppHostProjectFactory
{
    private static readonly HashSet<string> s_projectExtensions = new(StringComparer.OrdinalIgnoreCase) { ".csproj", ".fsproj", ".vbproj" };
    private readonly TestAppHostProject _testProject;
 
    /// <summary>
    /// Optional callback to control validation behavior. If not set, all valid project files are considered valid AppHosts.
    /// </summary>
    public Func<FileInfo, AppHostValidationResult>? ValidateAppHostCallback { get; set; }
 
    public TestAppHostProjectFactory()
    {
        _testProject = new TestAppHostProject(this);
    }
 
    public IAppHostProject GetProject(FileInfo appHostFile)
    {
        return TryGetProject(appHostFile) ?? throw new NotSupportedException($"No handler available for AppHost file '{appHostFile.Name}'.");
    }
 
    public IAppHostProject? TryGetProject(FileInfo appHostFile)
    {
        // Handle .csproj, .fsproj, .vbproj files
        if (s_projectExtensions.Contains(appHostFile.Extension))
        {
            return _testProject;
        }
 
        // Handle apphost.cs single-file apphosts
        if (appHostFile.Name.Equals("apphost.cs", StringComparison.OrdinalIgnoreCase))
        {
            // Check for #:sdk Aspire.AppHost.Sdk directive
            if (IsValidSingleFileAppHost(appHostFile))
            {
                return _testProject;
            }
        }
 
        return null;
    }
 
    public IAppHostProject? GetProjectByLanguageId(string languageId)
    {
        if (languageId.Equals("csharp", StringComparison.OrdinalIgnoreCase))
        {
            return _testProject;
        }
        return null;
    }
 
    public IEnumerable<IAppHostProject> GetAllProjects()
    {
        return [_testProject];
    }
 
    private static bool IsValidSingleFileAppHost(FileInfo candidateFile)
    {
        // Check no sibling .csproj files exist
        var siblingCsprojFiles = candidateFile.Directory!.EnumerateFiles("*.csproj", SearchOption.TopDirectoryOnly);
        if (siblingCsprojFiles.Any())
        {
            return false;
        }
 
        // Check for #:sdk Aspire.AppHost.Sdk directive
        try
        {
            using var reader = candidateFile.OpenText();
            string? line;
            while ((line = reader.ReadLine()) is not null)
            {
                var trimmedLine = line.TrimStart();
                if (trimmedLine.StartsWith("#:sdk Aspire.AppHost.Sdk", StringComparison.Ordinal))
                {
                    return true;
                }
            }
        }
        catch
        {
            return false;
        }
 
        return false;
    }
 
    /// <summary>
    /// Minimal test implementation of IAppHostProject for .NET projects.
    /// </summary>
    private sealed class TestAppHostProject : IAppHostProject
    {
        private static readonly string[] s_detectionPatterns = ["*.csproj", "*.fsproj", "*.vbproj", "apphost.cs"];
        private readonly TestAppHostProjectFactory _factory;
 
        public TestAppHostProject(TestAppHostProjectFactory factory)
        {
            _factory = factory;
        }
 
        public string LanguageId => "csharp";
        public string DisplayName => "C# (.NET)";
        public string[] DetectionPatterns => s_detectionPatterns;
        public string AppHostFileName => "AppHost.csproj";
 
        public bool CanHandle(FileInfo appHostFile)
        {
            var extension = appHostFile.Extension.ToLowerInvariant();
            if (extension is ".csproj" or ".fsproj" or ".vbproj")
            {
                return true;
            }
            if (appHostFile.Name.Equals("apphost.cs", StringComparison.OrdinalIgnoreCase))
            {
                return IsValidSingleFileAppHost(appHostFile);
            }
            return false;
        }
 
        public Task ScaffoldAsync(DirectoryInfo directory, string? projectName, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<int> RunAsync(AppHostProjectContext context, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<int> PublishAsync(PublishContext context, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<IReadOnlyList<(string PackageId, string Version)>> GetPackageReferencesAsync(FileInfo appHostFile, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<AppHostValidationResult> ValidateAppHostAsync(FileInfo appHostFile, CancellationToken cancellationToken)
        {
            if (_factory.ValidateAppHostCallback is not null)
            {
                return Task.FromResult(_factory.ValidateAppHostCallback(appHostFile));
            }
            return Task.FromResult(new AppHostValidationResult(IsValid: true));
        }
 
        public Task<bool> AddPackageAsync(AddPackageContext context, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<UpdatePackagesResult> UpdatePackagesAsync(UpdatePackagesContext context, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<bool> CheckAndHandleRunningInstanceAsync(FileInfo appHostFile, DirectoryInfo homeDirectory, CancellationToken cancellationToken)
            => Task.FromResult(true);
 
        private static bool IsValidSingleFileAppHost(FileInfo candidateFile)
        {
            var siblingCsprojFiles = candidateFile.Directory!.EnumerateFiles("*.csproj", SearchOption.TopDirectoryOnly);
            if (siblingCsprojFiles.Any())
            {
                return false;
            }
 
            try
            {
                using var reader = candidateFile.OpenText();
                string? line;
                while ((line = reader.ReadLine()) is not null)
                {
                    var trimmedLine = line.TrimStart();
                    if (trimmedLine.StartsWith("#:sdk Aspire.AppHost.Sdk", StringComparison.Ordinal))
                    {
                        return true;
                    }
                }
            }
            catch
            {
                return false;
            }
 
            return false;
        }
    }
}