// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREEXTENSION001
#pragma warning disable ASPIREPERSISTENCE001
#pragma warning disable ASPIREPIPELINES001
#pragma warning disable ASPIREPIPELINES003
#pragma warning disable ASPIREPROJECTS001
#pragma warning disable ASPIRECONTAINERRUNTIME001
#pragma warning disable ASPIRECSHARPAPPS001
using System.Globalization;
using System.Reflection;
using System.Text.Json;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Dcp.Model;
using Aspire.Hosting.Dcp.Process;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Resources;
using Aspire.Hosting.Tests.Publishing;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Aspire.TestUtilities;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Aspire.Hosting.Dotnet.Tests;
public class DotnetProjectResourceTests(ITestOutputHelper outputHelper)
{
[Fact]
public async Task AddDotnetProject_ProjectFile_ProducesDotnetRunProjectArgs()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true);
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
var expected = new List<string> { "run", "--project", projectPath, "--no-build" };
AddExpectedConfiguration(builder, expected);
expected.Add("--no-launch-profile");
Assert.Equal(expected, args);
}
[Fact]
public async Task AddDotnetProject_FileBasedApp_ProducesDotnetRunFileArgs()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var appPath = Path.Combine(builder.AppHostDirectory, "service.cs");
var app = builder.AddDotnetProject("svc", appPath, o => o.ExcludeLaunchProfile = true);
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
var expected = new List<string> { "run", "--file", appPath, "--no-build" };
AddExpectedConfiguration(builder, expected);
expected.Add("--no-launch-profile");
Assert.Equal(expected, args);
}
[Fact]
public void AddDotnetProject_UsesDotnetCommandAndProjectDirectoryAsWorkingDirectory()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true);
Assert.Equal("dotnet", app.Resource.Command);
Assert.Equal(Path.GetDirectoryName(projectPath), app.Resource.WorkingDirectory);
}
[Fact]
public void AddDotnetProject_ResourceSupportsServiceDiscoveryAndIsComputeResource()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var app = builder.AddDotnetProject("svc", "MyService.csproj", o => o.ExcludeLaunchProfile = true);
Assert.IsAssignableFrom<IResourceWithServiceDiscovery>(app.Resource);
Assert.IsAssignableFrom<ExecutableResource>(app.Resource);
Assert.IsAssignableFrom<IComputeResource>(app.Resource);
}
[Theory]
[InlineData(1, false, false)]
[InlineData(3, false, false)]
[InlineData(3, true, false)]
[InlineData(1, false, true)]
[InlineData(3, false, true)]
[InlineData(3, true, true)]
public void AddDotnetProject_ReplicasRenderEveryDcpInstance(int replicas, bool polyglot, bool fileBased)
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var resource = builder.AddDotnetProject("svc", fileBased ? "service.cs" : "Service.csproj", options => options.ExcludeLaunchProfile = true)
.WithExplicitStart();
if (polyglot)
{
resource.WithReplicasForPolyglot(replicas);
}
else
{
resource.WithReplicas(replicas);
}
using var app = builder.Build();
var rendered = app.Services.GetRequiredService<ExecutableCreator>()
.PrepareObjects(TestContext.Current.CancellationToken)
.Where(item => ReferenceEquals(item.ModelResource, resource.Resource))
.Select(item => item.DcpResource)
.OrderBy(static executable => int.Parse(executable.Metadata.Annotations[CustomResource.ResourceReplicaIndex], CultureInfo.InvariantCulture))
.ToArray();
Assert.Equal(replicas, rendered.Length);
Assert.True(resource.Resource.TryGetInstances(out var instances));
Assert.Equal(instances.Select(instance => instance.Name), rendered.Select(executable => executable.Metadata.Name));
Assert.Equal(replicas, rendered.Select(executable => executable.Metadata.Annotations[CustomResource.OtelServiceInstanceIdAnnotation]).Distinct().Count());
for (var index = 0; index < replicas; index++)
{
var executable = rendered[index];
Assert.Equal(index.ToString(CultureInfo.InvariantCulture), executable.Metadata.Annotations[CustomResource.ResourceReplicaIndex]);
Assert.Equal(replicas.ToString(CultureInfo.InvariantCulture), executable.Metadata.Annotations[CustomResource.ResourceReplicaCount]);
Assert.Equal("svc", executable.Metadata.Annotations[CustomResource.OtelServiceNameAnnotation]);
Assert.Equal(resource.Resource.Command, executable.Spec.ExecutablePath);
Assert.Equal(resource.Resource.WorkingDirectory, executable.Spec.WorkingDirectory);
Assert.False(executable.Spec.Start);
}
}
[Fact]
public void AddDotnetProject_PersistentReplicasRemainUnsupported()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.AddDotnetProject("svc", "Service.csproj", options => options.ExcludeLaunchProfile = true)
.WithReplicas(2)
.WithPersistentLifetime();
using var app = builder.Build();
var exception = Assert.Throws<InvalidOperationException>(() =>
app.Services.GetRequiredService<ExecutableCreator>().PrepareObjects(TestContext.Current.CancellationToken).ToArray());
Assert.Equal("Resource 'svc' uses multiple replicas and a persistent lifetime. These features do not work together.", exception.Message);
}
[Fact]
public void AddDotnetProject_AddsProjectMetadataAnnotation()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true);
Assert.True(app.Resource.TryGetLastAnnotation<IProjectMetadata>(out var metadata));
Assert.Equal(projectPath, metadata.ProjectPath);
}
[Theory]
[InlineData("project")]
[InlineData("directory")]
[InlineData("file")]
public async Task AddDotnetProject_InPublishMode_ProducesProjectManifest(string appKind)
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var appPath = appKind switch
{
"project" => CreateFile(workspace.Path, "MyService.csproj"),
"directory" => CreateProjectDirectory(workspace.Path),
"file" => CreateFile(workspace.Path, "service.cs"),
_ => throw new ArgumentOutOfRangeException(nameof(appKind), appKind, null)
};
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var app = builder.AddDotnetProject("svc", appPath, o => o.ExcludeLaunchProfile = true);
var manifest = await ManifestUtils.GetManifest(app.Resource, workspace.Path);
var metadata = app.Resource.GetProjectMetadata();
var expectedPath = Path.GetRelativePath(workspace.Path, metadata.ProjectPath).Replace('\\', '/');
Assert.Equal("project.v0", manifest["type"]?.GetValue<string>());
Assert.Equal(expectedPath, manifest["path"]?.GetValue<string>());
Assert.Null(manifest["args"]);
Assert.True(app.Resource.SupportsDotnetProgramPublishing());
Assert.IsAssignableFrom<IDotnetProgramResource>(app.Resource);
Assert.IsAssignableFrom<IContainerFilesDestinationResource>(app.Resource);
}
[Fact]
public void AddDotnetProject_InPublishMode_ConfiguresSdkPublishingWithoutCoordinatedBuild()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectPath = CreateFile(workspace.Path, "MyService.csproj");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var resource = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true);
var metadata = resource.Resource.GetProjectMetadata();
Assert.True(resource.Resource.SupportsDotnetProgramPublishing());
Assert.False(metadata.SuppressBuild);
Assert.DoesNotContain(builder.Resources, candidate => candidate is DotnetProjectBuildResource);
Assert.Single(resource.Resource.Annotations.OfType<PipelineStepAnnotation>());
Assert.Single(resource.Resource.Annotations.OfType<PipelineConfigurationAnnotation>());
Assert.Single(resource.Resource.Annotations.OfType<ContainerBuildOptionsCallbackAnnotation>());
}
[Fact]
public async Task DirectlyConstructedDotnetProjectResource_ManifestRequiresPublishingConfiguration()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var resource = new DotnetProjectResource("svc", workspace.Path);
var exception = await Assert.ThrowsAsync<DistributedApplicationException>(
() => ManifestUtils.GetManifest(resource, workspace.Path));
Assert.Equal(
"The .NET program resource 'svc' is not configured for publishing. " +
"Create it with a supported builder API or call WithDotnetProgramPublishing() after attaching project metadata.",
exception.Message);
}
[Fact]
public async Task AddDotnetProject_ExcludedFromManifest_DoesNotFailPublishing()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectPath = CreateFile(workspace.Path, "MyService.csproj");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var resource = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.ExcludeFromManifest();
using var app = builder.Build();
await ExecutePipelineAsync(app);
var manifest = await ManifestUtils.GetManifestOrNull(resource.Resource, workspace.Path);
Assert.Null(manifest);
}
[Fact]
public async Task AddDotnetProject_WithManifestPublishingCallback_ProducesCustomManifest()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectPath = CreateFile(workspace.Path, "MyService.csproj");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var resource = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithManifestPublishingCallback(context => context.Writer.WriteString("type", "custom.v0"));
using var app = builder.Build();
await ExecutePipelineAsync(app);
var manifest = await ManifestUtils.GetManifest(resource.Resource, workspace.Path);
Assert.Equal("""{"type":"custom.v0"}""", manifest.ToJsonString());
}
[Fact]
public async Task AddDotnetProject_PublishAsDockerFile_ProducesContainerManifest()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectPath = CreateFile(workspace.Path, "MyService.csproj");
await File.WriteAllTextAsync(Path.Combine(workspace.Path, "Dockerfile"), "FROM scratch");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var resource = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.PublishAsDockerFile();
using var app = builder.Build();
await ExecutePipelineAsync(app);
var manifest = await ManifestUtils.GetManifest(resource.Resource, workspace.Path);
var expected =
"""
{
"type": "container.v1",
"build": {
"context": ".",
"dockerfile": "Dockerfile"
},
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory"
}
}
""";
Assert.Equal(expected, manifest.ToString(), ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
[RequiresFeature(TestFeature.ContainerRuntime | TestFeature.ContainerImageBuild)]
public async Task FileBasedAppWithContainerFilesPreservesLocalImageAndBuildsArchive(bool legacyProject)
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var imageName = $"localhost/file-app-{Guid.NewGuid():N}";
const string ImageTag = "release";
var imageReference = $"{imageName}:{ImageTag}";
var sentinelReference = $"{imageName}:sentinel";
var marker = $"archive-marker-{Guid.NewGuid():N}";
var appPath = Path.Combine(workspace.Path, "app.cs");
await File.WriteAllTextAsync(appPath, """
#:property PublishAot=false
#:property TargetFramework=net10.0
Console.WriteLine(File.ReadAllText("assets"));
""");
var archivePath = Path.Combine(workspace.Path, "app.tar.gz");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var assets = builder.AddContainer("assets", imageName, ImageTag)
.WithAnnotation(new ContainerFilesSourceAnnotation { SourcePath = "/sentinel.txt" });
IResourceBuilder<IComputeResource> resource = legacyProject
? builder.AddCSharpApp("file-app", appPath, options => options.ExcludeLaunchProfile = true)
: builder.AddDotnetProject("file-app", appPath, options => options.ExcludeLaunchProfile = true);
resource
.WithAnnotation(new ContainerFilesDestinationAnnotation
{
Source = assets.Resource,
DestinationPath = "assets"
})
.WithContainerBuildOptions(context =>
{
context.Destination = ContainerImageDestination.Archive;
context.ImageFormat = ContainerImageFormat.Docker;
context.LocalImageName = imageName;
context.LocalImageTag = ImageTag;
context.OutputPath = archivePath;
context.TargetPlatform = ContainerTargetPlatform.LinuxAmd64;
});
using var app = builder.Build();
using var cancellation = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
cancellation.CancelAfter(TestConstants.LongTimeoutTimeSpan);
var runtime = await app.Services.GetRequiredService<IContainerRuntimeResolver>().ResolveAsync(cancellation.Token);
var logger = app.Services.GetRequiredService<ILogger<DotnetProjectResourceTests>>();
await using var finalImageCleanup = new TemporaryContainerImage(runtime, imageReference, logger);
var sentinelDockerfilePath = Path.Combine(workspace.Path, "Dockerfile.sentinel");
await File.WriteAllTextAsync(Path.Combine(workspace.Path, "sentinel.txt"), marker, cancellation.Token);
await File.WriteAllTextAsync(sentinelDockerfilePath, "FROM scratch\nCOPY sentinel.txt /sentinel.txt\n", cancellation.Token);
await runtime.BuildImageAsync(
workspace.Path,
sentinelDockerfilePath,
new ContainerImageBuildOptions
{
ImageName = imageName,
Tag = ImageTag,
TargetPlatform = ContainerTargetPlatform.LinuxAmd64,
RequiresLocalImageStore = true
},
[],
[],
null,
cancellation.Token);
var processRunner = app.Services.GetRequiredService<IProcessRunner>();
var originalImageId = await RunContainerRuntimeAsync(
processRunner, runtime, ["image", "inspect", "--format", "{{.Id}}", imageReference], cancellation.Token);
Assert.NotEmpty(originalImageId);
// Keep an owned alias so loading the archive cannot leave the sentinel image untagged.
await using var sentinelCleanup = new TemporaryContainerImage(runtime, sentinelReference, logger);
await runtime.TagImageAsync(imageReference, sentinelReference, cancellation.Token);
var imageBuilder = app.Services.GetRequiredService<IResourceContainerImageManager>();
await imageBuilder.BuildImageAsync(resource.Resource, cancellation.Token);
var preservedImageId = await RunContainerRuntimeAsync(
processRunner, runtime, ["image", "inspect", "--format", "{{.Id}}", imageReference], cancellation.Token);
Assert.Equal(originalImageId, preservedImageId);
Assert.Equal([imageReference], TestContainerImageArchive.ReadDockerImageReferences(archivePath));
await RunContainerRuntimeAsync(processRunner, runtime, ["image", "load", "--input", archivePath], cancellation.Token);
var loadedImageId = await RunContainerRuntimeAsync(
processRunner, runtime, ["image", "inspect", "--format", "{{.Id}}", imageReference], cancellation.Token);
Assert.NotEqual(originalImageId, loadedImageId);
var containerOutput = await RunContainerRuntimeAsync(
processRunner, runtime, ["run", "--rm", "--platform", "linux/amd64", imageReference], cancellation.Token);
Assert.Equal(marker, containerOutput);
}
[Fact]
[RequiresFeature(TestFeature.ContainerImageBuild)]
public async Task AddDotnetProject_ProjectBuildEnvironmentFlowsToSdkPublish()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "project"));
var projectPath = Path.Combine(projectDirectory.FullName, "Project.csproj");
await File.WriteAllTextAsync(projectPath, """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<Target Name="ValidateBuildFlavor" BeforeTargets="Publish">
<Error Condition="'$(BUILD_FLAVOR)' != 'custom'" Text="BUILD_FLAVOR was not provided." />
</Target>
</Project>
""");
await File.WriteAllTextAsync(
Path.Combine(projectDirectory.FullName, "Program.cs"),
"""System.Console.WriteLine("project");""");
var archivePath = Path.Combine(workspace.Path, "project.tar.gz");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var resource = builder.AddDotnetProject("project", projectPath, options => options.ExcludeLaunchProfile = true)
.WithBuildEnvironment("BUILD_FLAVOR", "custom")
.WithContainerBuildOptions(context =>
{
context.Destination = ContainerImageDestination.Archive;
context.ImageFormat = ContainerImageFormat.Docker;
context.OutputPath = archivePath;
context.TargetPlatform = ContainerTargetPlatform.LinuxAmd64;
});
using var app = builder.Build();
var imageBuilder = app.Services.GetRequiredService<IResourceContainerImageManager>();
await imageBuilder.BuildImageAsync(resource.Resource, TestContext.Current.CancellationToken);
Assert.True(File.Exists(archivePath));
Assert.Empty(resource.Resource.GetProjectMetadata().BuildEnvironment);
Assert.Null(resource.Resource.GetProjectMetadata().BuildWorkingDirectory);
}
[Fact]
public async Task AddDotnetProject_ProjectBuildEnvironmentRejectsContainerArtifactPropertyCaseInsensitively()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
builder.Services.AddFakeContainerRuntime(new FakeContainerRuntime());
var projectPath = Path.Combine(workspace.Path, "Project.csproj");
var resource = builder.AddDotnetProject("project", projectPath, options => options.ExcludeLaunchProfile = true)
.WithBuildEnvironment("containerrepository", "override")
.WithContainerBuildOptions(context =>
{
context.Destination = ContainerImageDestination.Archive;
context.ImageFormat = ContainerImageFormat.Oci;
context.OutputPath = Path.Combine(workspace.Path, "project.tar");
});
using var app = builder.Build();
var imageBuilder = app.Services.GetRequiredService<IResourceContainerImageManager>();
var exception = await Assert.ThrowsAsync<DistributedApplicationException>(
() => imageBuilder.BuildImageAsync(resource.Resource, TestContext.Current.CancellationToken));
Assert.Equal(
"The build environment property 'containerrepository' for .NET program resource 'project' " +
"is reserved by Aspire container publishing because it controls the image artifact used by downstream steps. " +
"Configure container publishing with WithContainerBuildOptions instead.",
exception.Message);
}
[Fact]
public void AddDotnetProject_PublishAsDockerFile_UsesProjectContextAndProjectPortDefaults()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "project"));
var projectPath = CreateFile(projectDirectory.FullName, "MyService.csproj");
var runtimeDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "runtime"));
File.WriteAllText(Path.Combine(projectDirectory.FullName, "Dockerfile"), "FROM scratch");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithWorkingDirectory(runtimeDirectory.FullName)
.WithHttpEndpoint()
.PublishAsDockerFile();
var container = Assert.Single(builder.Resources.OfType<ContainerResource>());
var dockerfile = Assert.Single(container.Annotations.OfType<DockerfileBuildAnnotation>());
var endpoint = Assert.Single(container.Annotations.OfType<EndpointAnnotation>());
Assert.Equal(projectDirectory.FullName, dockerfile.ContextPath);
Assert.Equal(8080, endpoint.TargetPort);
}
[Fact]
public void AddDotnetProject_DirectoryContainingSingleProjectFile_ResolvesToThatProjectFile()
{
// DotnetProjectMetadata defers path resolution to ProjectPathResolver, which resolves a directory
// containing exactly one .csproj to that project file. Verify AddDotnetProject preserves that
// contract end-to-end (it used to be exercised only through the core AddProject<T> path).
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectDir = Directory.CreateDirectory(Path.Combine(workspace.Path, "MyService"));
var projectPath = Path.Combine(projectDir.FullName, "MyService.csproj");
File.WriteAllText(projectPath, "<Project />");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var app = builder.AddDotnetProject("svc", projectDir.FullName, o => o.ExcludeLaunchProfile = true);
Assert.True(app.Resource.TryGetLastAnnotation<IProjectMetadata>(out var metadata));
Assert.Equal(projectPath, metadata.ProjectPath);
}
[Fact]
public void AddDotnetProject_AmbiguousDirectory_PassesPathThroughUnchanged()
{
// When a directory contains zero or multiple .csproj files, ProjectPathResolver deliberately passes
// the directory path through unchanged rather than throwing, so the failure surfaces later as a
// resource start error naming the resource.
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectDir = Directory.CreateDirectory(Path.Combine(workspace.Path, "AmbiguousService"));
File.WriteAllText(Path.Combine(projectDir.FullName, "First.csproj"), "<Project />");
File.WriteAllText(Path.Combine(projectDir.FullName, "Second.csproj"), "<Project />");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var app = builder.AddDotnetProject("svc", projectDir.FullName, o => o.ExcludeLaunchProfile = true);
Assert.True(app.Resource.TryGetLastAnnotation<IProjectMetadata>(out var metadata));
Assert.Equal(projectDir.FullName, metadata.ProjectPath);
}
[Fact]
public void AddDotnetProject_AddsSupportsDebuggingAnnotationInRunMode()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var app = builder.AddDotnetProject("appName", "app-path", options => { options.ExcludeLaunchProfile = true; });
var annotation = app.Resource.Annotations.OfType<SupportsDebuggingAnnotation>().SingleOrDefault();
Assert.NotNull(annotation);
Assert.Equal("project", annotation.LaunchConfigurationType);
}
[Theory]
[InlineData("MyService.csproj")]
[InlineData("service.cs")]
public async Task AddDotnetProject_RebuilderUsesConfiguredBuildConfiguration(string projectFileName)
{
using var builder = TestDistributedApplicationBuilder.Create();
var versionProvider = UseDotnetSdkVersion(builder, "11.0.100-rc.2.1");
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", projectFileName);
var project = builder.AddDotnetProject("svc", projectPath, options => options.ExcludeLaunchProfile = true);
var launchDefaults = Assert.Single(project.Resource.Annotations.OfType<ProjectLaunchDefaultsAnnotation>());
launchDefaults.BuildConfiguration = "Release";
var rebuilder = Assert.Single(builder.Resources.OfType<ProjectRebuilderResource>());
await using var app = builder.Build();
await EventingTestHelpers.SubscribeEventingSubscribersAsync(
app,
TestContext.Current.CancellationToken);
await builder.Eventing.PublishAsync(
new BeforeResourceStartedEvent(rebuilder, app.Services),
TestContext.Current.CancellationToken);
var args = await ArgumentEvaluator.GetArgumentListAsync(rebuilder, app.Services);
var expected = new List<string>
{
"build",
projectPath,
};
var isProjectBuild = projectFileName.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase);
if (isProjectBuild)
{
expected.Add("-mt");
}
expected.Add("--configuration");
expected.Add("Release");
Assert.Equal(expected, args);
Assert.Equal(1, versionProvider.CallCount);
Assert.Equal(rebuilder.WorkingDirectory, Assert.Single(versionProvider.WorkingDirectories));
}
[Fact]
public async Task AddDotnetProject_MaterializesEndpointsFromLaunchProfile()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectDir = Directory.CreateDirectory(Path.Combine(workspace.Path, "MyService"));
var projectPath = Path.Combine(projectDir.FullName, "MyService.csproj");
await File.WriteAllTextAsync(projectPath, "<Project />");
var propertiesDir = Directory.CreateDirectory(Path.Combine(projectDir.FullName, "Properties"));
await File.WriteAllTextAsync(Path.Combine(propertiesDir.FullName, "launchSettings.json"), """
{
"profiles": {
"http": {
"commandName": "Project",
"applicationUrl": "http://localhost:5111"
}
}
}
""");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var app = builder.AddDotnetProject("svc", projectPath);
var endpoint = Assert.Single(app.Resource.Annotations.OfType<EndpointAnnotation>());
Assert.Equal("http", endpoint.UriScheme);
Assert.Equal(5111, endpoint.Port);
}
[Fact]
public void AddLifeCycleCommands_DotnetProjectResource_RestartHasDetailedProjectDescription()
{
// A DotnetProjectResource is a .NET app launched via the SDK, so it should receive the same
// detailed "rebuild is required" restart description that ProjectResource gets. The marker for
// that is the project-defaults annotation applied by AddDotnetProject.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var resource = builder.AddDotnetProject("testapp", projectPath, o => o.ExcludeLaunchProfile = true).Resource;
resource.AddLifeCycleCommands();
var restartCommand = resource.Annotations.OfType<ResourceCommandAnnotation>().Single(a => a.Name == KnownResourceCommands.RestartCommand);
Assert.Equal(CommandStrings.RestartProjectDescription, restartCommand.DisplayDescription);
}
[Fact]
public void AddLifeCycleCommands_FileBasedApp_AddsRebuildCommand()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var appPath = Path.Combine(builder.AppHostDirectory, "service.cs");
var resource = builder.AddDotnetProject("testapp", appPath, o => o.ExcludeLaunchProfile = true).Resource;
resource.AddLifeCycleCommands();
Assert.Contains(
resource.Annotations.OfType<ResourceCommandAnnotation>(),
annotation => annotation.Name == KnownResourceCommands.RebuildCommand);
}
[Fact]
public void AddLifeCycleCommands_DirectlyConstructedDotnetProjectResource_RestartHasDetailedProjectDescription()
{
// The type has a public constructor, so it can be added with AddResource instead of
// AddDotnetProject. It is still a .NET app launched via the SDK, so the constructor carries the
// project-defaults annotation and the resource gets the same treatment as ProjectResource.
var resource = new DotnetProjectResource("testapp", AppContext.BaseDirectory);
resource.AddLifeCycleCommands();
var restartCommand = resource.Annotations.OfType<ResourceCommandAnnotation>().Single(a => a.Name == KnownResourceCommands.RestartCommand);
Assert.Equal(CommandStrings.RestartProjectDescription, restartCommand.DisplayDescription);
Assert.Contains(resource.Annotations.OfType<ResourceCommandAnnotation>(), a => a.Name == KnownResourceCommands.RebuildCommand);
}
[Fact]
public async Task AddDotnetProject_DebugAnnotator_ProducesProjectLaunchConfiguration()
{
// The versioned external-build capability prevents an older IDE from accepting the resource while ignoring
// coordinated-build metadata. The producer still resolves the full project launch configuration.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true);
Assert.True(app.Resource.TryGetLastAnnotation<SupportsDebuggingAnnotation>(out var supportsDebugging));
Assert.Equal(KnownLaunchConfigurationTypes.ProjectWithExternalBuild, supportsDebugging.LaunchConfigurationType);
var callbackContext = LaunchConfigurationTestHelpers.CreateCallbackContext(
app.Resource,
ExecutableLaunchMode.Debug);
var launchConfig = Assert.IsType<ProjectLaunchConfiguration>(
await app.Resource.CreateLaunchConfigurationAsync(callbackContext));
Assert.Equal(KnownLaunchConfigurationTypes.ProjectWithExternalBuild, launchConfig.Type);
Assert.Equal(ExecutableLaunchMode.Debug, launchConfig.Mode);
Assert.Equal(projectPath, launchConfig.ProjectPath);
Assert.True(launchConfig.DisableLaunchProfile);
Assert.Equal(string.Empty, launchConfig.LaunchProfile);
Assert.Equal(
builder.AppHostAssembly?.GetCustomAttribute<AssemblyConfigurationAttribute>()?.Configuration,
launchConfig.BuildConfiguration);
Assert.True(launchConfig.SuppressBuild);
}
[Fact]
public async Task AddDotnetProject_LaunchConfiguration_ResolvesEffectiveLaunchProfile()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectDir = Directory.CreateDirectory(Path.Combine(workspace.Path, "MyService"));
var projectPath = Path.Combine(projectDir.FullName, "MyService.csproj");
await File.WriteAllTextAsync(projectPath, "<Project />");
var propertiesDir = Directory.CreateDirectory(Path.Combine(projectDir.FullName, "Properties"));
await File.WriteAllTextAsync(Path.Combine(propertiesDir.FullName, "launchSettings.json"), """
{
"profiles": {
"http": {
"commandName": "Project",
"applicationUrl": "http://localhost:5111"
}
}
}
""");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var app = builder.AddDotnetProject("svc", projectPath);
var callbackContext = LaunchConfigurationTestHelpers.CreateCallbackContext(
app.Resource,
ExecutableLaunchMode.Debug);
var launchConfig = Assert.IsType<ProjectLaunchConfiguration>(
await app.Resource.CreateLaunchConfigurationAsync(callbackContext));
Assert.False(launchConfig.DisableLaunchProfile);
Assert.Equal("http", launchConfig.LaunchProfile);
}
[Fact]
public async Task AddDotnetProject_CustomLaunchToolArgs_ReplaceDotnetRunScaffoldingInRunMode()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml")
.WithLaunchToolArgs(AddCustomLaunchToolArgs, ownedByLaunchConfigurationType: "custom")
.WithDebugSupport(_ => new ExecutableLaunchConfiguration("custom"), "custom");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
Assert.Equal(["tool", "exec", "package", "--yes", "--", "--config", "prod.yaml"], args);
}
[Fact]
public async Task AddDotnetProject_CustomLaunchToolArgs_ReplaceDotnetRunScaffoldingInPublishMode()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml")
.WithLaunchToolArgs(AddCustomLaunchToolArgs, ownedByLaunchConfigurationType: "custom")
.WithDebugSupport(_ => new ExecutableLaunchConfiguration("custom"), "custom");
Assert.Empty(app.Resource.Annotations.OfType<SupportsDebuggingAnnotation>());
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
Assert.Equal(["tool", "exec", "package", "--yes", "--", "--config", "prod.yaml"], args);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task AddDotnetProject_CustomLaunchToolArgs_PreserveLaunchProfileArguments(bool inDebugSession)
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var projectDir = Directory.CreateDirectory(Path.Combine(workspace.Path, "MyService"));
var projectPath = Path.Combine(projectDir.FullName, "MyService.csproj");
await File.WriteAllTextAsync(projectPath, "<Project />");
var propertiesDir = Directory.CreateDirectory(Path.Combine(projectDir.FullName, "Properties"));
await File.WriteAllTextAsync(Path.Combine(propertiesDir.FullName, "launchSettings.json"), """
{
"profiles": {
"http": {
"commandName": "Project",
"commandLineArgs": "--profile-arg \"profile value\""
}
}
}
""");
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
if (inDebugSession)
{
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = ["custom"]
});
}
var app = builder.AddDotnetProject("svc", projectPath)
.WithArgs("--config", "prod.yaml")
.WithLaunchToolArgs(AddCustomLaunchToolArgs, ownedByLaunchConfigurationType: "custom")
.WithDebugSupport(_ => new ExecutableLaunchConfiguration("custom"), "custom");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
Assert.Equal(
["tool", "exec", "package", "--yes", "--", "--profile-arg", "profile value", "--config", "prod.yaml"],
args);
}
[Fact]
public async Task AddDotnetProject_InDebugSession_OmitsDotnetRunScaffoldingWhenExternalBuildIsSupported()
{
// The versioned capability guarantees that the IDE honors coordinated-build values and suppression.
// Emitting `dotnet run …` as well would hand those tool arguments to the debugged program.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = [KnownLaunchConfigurationTypes.ProjectWithExternalBuild]
});
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
Assert.Collection(args,
arg => Assert.Equal("--config", arg),
arg => Assert.Equal("prod.yaml", arg));
}
[Fact]
public async Task AddDotnetProject_InDebugSession_OmitsDotnetRunScaffoldingWhenLegacyProjectLaunchIsSupported()
{
// An IDE that supports only ordinary project launch owns both building and launching the resource. The
// coordinated-build path is disabled for this session, so only the user's program arguments are emitted.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = [KnownLaunchConfigurationTypes.Project]
});
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
Assert.Equal(["--config", "prod.yaml"], args);
}
[Fact]
public async Task AddDotnetProject_FileBasedApp_InCapabilitylessDebugSession_KeepsDotnetRunFileArgs()
{
// Visual Studio does not advertise launch capabilities and cannot launch a bare .cs file as a loaded project.
// Keep the complete process invocation instead of handing the resource to the IDE.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
var appPath = Path.Combine(builder.AppHostDirectory, "service.cs");
var app = builder.AddDotnetProject("svc", appPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--flag");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
List<string> expectedArgs =
[
"run",
"--file",
appPath,
"--no-cache"
];
AddExpectedConfiguration(builder, expectedArgs);
expectedArgs.Add("--no-launch-profile");
expectedArgs.Add("--flag");
Assert.Equal(expectedArgs, args);
}
[Fact]
public async Task AddDotnetProject_FileBasedApp_InDebugSessionWithProjectCapability_OmitsDotnetRunScaffolding()
{
// The VS Code extension explicitly advertises project support and accepts .cs files, so it owns the
// file-based app invocation just as it does for a .csproj.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = [KnownLaunchConfigurationTypes.Project]
});
var appPath = Path.Combine(builder.AppHostDirectory, "service.cs");
var app = builder.AddDotnetProject("svc", appPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--flag");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
Assert.Equal(["--flag"], args);
}
[Fact]
public async Task AddDotnetProject_InDebugSession_KeepsDotnetRunArgs_WhenProjectLaunchUnsupported()
{
// When the IDE does not advertise project support, the resource runs as a plain process, so the full
// `dotnet run --project ...` command must be preserved.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = ["python"]
});
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
// run --project <path> [--configuration <cfg>] --no-launch-profile --config prod.yaml
Assert.Equal("run", args[0]);
Assert.Equal("--project", args[1]);
Assert.Equal(projectPath, args[2]);
Assert.Contains("--no-launch-profile", args);
Assert.Equal("--config", args[^2]);
Assert.Equal("prod.yaml", args[^1]);
}
[Fact]
public async Task AddDotnetProject_InDebugSession_KeepsDotnetRunArgs_WhenActiveCustomDebugSupportDoesNotOwnInvocation()
{
// SupportsDebugging() consults only the LAST SupportsDebuggingAnnotation. When a caller stacks a
// custom, non-"project" WithDebugSupport that does not own the .NET SDK invocation, the selected launch
// still needs the complete `dotnet run …` scaffolding.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = ["custom"]
});
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml")
.WithDebugSupport(_ => new ExecutableLaunchConfiguration("custom"), "custom");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
// run --project <path> [--configuration <cfg>] --no-launch-profile --config prod.yaml
Assert.Equal("run", args[0]);
Assert.Equal("--project", args[1]);
Assert.Equal(projectPath, args[2]);
Assert.Contains("--no-launch-profile", args);
Assert.Equal("--config", args[^2]);
Assert.Equal("prod.yaml", args[^1]);
}
[Fact]
public async Task AddDotnetProject_InDebugSession_OmitsDotnetRunScaffolding_WhenActiveCustomDebugSupportOwnsLaunchToolArgs()
{
// A stacked custom debug configuration with launch tool arguments owns the tool invocation.
// The `dotnet run …` scaffolding must be omitted; re-emitting it would duplicate that invocation.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = ["custom"]
});
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml")
.WithLaunchToolArgs(ctx => ctx.Args.Add("launch-tool-arg"), ownedByLaunchConfigurationType: "custom")
.WithDebugSupport(_ => new ExecutableLaunchConfiguration("custom"), "custom");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
// Only the custom tool invocation plus the user args remain; no `dotnet run …` scaffolding.
Assert.Collection(args,
arg => Assert.Equal("launch-tool-arg", arg),
arg => Assert.Equal("--config", arg),
arg => Assert.Equal("prod.yaml", arg));
}
[Fact]
public async Task AddDotnetProject_InDebugSession_OmitsDotnetRunScaffolding_WhenOwnedLaunchToolArgsAreEmpty()
{
// Launch tool argument ownership, rather than the number of values produced, determines who supplies the
// project launch. A no-op custom tool invocation must still suppress `dotnet run`.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = ["custom"]
});
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml")
.WithLaunchToolArgs(static _ => { }, ownedByLaunchConfigurationType: "custom")
.WithDebugSupport(_ => new ExecutableLaunchConfiguration("custom"), "custom");
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
Assert.Collection(args,
arg => Assert.Equal("--config", arg),
arg => Assert.Equal("prod.yaml", arg));
}
[Theory]
[InlineData(PersistenceMode.Persistent)]
[InlineData(PersistenceMode.ParentProcess)]
[InlineData(PersistenceMode.Resource)]
public async Task AddDotnetProject_InDebugSession_EffectivePersistentLifetimeKeepsDotnetRunProjectArgs(PersistenceMode persistenceMode)
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = ["project"]
});
var projectPath = Path.Combine(builder.AppHostDirectory, "MyService", "MyService.csproj");
var app = builder.AddDotnetProject("svc", projectPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--config", "prod.yaml");
switch (persistenceMode)
{
case PersistenceMode.Persistent:
app.WithPersistentLifetime();
break;
case PersistenceMode.ParentProcess:
app.WithParentProcessLifetime(Environment.ProcessId);
break;
case PersistenceMode.Resource:
var source = builder.AddContainer("source", "image").WithPersistentLifetime();
app.WithLifetimeOf(source);
break;
default:
throw new ArgumentOutOfRangeException(nameof(persistenceMode), persistenceMode, null);
}
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
Assert.Equal("run", args[0]);
Assert.Equal("--project", args[1]);
Assert.Equal(projectPath, args[2]);
Assert.Contains("--no-launch-profile", args);
Assert.Equal("--config", args[^2]);
Assert.Equal("prod.yaml", args[^1]);
}
[Fact]
public async Task AddDotnetProject_FileBasedApp_InDebugSession_PersistentLifetimeKeepsDotnetRunFileArgs()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(new RunSessionInfo
{
ProtocolsSupported = ["test"],
SupportedLaunchConfigurations = ["project"]
});
var appPath = Path.Combine(builder.AppHostDirectory, "service.cs");
var app = builder.AddDotnetProject("svc", appPath, o => o.ExcludeLaunchProfile = true)
.WithArgs("--flag")
.WithPersistentLifetime();
using var application = builder.Build();
var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource, application.Services);
Assert.Equal("run", args[0]);
Assert.Equal("--file", args[1]);
Assert.Equal(appPath, args[2]);
Assert.Equal("--no-cache", args[3]);
Assert.Contains("--no-launch-profile", args);
Assert.Equal("--flag", args[^1]);
}
private static void AddCustomLaunchToolArgs(CommandLineArgsCallbackContext context)
{
context.Args.Add("tool");
context.Args.Add("exec");
context.Args.Add("package");
context.Args.Add("--yes");
context.Args.Add("--");
}
private static void AddExpectedConfiguration(IDistributedApplicationBuilder builder, List<string> expected)
{
if (builder.AppHostAssembly?.GetCustomAttribute<AssemblyConfigurationAttribute>()?.Configuration is { Length: > 0 } configuration)
{
expected.Add("--configuration");
expected.Add(configuration);
}
}
private static TestDotnetSdkVersionProvider UseDotnetSdkVersion(
IDistributedApplicationBuilder builder,
string? version)
{
var provider = new TestDotnetSdkVersionProvider(version);
builder.Services.AddSingleton<IDotnetSdkVersionProvider>(provider);
return provider;
}
private static async Task ExecutePipelineAsync(DistributedApplication app)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
cts.CancelAfter(TestConstants.LongTimeoutTimeSpan);
var pipeline = app.Services.GetRequiredService<IDistributedApplicationPipeline>();
var context = new PipelineContext(
app.Services.GetRequiredService<DistributedApplicationModel>(),
app.Services.GetRequiredService<DistributedApplicationExecutionContext>(),
app.Services,
app.Services.GetRequiredService<ILogger<DotnetProjectResourceTests>>(),
cts.Token);
await pipeline.ExecuteAsync(context).WaitAsync(cts.Token);
}
private static async Task<string> RunContainerRuntimeAsync(
IProcessRunner processRunner,
IContainerRuntime runtime,
string[] arguments,
CancellationToken cancellationToken)
{
var executable = runtime.Name switch
{
"Docker" => "docker",
"Podman" => "podman",
_ => throw new InvalidOperationException($"Unexpected container runtime '{runtime.Name}'.")
};
var (pendingResult, process) = processRunner.Run(new ProcessSpec(executable)
{
ArgumentList = arguments,
ThrowOnNonZeroReturnCode = true
});
await using var processLifetime = process.ConfigureAwait(false);
var result = await pendingResult.WaitAsync(cancellationToken);
Assert.Equal(0, result.ExitCode);
return string.Join(Environment.NewLine, result.ProcessOutput).Trim();
}
private static string CreateProjectDirectory(string workspacePath)
{
var projectDirectory = Directory.CreateDirectory(Path.Combine(workspacePath, "MyService"));
CreateFile(projectDirectory.FullName, "MyService.csproj");
return projectDirectory.FullName;
}
private static string CreateFile(string directory, string fileName)
{
var path = Path.Combine(directory, fileName);
File.WriteAllText(path, string.Empty);
return path;
}
}