File: Commands\Run\Api\RunApiCommand.cs
Web Access
Project: ..\..\..\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.Frozen;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.CommandLine;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.DotNet.Cli.Utils;
 
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 sourceFile = SourceFile.Load(EntryPointFileFullPath);
            var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: true, DiagnosticBag.Collect(out var diagnostics));
            string artifactsPath = ArtifactsPath ?? VirtualProjectBuildingCommand.GetArtifactsPath(EntryPointFileFullPath);
 
            var csprojWriter = new StringWriter();
            VirtualProjectBuildingCommand.WriteProjectFile(csprojWriter, directives, isVirtualProject: true, targetFilePath: EntryPointFileFullPath, artifactsPath: artifactsPath);
 
            return new RunApiOutput.Project
            {
                Content = csprojWriter.ToString(),
                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: EntryPointFileFullPath,
                msbuildArgs: msbuildArgs)
            {
                CustomArtifactsPath = ArtifactsPath,
            };
            buildCommand.MarkArtifactsFolderUsed();
 
            var runCommand = new RunCommand(
                noBuild: false,
                projectFileFullPath: null,
                entryPointFileFullPath: EntryPointFileFullPath,
                launchProfile: null,
                noLaunchProfile: false,
                noLaunchProfileArguments: false,
                noRestore: false,
                noCache: false,
                interactive: false,
                msbuildArgs: msbuildArgs,
                applicationArgs: [],
                readCodeFromStdin: false,
                environmentVariables: ReadOnlyDictionary<string, string>.Empty,
                msbuildRestoreProperties: ReadOnlyDictionary<string, string>.Empty);
 
            runCommand.TryGetLaunchProfileSettingsIfNeeded(out var launchSettings);
            var targetCommand = (Utils.Command)runCommand.GetTargetCommand(buildCommand.CreateProjectInstance, cachedRunProperties: null);
            runCommand.ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);
 
            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 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;