Skip to content

Commit 474a6b3

Browse files
committed
Enhance support for file-based apps and optimize AppHost SDK
- Modified Sdk.in.props to auto-import Microsoft.NET.Sdk - Refined Sdk.in.targets to streamline implicit references and removed workaround targets for file-based apps no longer needed as of rc.2. - Introduced CSharpAppResource class to represent C# projects or file-based apps which doesn't suppress build of the project. - Updated DcpExecutor to handle file-based apps with appropriate run commands. - Enhanced IProjectMetadata to include suppress build functionality. - Improved ProjectResourceBuilderExtensions to support adding C# projects or file-based apps. - Updated project templates to reflect changes in SDK references. - Added unit tests for resource icon resolution to cover file extensions.
1 parent 21f01a7 commit 474a6b3

File tree

14 files changed

+118
-67
lines changed

14 files changed

+118
-67
lines changed

playground/FileBasedApps/Directory.Build.targets

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<AspireProjectOrPackageReference Include="Aspire.Hosting.AppHost" />
55
</ItemGroup>
66

7+
<ItemGroup>
8+
<Compile Include="..\KnownResourceNames.cs" Link="KnownResourceNames.cs" />
9+
</ItemGroup>
10+
711
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.targets', '$(MSBuildThisFileDirectory)../'))" />
812

913
</Project>

playground/FileBasedApps/apphost.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,14 @@
55

66
builder.AddCSharpApp("api", "api.cs");
77

8+
#if !SKIP_DASHBOARD_REFERENCE
9+
// This project is only added in playground projects to support development/debugging
10+
// of the dashboard. It is not required in end developer code. Comment out this code
11+
// or build with `/p:SkipDashboardReference=true`, to test end developer
12+
// dashboard launch experience, Refer to Directory.Build.props for the path to
13+
// the dashboard binary (defaults to the Aspire.Dashboard bin output in the
14+
// artifacts dir).
15+
builder.AddProject<Projects.Aspire_Dashboard>(KnownResourceNames.AspireDashboard);
16+
#endif
17+
818
builder.Build().Run();

src/Aspire.AppHost.Sdk/SDK/Sdk.in.props

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
<!-- Do not import the workload targets as this project is instead using the Aspire.AppHost.Sdk -->
55
<SkipAspireWorkloadManifest>true</SkipAspireWorkloadManifest>
66
<IsAspireHost>true</IsAspireHost>
7+
<!-- Auto-import the Microsoft.NET.Sdk if not already imported -->
8+
<_ImportMicrosoftNETSdkFromAspireSdk Condition=" '$(UsingMicrosoftNETSdk)' == '' ">true</_ImportMicrosoftNETSdkFromAspireSdk>
79
</PropertyGroup>
810

911
<PropertyGroup Condition=" '$(FileBasedProgram)' == 'true' ">
12+
<!-- Disable AOT and Trimming for file-based apps -->
1013
<PublishAot>false</PublishAot>
11-
<PublishTrimmed>false</PublishTrimmed>
14+
<!-- <PublishTrimmed>false</PublishTrimmed>
1215
<EnableAotAnalyzer>false</EnableAotAnalyzer>
1316
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
14-
<EnableSingleFileAnalyzer>false</EnableSingleFileAnalyzer>
15-
<_ImportMicrosoftNETSdkFromAspireSdk Condition=" '$(UsingMicrosoftNETSdk)' == '' ">true</_ImportMicrosoftNETSdkFromAspireSdk>
16-
<_ForceAspireFileBasedAppHostDefaults Condition=" '$(IsAspireHost)' == 'true' ">true</_ForceAspireFileBasedAppHostDefaults>
17+
<EnableSingleFileAnalyzer>false</EnableSingleFileAnalyzer> -->
1718
</PropertyGroup>
1819

1920
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" Condition=" '$(_ImportMicrosoftNETSdkFromAspireSdk)' == 'true' " />
20-
</Project>
21+
</Project>

src/Aspire.AppHost.Sdk/SDK/Sdk.in.targets

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,26 +51,10 @@
5151
<AspireRidToolExecutable>$(AspireRidToolDirectory)Aspire.RuntimeIdentifier.Tool.dll</AspireRidToolExecutable>
5252
</PropertyGroup>
5353

54-
<!-- If the AppHost is a file-based app, then force disable AOT and Trimming.
55-
This can be removed once https://github.com/dotnet/sdk/pull/50885 is merged and flows through to .NET 10 SDK as the values set in Sdk.props will be sufficient. -->
56-
<Target Name="DisablePublishAotForAspireHost" BeforeTargets="_ComputeToolPackInputsToProcessFrameworkReferences;_CollectTargetFrameworkForTelemetry;ProcessFrameworkReferences;Restore;CollectPackageReferences"
57-
Condition="'$(_ForceAspireFileBasedAppHostDefaults)' == 'true'">
58-
<PropertyGroup>
59-
<PublishAot>false</PublishAot>
60-
<PublishTrimmed>false</PublishTrimmed>
61-
<EnableAotAnalyzer>false</EnableAotAnalyzer>
62-
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
63-
<EnableSingleFileAnalyzer>false</EnableSingleFileAnalyzer>
64-
</PropertyGroup>
65-
<ItemGroup>
66-
<ProjectCapability Remove="NativeAOT;PublishAot;PublishTrimmed" />
67-
</ItemGroup>
68-
</Target>
69-
70-
<!-- If the AppHost is a file-based app, add an implicit reference to the Aspire.Hosting.AppHost package if not already referenced.
71-
This is done here so that file-based apps only need to set the single #sdk: directive line to make them an Aspire AppHost.
54+
<!-- Add an implicit reference to the Aspire.Hosting.AppHost package if not already referenced.
55+
This is done here so that AppHost projects and file-based apps only need to set the SDK to make them an Aspire AppHost.
7256
This mechanism can be disabled by setting `SkipAddAspireDefaultReferences` to `true` -->
73-
<Target Name="AddImplicitAspireAppHostPackage" BeforeTargets="Restore;CollectPackageReferences" Condition="'$(_ForceAspireFileBasedAppHostDefaults)' == 'true' and '$(SkipAddAspireDefaultReferences)' != 'true'">
57+
<Target Name="AddImplicitAspireAppHostPackage" BeforeTargets="Restore;CollectPackageReferences" Condition="'$(SkipAddAspireDefaultReferences)' != 'true'">
7458
<PropertyGroup>
7559
<_ImplicitAppHostVersion>@VERSION@</_ImplicitAppHostVersion>
7660
</PropertyGroup>

src/Aspire.Dashboard/Model/ResourceIconHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ string t when t.Contains("database", StringComparison.OrdinalIgnoreCase) => icon
5151
var extension = Path.GetExtension(projectPath);
5252
return extension?.ToLowerInvariant() switch
5353
{
54-
".csproj" => iconResolver.ResolveIconName("CodeCsRectangle", desiredSize, desiredVariant),
54+
".csproj" or ".cs" => iconResolver.ResolveIconName("CodeCsRectangle", desiredSize, desiredVariant),
5555
".fsproj" => iconResolver.ResolveIconName("CodeFsRectangle", desiredSize, desiredVariant),
5656
".vbproj" => iconResolver.ResolveIconName("CodeVbRectangle", desiredSize, desiredVariant),
5757
_ => iconResolver.ResolveIconName("CodeCircle", desiredSize, desiredVariant)

src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ namespace Projects%3B
6060
/// The path to the ]]>%(ClassName)<![CDATA[ project.
6161
/// </summary>
6262
public string ProjectPath => """]]>%(ProjectPath)<![CDATA["""%3B
63+
64+
/// <summary>
65+
/// Gets a value indicating whether building the project before running it should be suppressed.
66+
/// </summary>
67+
/// <remarks>
68+
/// Projects added via ProjectReference items in the AppHost project file are built as part of building the AppHost projectprocess
69+
/// so building them again before running is unnecessary. This property always returns true.
70+
/// </remarks>
71+
public bool SuppressBuild => true%3B
6372
}]]>
6473
</Source>
6574
</AspireProjectMetadataSource>
@@ -96,7 +105,7 @@ namespace Projects%3B
96105
97106
#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
98107
/// <summary>
99-
/// Metadata for the Aspire Host project.
108+
/// Metadata for the Aspire AppHost project.
100109
/// </summary>
101110
[global::System.CodeDom.Compiler.GeneratedCode("Aspire.Hosting", null)]
102111
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification = "Generated code.")]

src/Aspire.Hosting/ApplicationModel/ProjectResource.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,13 @@ internal bool ShouldInjectEndpointEnvironment(EndpointReference e)
3939
.Any(f => !f(endpoint));
4040
}
4141
}
42+
43+
/// <summary>
44+
/// A resource that represents a specified C# project or file-based app.
45+
/// </summary>
46+
/// <param name="name">The name of the resource.</param>
47+
public class CSharpAppResource(string name)
48+
: ProjectResource(name)
49+
{
50+
51+
}

src/Aspire.Hosting/Dcp/DcpExecutor.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,14 +1044,18 @@ private void PrepareProjectExecutables()
10441044
{
10451045
exeSpec.Spec.ExecutionType = ExecutionType.Process;
10461046

1047+
// `dotnet watch` does not work with file-based apps yet, so we have to use `dotnet run` in that case
10471048
if (_configuration.GetBool("DOTNET_WATCH") is not true || projectMetadata.IsFileBasedApp)
10481049
{
10491050
projectArgs.AddRange([
10501051
"run",
1051-
"--no-build",
10521052
projectMetadata.IsFileBasedApp ? "--file" : "--project",
10531053
projectMetadata.ProjectPath,
10541054
]);
1055+
if (projectMetadata.SuppressBuild)
1056+
{
1057+
projectArgs.Add("--no-build");
1058+
}
10551059
}
10561060
else
10571061
{
@@ -1066,7 +1070,7 @@ private void PrepareProjectExecutables()
10661070

10671071
if (!string.IsNullOrEmpty(_distributedApplicationOptions.Configuration))
10681072
{
1069-
projectArgs.AddRange(new[] { "-c", _distributedApplicationOptions.Configuration });
1073+
projectArgs.AddRange(new[] { "--configuration", _distributedApplicationOptions.Configuration });
10701074
}
10711075

10721076
// We pretty much always want to suppress the normal launch profile handling

src/Aspire.Hosting/IProjectMetadata.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ public interface IProjectMetadata : IResourceAnnotation
2525
// Internal for testing.
2626
internal IConfiguration? Configuration => null;
2727

28+
/// <summary>
29+
/// Gets a value indicating whether building the project before running it should be suppressed.
30+
/// </summary>
31+
public bool SuppressBuild => false;
32+
2833
internal bool IsFileBasedApp => string.Equals(Path.GetExtension(ProjectPath), ".cs", StringComparison.OrdinalIgnoreCase);
2934
}
3035

@@ -33,6 +38,8 @@ internal sealed class ProjectMetadata(string projectPath) : IProjectMetadata
3338
{
3439
public string ProjectPath { get; } = ResolveProjectPath(projectPath);
3540

41+
public bool SuppressBuild => false;
42+
3643
private static string ResolveProjectPath(string path)
3744
{
3845
if (Directory.Exists(path))

src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,29 @@ public static IResourceBuilder<ProjectResource> AddProject(this IDistributedAppl
289289
}
290290

291291
/// <summary>
292-
///
292+
/// Adds a C# project or file-based app to the application model.
293293
/// </summary>
294-
/// <param name="builder"></param>
295-
/// <param name="name"></param>
296-
/// <param name="path"></param>
297-
/// <returns></returns>
294+
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
295+
/// <param name="name">The name of the resource. This name will be used for service discovery when referenced in a dependency.</param>
296+
/// <param name="path">The path to the file-based app file, project file, or project directory.</param>
297+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
298+
/// <remarks>
299+
/// <para>
300+
/// This overload of the <see cref="AddCSharpApp(IDistributedApplicationBuilder, string, string)"/> method adds a C# project or file-based app to the application
301+
/// model using a path to the file-based app .cs file, project file (.csproj), or project directory.
302+
/// If the path is not an absolute path then it will be computed relative to the app host directory.
303+
/// </para>
304+
/// <example>
305+
/// Add a file-based app to the app model via a file path.
306+
/// <code lang="csharp">
307+
/// var builder = DistributedApplication.CreateBuilder(args);
308+
///
309+
/// builder.AddCSharpApp("inventoryservice", @"..\InventoryService.cs");
310+
///
311+
/// builder.Build().Run();
312+
/// </code>
313+
/// </example>
314+
/// </remarks>
298315
[Experimental("ASPIRECSHARPAPPS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
299316
public static IResourceBuilder<ProjectResource> AddCSharpApp(this IDistributedApplicationBuilder builder, string name, string path)
300317
{
@@ -306,15 +323,32 @@ public static IResourceBuilder<ProjectResource> AddCSharpApp(this IDistributedAp
306323
}
307324

308325
/// <summary>
309-
///
326+
/// Adds a C# project or file-based app to the application model.
310327
/// </summary>
311-
/// <param name="builder"></param>
312-
/// <param name="name"></param>
313-
/// <param name="path"></param>
314-
/// <param name="configure"></param>
315-
/// <returns></returns>
328+
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
329+
/// <param name="name">The name of the resource. This name will be used for service discovery when referenced in a dependency.</param>
330+
/// <param name="path">The path to the file-based app file, project file, or project directory.</param>
331+
/// <param name="configure">An optional action to configure the C# app resource options.</param>
332+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
333+
/// <remarks>
334+
/// <para>
335+
/// This overload of the <see cref="AddCSharpApp(IDistributedApplicationBuilder, string, string)"/> method adds a C# project or file-based app to the application
336+
/// model using a path to the file-based app .cs file, project file (.csproj), or project directory.
337+
/// If the path is not an absolute path then it will be computed relative to the app host directory.
338+
/// </para>
339+
/// <example>
340+
/// Add a file-based app to the app model via a file path.
341+
/// <code lang="csharp">
342+
/// var builder = DistributedApplication.CreateBuilder(args);
343+
///
344+
/// builder.AddCSharpApp("inventoryservice", @"..\InventoryService.cs");
345+
///
346+
/// builder.Build().Run();
347+
/// </code>
348+
/// </example>
349+
/// </remarks>
316350
[Experimental("ASPIRECSHARPAPPS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
317-
public static IResourceBuilder<ProjectResource> AddCSharpApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string path, Action<ProjectResourceOptions> configure)
351+
public static IResourceBuilder<CSharpAppResource> AddCSharpApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string path, Action<ProjectResourceOptions> configure)
318352
{
319353
ArgumentNullException.ThrowIfNull(builder);
320354
ArgumentNullException.ThrowIfNull(name);
@@ -324,24 +358,25 @@ public static IResourceBuilder<ProjectResource> AddCSharpApp(this IDistributedAp
324358
var options = new ProjectResourceOptions();
325359
configure(options);
326360

327-
var project = new ProjectResource(name);
361+
var app = new CSharpAppResource(name);
328362

329363
path = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, path));
330364
IProjectMetadata projectMetadata = new ProjectMetadata(path);
331365

332-
if (projectMetadata.IsFileBasedApp && (RuntimeUtils.TryGetVersion(out var version) && version.Major < 10))
366+
if (projectMetadata.IsFileBasedApp && RuntimeUtils.TryGetVersion(out var version) && version.Major < 10)
333367
{
334-
// File-based apps are only supported on .NET 10 and later
335-
throw new DistributedApplicationException($"File-based apps are only supported on .NET 10 and later. The current version is {version?.ToString() ?? "unknown"}.");
368+
// File-based apps are only supported on .NET 10 or later
369+
throw new DistributedApplicationException($"File-based apps are only supported on .NET 10 or later. The current version is {version?.ToString() ?? "unknown"}.");
336370
}
337371

338-
return builder.AddResource(project)
372+
return builder.AddResource(app)
339373
.WithAnnotation(projectMetadata)
340-
.WithVSCodeDebugSupport(path, "coreclr", "ms-dotnettools.csharp")
374+
.WithVSCodeDebugSupport(mode => new ProjectLaunchConfiguration { ProjectPath = projectMetadata.ProjectPath, Mode = mode }, "ms-dotnettools.csharp")
341375
.WithProjectDefaults(options);
342376
}
343377

344-
private static IResourceBuilder<ProjectResource> WithProjectDefaults(this IResourceBuilder<ProjectResource> builder, ProjectResourceOptions options)
378+
private static IResourceBuilder<TProjectResource> WithProjectDefaults<TProjectResource>(this IResourceBuilder<TProjectResource> builder, ProjectResourceOptions options)
379+
where TProjectResource : ProjectResource
345380
{
346381
// We only want to turn these on for .NET projects, ConfigureOtlpEnvironment works for any resource type that
347382
// implements IDistributedApplicationResourceWithEnvironment.

0 commit comments

Comments
 (0)