File: Commands\Run\Api\RunApiCommand.cs
Web Access
Project: src\src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.CommandLine;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.FileBasedPrograms;
using Microsoft.DotNet.ProjectTools;

namespace Microsoft.DotNet.Cli.Commands.Run.Api;

/// <summary>
/// Takes JSON from stdin lines, produces JSON on stdout lines, doesn't perform any changes.
/// Can be used by IDEs to see the project file behind a file-based program.
/// </summary>
internal sealed class RunApiCommand(ParseResult parseResult) : CommandBase(parseResult)
{
    public override int Execute()
    {
        for (string? line; (line = Console.ReadLine()) != null;)
        {
            if (string.IsNullOrWhiteSpace(line))
            {
                continue;
            }

            try
            {
                RunApiInput input = JsonSerializer.Deserialize(line, RunFileApiJsonSerializerContext.Default.RunApiInput)!;
                RunApiOutput output = input.Execute();
                Respond(output);
            }
            catch (Exception ex)
            {
                Respond(new RunApiOutput.Error { Message = ex.Message, Details = ex.ToString() });
            }
        }

        return 0;

        static void Respond(RunApiOutput message)
        {
            string json = JsonSerializer.Serialize(message, RunFileApiJsonSerializerContext.Default.RunApiOutput);
            Console.WriteLine(json);
        }
    }
}

[JsonDerivedType(typeof(GetProject), nameof(GetProject))]
[JsonDerivedType(typeof(GetRunCommand), nameof(GetRunCommand))]
internal abstract class RunApiInput
{
    private RunApiInput() { }

    public abstract RunApiOutput Execute();

    public sealed class GetProject : RunApiInput
    {
        public string? ArtifactsPath { get; init; }
        public required string EntryPointFileFullPath { get; init; }

        public override RunApiOutput Execute()
        {
            var builder = new VirtualProjectBuilder(
                entryPointFileFullPath: EntryPointFileFullPath,
                targetFramework: VirtualProjectBuildingCommand.TargetFramework,
                artifactsPath: ArtifactsPath);

            var errorReporter = ErrorReporters.CreateCollectingReporter(out var diagnostics);

            builder.CreateProjectInstance(
                new ProjectCollection(),
                errorReporter,
                project: out _,
                out var projectRootElement,
                out var evaluatedDirectives,
                validateAllDirectives: true);

            var csprojWriter = new StringWriter();
            VirtualProjectBuilder.WriteProjectFile(
                csprojWriter,
                evaluatedDirectives,
                VirtualProjectBuilder.GetDefaultProperties(VirtualProjectBuildingCommand.TargetFramework),
                isVirtualProject: true,
                entryPointFilePath: EntryPointFileFullPath,
                artifactsPath: builder.ArtifactsPath);

            return new RunApiOutput.Project
            {
                Content = csprojWriter.ToString(),
                ProjectPath = projectRootElement.FullPath,
                Diagnostics = diagnostics.ToImmutableArray(),
            };
        }
    }

    public sealed class GetRunCommand : RunApiInput
    {
        public string? ArtifactsPath { get; init; }
        public required string EntryPointFileFullPath { get; init; }

        public override RunApiOutput Execute()
        {
            var msbuildArgs = MSBuildArgs.FromVerbosity(VerbosityOptions.quiet);
            var buildCommand = new VirtualProjectBuildingCommand(
                EntryPointFileFullPath, msbuildArgs, artifactsPath: ArtifactsPath);

            buildCommand.MarkArtifactsFolderUsed();

            var runCommand = new RunCommand(
                noBuild: false,
                projectFileFullPath: null,
                entryPointFileFullPath: EntryPointFileFullPath,
                launchProfile: null,
                noLaunchProfile: false,
                noLaunchProfileArguments: false,
                device: null,
                listDevices: false,
                noRestore: false,
                noCache: false,
                interactive: false,
                msbuildArgs: msbuildArgs,
                applicationArgs: [],
                readCodeFromStdin: false,
                environmentVariables: ReadOnlyDictionary<string, string>.Empty);

            var result = runCommand.ReadLaunchProfileSettings();
            var targetCommand = (Utils.Command)runCommand.GetTargetCommand(result.Profile, buildCommand.CreateProjectInstance, cachedRunProperties: null, logger: null);

            return new RunApiOutput.RunCommand
            {
                ExecutablePath = targetCommand.CommandName,
                CommandLineArguments = targetCommand.CommandArgs,
                WorkingDirectory = targetCommand.StartInfo.WorkingDirectory,
                EnvironmentVariables = targetCommand.CustomEnvironmentVariables ?? ReadOnlyDictionary<string, string?>.Empty,
            };
        }
    }
}

[JsonDerivedType(typeof(Error), nameof(Error))]
[JsonDerivedType(typeof(Project), nameof(Project))]
[JsonDerivedType(typeof(RunCommand), nameof(RunCommand))]
internal abstract class RunApiOutput
{
    private RunApiOutput() { }

    /// <summary>
    /// When the API shape or behavior changes, this should be incremented so the callers (IDEs) can act accordingly
    /// (e.g., show an error message when an incompatible SDK version is being used).
    /// </summary>
    [JsonPropertyOrder(-1)]
    public int Version { get; } = 1;

    public sealed class Error : RunApiOutput
    {
        public required string Message { get; init; }
        public required string Details { get; init; }
    }

    public sealed class Project : RunApiOutput
    {
        public required string Content { get; init; }
        public required string ProjectPath { get; init; }
        public required ImmutableArray<SimpleDiagnostic> Diagnostics { get; init; }
    }

    public sealed class RunCommand : RunApiOutput
    {
        public required string ExecutablePath { get; init; }
        public required string CommandLineArguments { get; init; }
        public required string? WorkingDirectory { get; init; }
        public required IReadOnlyDictionary<string, string?> EnvironmentVariables { get; init; }
    }
}

[JsonSerializable(typeof(RunApiInput))]
[JsonSerializable(typeof(RunApiOutput))]
internal partial class RunFileApiJsonSerializerContext : JsonSerializerContext;