File: Projects\CSharpProjectFile.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Xml.Linq;
using Aspire.Cli.Configuration;
 
namespace Aspire.Cli.Projects;
 
/// <summary>
/// Mutable model used to generate small SDK-style C# project files for Aspire CLI infrastructure.
/// </summary>
/// <param name="sdk">The SDK attribute value to write on the generated project root.</param>
internal sealed class CSharpProjectFile(string sdk = "Microsoft.NET.Sdk")
{
    /// <summary>
    /// Gets the SDK attribute value written on the generated project root.
    /// </summary>
    public string Sdk { get; } = sdk;
 
    /// <summary>
    /// Gets the generated project properties.
    /// </summary>
    public List<CSharpProjectProperty> Properties { get; } = [];
 
    /// <summary>
    /// Gets the generated package references.
    /// </summary>
    public List<CSharpPackageReference> PackageReferences { get; } = [];
 
    /// <summary>
    /// Gets the generated project references.
    /// </summary>
    public List<CSharpProjectReference> ProjectReferences { get; } = [];
 
    /// <summary>
    /// Gets the generated project imports.
    /// </summary>
    public List<CSharpProjectImport> Imports { get; } = [];
 
    /// <summary>
    /// Gets the generated none items.
    /// </summary>
    public List<CSharpNoneItem> NoneItems { get; } = [];
 
    /// <summary>
    /// Gets custom target elements to append to the generated project.
    /// </summary>
    public List<XElement> Targets { get; } = [];
 
    /// <summary>
    /// Adds a property to the generated project.
    /// </summary>
    public void AddProperty(string name, string value)
    {
        Properties.Add(new CSharpProjectProperty(name, value));
    }
 
    /// <summary>
    /// Adds integration references as package references or project references.
    /// </summary>
    public void AddIntegrationReferences(
        IEnumerable<IntegrationReference> integrationReferences,
        string? repoRoot,
        bool? isAspireProjectResource = null,
        bool? referenceOutputAssembly = null,
        bool? privateReference = null,
        ISet<string>? addedProjectPaths = null,
        string packageVersionAttributeName = CSharpPackageReference.DefaultVersionAttributeName)
    {
        var addedIntegrations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        foreach (var integrationReference in integrationReferences)
        {
            if (integrationReference.IsProjectReference)
            {
                if (addedIntegrations.Add(integrationReference.Name))
                {
                    AddProjectReference(
                        integrationReference.ProjectPath!,
                        isAspireProjectResource,
                        referenceOutputAssembly,
                        privateReference,
                        addedProjectPaths);
                }
 
                continue;
            }
 
            if (integrationReference.Name.StartsWith("Aspire.Hosting", StringComparison.OrdinalIgnoreCase) &&
                !integrationReference.DisableLocalProjectSubstitution)
            {
                if (TryGetRepositoryProject(repoRoot, integrationReference.Name, out var projectPath) &&
                    addedIntegrations.Add(integrationReference.Name))
                {
                    AddProjectReference(
                        projectPath,
                        isAspireProjectResource,
                        referenceOutputAssembly,
                        privateReference,
                        addedProjectPaths);
                }
 
                continue;
            }
 
            if (integrationReference.Version is null)
            {
                throw new InvalidOperationException($"Integration '{integrationReference.Name}' is neither a project reference nor a package reference (both Version and ProjectPath are null).");
            }
 
            PackageReferences.Add(new CSharpPackageReference(integrationReference.Name, integrationReference.Version, packageVersionAttributeName));
        }
    }
 
    /// <summary>
    /// Adds a reference to a repository-local project when it exists.
    /// </summary>
    public bool AddRepositoryProjectReferenceIfExists(
        string? repoRoot,
        string projectName,
        bool? isAspireProjectResource = null,
        bool? referenceOutputAssembly = null,
        bool? privateReference = null,
        ISet<string>? addedProjectPaths = null)
    {
        if (!TryGetRepositoryProject(repoRoot, projectName, out var projectPath) ||
            (addedProjectPaths is not null && !addedProjectPaths.Add(projectPath)))
        {
            return false;
        }
 
        AddProjectReference(
            projectPath,
            isAspireProjectResource,
            referenceOutputAssembly,
            privateReference);
 
        return true;
    }
 
    /// <summary>
    /// Finds a repository-local project under the Aspire source tree.
    /// </summary>
    public static bool TryGetRepositoryProject(string? repoRoot, string projectName, out string projectPath)
    {
        projectPath = null!;
        if (repoRoot is null)
        {
            return false;
        }
 
        var candidatePath = Path.Combine(repoRoot, "src", projectName, $"{projectName}.csproj");
        if (!File.Exists(candidatePath))
        {
            return false;
        }
 
        projectPath = candidatePath;
        return true;
    }
 
    private void AddProjectReference(
        string projectPath,
        bool? isAspireProjectResource,
        bool? referenceOutputAssembly,
        bool? privateReference,
        ISet<string>? addedProjectPaths = null)
    {
        if (addedProjectPaths is not null && !addedProjectPaths.Add(projectPath))
        {
            return;
        }
 
        ProjectReferences.Add(new CSharpProjectReference(
            projectPath,
            isAspireProjectResource,
            referenceOutputAssembly,
            privateReference));
    }
 
    /// <summary>
    /// Converts the project model into an XML document.
    /// </summary>
    public XDocument ToXDocument()
    {
        var root = new XElement("Project", new XAttribute("Sdk", Sdk));
 
        if (Properties.Count > 0)
        {
            root.Add(new XElement("PropertyGroup",
                Properties.Select(property => new XElement(property.Name, property.Value))));
        }
 
        if (PackageReferences.Count > 0)
        {
            root.Add(new XElement("ItemGroup",
                PackageReferences.Select(CreatePackageReferenceElement)));
        }
 
        if (ProjectReferences.Count > 0)
        {
            root.Add(new XElement("ItemGroup",
                ProjectReferences.Select(CreateProjectReferenceElement)));
        }
 
        if (NoneItems.Count > 0)
        {
            root.Add(new XElement("ItemGroup",
                NoneItems.Select(CreateNoneElement)));
        }
 
        root.Add(Imports.Select(import => new XElement("Import", new XAttribute("Project", import.ProjectPath))));
        root.Add(Targets);
 
        return new XDocument(root);
    }
 
    /// <summary>
    /// Creates a PackageReference XML element.
    /// </summary>
    internal static XElement CreatePackageReferenceElement(CSharpPackageReference packageReference)
    {
        var element = new XElement("PackageReference", new XAttribute("Include", packageReference.Name));
 
        if (packageReference.Version is not null)
        {
            element.Add(new XAttribute(packageReference.VersionAttributeName, packageReference.Version));
        }
 
        return element;
    }
 
    /// <summary>
    /// Creates a ProjectReference XML element.
    /// </summary>
    internal static XElement CreateProjectReferenceElement(CSharpProjectReference projectReference)
    {
        var element = new XElement("ProjectReference", new XAttribute("Include", projectReference.ProjectPath));
 
        AddBooleanElement(element, "IsAspireProjectResource", projectReference.IsAspireProjectResource);
        AddBooleanElement(element, "ReferenceOutputAssembly", projectReference.ReferenceOutputAssembly);
        AddBooleanElement(element, "Private", projectReference.Private);
 
        return element;
    }
 
    private static XElement CreateNoneElement(CSharpNoneItem noneItem)
    {
        var element = new XElement("None", new XAttribute("Update", noneItem.Update));
 
        if (noneItem.CopyToOutputDirectory is not null)
        {
            element.Add(new XAttribute("CopyToOutputDirectory", noneItem.CopyToOutputDirectory));
        }
 
        return element;
    }
 
    /// <summary>
    /// Adds a boolean child element when the value is specified.
    /// </summary>
    internal static void AddBooleanElement(XElement element, string name, bool? value)
    {
        if (value is not null)
        {
            element.Add(new XElement(name, value.Value ? "true" : "false"));
        }
    }
}
 
/// <summary>
/// Represents a generated project property.
/// </summary>
internal sealed record CSharpProjectProperty(string Name, string Value);
 
/// <summary>
/// Represents a generated package reference.
/// </summary>
internal sealed record CSharpPackageReference(string Name, string? Version = null, string VersionAttributeName = CSharpPackageReference.DefaultVersionAttributeName)
{
    public const string DefaultVersionAttributeName = "Version";
    public const string VersionOverrideAttributeName = "VersionOverride";
}
 
/// <summary>
/// Represents a generated project reference.
/// </summary>
internal sealed record CSharpProjectReference(
    string ProjectPath,
    bool? IsAspireProjectResource = null,
    bool? ReferenceOutputAssembly = null,
    bool? Private = null);
 
/// <summary>
/// Represents a generated project import.
/// </summary>
internal sealed record CSharpProjectImport(string ProjectPath);
 
/// <summary>
/// Represents a generated None item.
/// </summary>
internal sealed record CSharpNoneItem(string Update, string? CopyToOutputDirectory = null);