From 6aa8ed3ba9b0186138176f9cb7b0401aeac4e241 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Sep 2025 09:51:09 +0000 Subject: [PATCH 1/5] Initial plan From 0c46892a09ea7ecebe13d2a8a72a3ccf1eb0eb60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Sep 2025 10:10:22 +0000 Subject: [PATCH 2/5] Implement Files resource functionality with tests Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- .../ApplicationModel/FilesProducedEvent.cs | 30 ++++ .../ApplicationModel/FilesResource.cs | 39 ++++ .../ApplicationModel/IResourceWithFiles.cs | 15 ++ .../FilesResourceBuilderExtensions.cs | 89 ++++++++++ .../FilesResourceTests.cs | 166 ++++++++++++++++++ 5 files changed, 339 insertions(+) create mode 100644 src/Aspire.Hosting/ApplicationModel/FilesProducedEvent.cs create mode 100644 src/Aspire.Hosting/ApplicationModel/FilesResource.cs create mode 100644 src/Aspire.Hosting/ApplicationModel/IResourceWithFiles.cs create mode 100644 src/Aspire.Hosting/FilesResourceBuilderExtensions.cs create mode 100644 tests/Aspire.Hosting.Tests/FilesResourceTests.cs diff --git a/src/Aspire.Hosting/ApplicationModel/FilesProducedEvent.cs b/src/Aspire.Hosting/ApplicationModel/FilesProducedEvent.cs new file mode 100644 index 00000000000..1a53ebdad68 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/FilesProducedEvent.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.Eventing; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Event that is raised when files are produced by a resource. +/// +/// The resource that produced the files. +/// The service provider for the app host. +/// The collection of file paths that were produced. +public class FilesProducedEvent(IResource resource, IServiceProvider services, IEnumerable files) : IDistributedApplicationResourceEvent +{ + /// + /// The resource that produced the files. + /// + public IResource Resource => resource; + + /// + /// The service provider for the app host. + /// + public IServiceProvider Services => services; + + /// + /// The collection of file paths that were produced. + /// + public IEnumerable Files => files; +} \ No newline at end of file diff --git a/src/Aspire.Hosting/ApplicationModel/FilesResource.cs b/src/Aspire.Hosting/ApplicationModel/FilesResource.cs new file mode 100644 index 00000000000..e991f0453f2 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/FilesResource.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a files resource that can be used by an application. +/// +/// The name of the resource. +/// The collection of file paths associated with this resource. +public class FilesResource(string name, IEnumerable files) : Resource(name), IResourceWithFiles, IResourceWithoutLifetime +{ + private readonly List _files = files?.ToList() ?? []; + + /// + /// Gets the collection of file paths associated with this resource. + /// + public IEnumerable Files => _files; + + /// + /// Adds a file path to the resource. + /// + /// The file path to add. + public void AddFile(string filePath) + { + ArgumentNullException.ThrowIfNull(filePath); + _files.Add(filePath); + } + + /// + /// Adds multiple file paths to the resource. + /// + /// The file paths to add. + public void AddFiles(IEnumerable filePaths) + { + ArgumentNullException.ThrowIfNull(filePaths); + _files.AddRange(filePaths); + } +} \ No newline at end of file diff --git a/src/Aspire.Hosting/ApplicationModel/IResourceWithFiles.cs b/src/Aspire.Hosting/ApplicationModel/IResourceWithFiles.cs new file mode 100644 index 00000000000..7559f76b422 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/IResourceWithFiles.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a resource that can work with files. +/// +public interface IResourceWithFiles +{ + /// + /// Gets the collection of file paths associated with this resource. + /// + IEnumerable Files { get; } +} \ No newline at end of file diff --git a/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs b/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs new file mode 100644 index 00000000000..b3013343580 --- /dev/null +++ b/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding files resources to an application. +/// +public static class FilesResourceBuilderExtensions +{ + /// + /// Adds a files resource to the application. + /// + /// The distributed application builder. + /// The name of the files resource. + /// The collection of file paths to associate with this resource. + /// A resource builder for the files resource. + public static IResourceBuilder AddFiles(this IDistributedApplicationBuilder builder, [ResourceName] string name, IEnumerable files) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(files); + + var filesResource = new FilesResource(name, files); + return builder.AddResource(filesResource); + } + + /// + /// Adds a files resource to the application with a single file. + /// + /// The distributed application builder. + /// The name of the files resource. + /// The file path to associate with this resource. + /// A resource builder for the files resource. + public static IResourceBuilder AddFiles(this IDistributedApplicationBuilder builder, [ResourceName] string name, string filePath) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(filePath); + + return builder.AddFiles(name, [filePath]); + } + + /// + /// Adds a files resource to the application without any initial files. + /// + /// The distributed application builder. + /// The name of the files resource. + /// A resource builder for the files resource. + public static IResourceBuilder AddFiles(this IDistributedApplicationBuilder builder, [ResourceName] string name) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + + return builder.AddFiles(name, []); + } + + /// + /// Adds a file to an existing files resource. + /// + /// The resource builder for the files resource. + /// The file path to add. + /// The resource builder for the files resource. + public static IResourceBuilder WithFile(this IResourceBuilder builder, string filePath) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(filePath); + + builder.Resource.AddFile(filePath); + return builder; + } + + /// + /// Adds multiple files to an existing files resource. + /// + /// The resource builder for the files resource. + /// The file paths to add. + /// The resource builder for the files resource. + public static IResourceBuilder WithFiles(this IResourceBuilder builder, IEnumerable filePaths) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(filePaths); + + builder.Resource.AddFiles(filePaths); + return builder; + } +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Tests/FilesResourceTests.cs b/tests/Aspire.Hosting.Tests/FilesResourceTests.cs new file mode 100644 index 00000000000..fcbeba6e979 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/FilesResourceTests.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting.Tests; + +public class FilesResourceTests +{ + [Fact] + public void AddFilesResourceWithMultipleFiles() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var files = new[] { "/path/to/file1.txt", "/path/to/file2.txt" }; + + appBuilder.AddFiles("myfiles", files); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + var filesResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("myfiles", filesResource.Name); + Assert.Equal(files, filesResource.Files); + } + + [Fact] + public void AddFilesResourceWithSingleFile() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var filePath = "/path/to/file.txt"; + + appBuilder.AddFiles("myfile", filePath); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + var filesResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("myfile", filesResource.Name); + Assert.Single(filesResource.Files, filePath); + } + + [Fact] + public void AddFilesResourceWithNoInitialFiles() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + appBuilder.AddFiles("emptyfiles"); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + var filesResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("emptyfiles", filesResource.Name); + Assert.Empty(filesResource.Files); + } + + [Fact] + public void WithFileAddsFileToResource() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + appBuilder.AddFiles("myfiles") + .WithFile("/path/to/file1.txt") + .WithFile("/path/to/file2.txt"); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + var filesResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("myfiles", filesResource.Name); + Assert.Equal(2, filesResource.Files.Count()); + Assert.Contains("/path/to/file1.txt", filesResource.Files); + Assert.Contains("/path/to/file2.txt", filesResource.Files); + } + + [Fact] + public void WithFilesAddsMultipleFilesToResource() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var additionalFiles = new[] { "/path/to/file3.txt", "/path/to/file4.txt" }; + + appBuilder.AddFiles("myfiles", "/path/to/file1.txt") + .WithFiles(additionalFiles); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + var filesResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("myfiles", filesResource.Name); + Assert.Equal(3, filesResource.Files.Count()); + Assert.Contains("/path/to/file1.txt", filesResource.Files); + Assert.Contains("/path/to/file3.txt", filesResource.Files); + Assert.Contains("/path/to/file4.txt", filesResource.Files); + } + + [Fact] + public void FilesResourceImplementsIResourceWithFiles() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var files = new[] { "/path/to/file1.txt" }; + + appBuilder.AddFiles("myfiles", files); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + var filesResource = Assert.Single(appModel.Resources.OfType()); + + Assert.IsAssignableFrom(filesResource); + var resourceWithFiles = (IResourceWithFiles)filesResource; + Assert.Equal(files, resourceWithFiles.Files); + } + + [Fact] + public void FilesResourceImplementsIResourceWithoutLifetime() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + appBuilder.AddFiles("myfiles"); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + var filesResource = Assert.Single(appModel.Resources.OfType()); + + Assert.IsAssignableFrom(filesResource); + } + + [Fact] + public void FilesProducedEventCreatesCorrectly() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var filesResource = new FilesResource("test", ["/path/to/file.txt"]); + var files = new[] { "/path/to/output1.txt", "/path/to/output2.txt" }; + + using var app = appBuilder.Build(); + var services = app.Services; + + var filesProducedEvent = new FilesProducedEvent(filesResource, services, files); + + Assert.Equal(filesResource, filesProducedEvent.Resource); + Assert.Equal(services, filesProducedEvent.Services); + Assert.Equal(files, filesProducedEvent.Files); + } + + [Fact] + public void FilesResourceAddFileThrowsOnNull() + { + var filesResource = new FilesResource("test", []); + + Assert.Throws(() => filesResource.AddFile(null!)); + } + + [Fact] + public void FilesResourceAddFilesThrowsOnNull() + { + var filesResource = new FilesResource("test", []); + + Assert.Throws(() => filesResource.AddFiles(null!)); + } +} \ No newline at end of file From 8d99ff33c8efc4780bdfba3d863c67987d1f844d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Sep 2025 11:25:30 +0000 Subject: [PATCH 3/5] Update Files resource API per feedback - remove file list parameter and replace WithFile/WithFiles with WithSource Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- .../files/Files.AppHost/Files.AppHost.csproj | 19 +++++ playground/files/Files.AppHost/Program.cs | 20 ++++++ .../Properties/launchSettings.json | 29 ++++++++ playground/files/content/index.html | 67 ++++++++++++++++++ .../FilesResourceBuilderExtensions.cs | 61 ++-------------- .../FilesResourceTests.cs | 70 ++----------------- 6 files changed, 149 insertions(+), 117 deletions(-) create mode 100644 playground/files/Files.AppHost/Files.AppHost.csproj create mode 100644 playground/files/Files.AppHost/Program.cs create mode 100644 playground/files/Files.AppHost/Properties/launchSettings.json create mode 100644 playground/files/content/index.html diff --git a/playground/files/Files.AppHost/Files.AppHost.csproj b/playground/files/Files.AppHost/Files.AppHost.csproj new file mode 100644 index 00000000000..91e3411b5d1 --- /dev/null +++ b/playground/files/Files.AppHost/Files.AppHost.csproj @@ -0,0 +1,19 @@ + + + + Exe + $(DefaultTargetFramework) + enable + enable + true + + + + + + + + + + + \ No newline at end of file diff --git a/playground/files/Files.AppHost/Program.cs b/playground/files/Files.AppHost/Program.cs new file mode 100644 index 00000000000..e9bdb331b79 --- /dev/null +++ b/playground/files/Files.AppHost/Program.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +var builder = DistributedApplication.CreateBuilder(args); + +// Add a files resource with a source directory containing static content +builder.AddFiles("static-content") + .WithSource("content"); + +#if !SKIP_DASHBOARD_REFERENCE +// This project is only added in playground projects to support development/debugging +// of the dashboard. It is not required in end developer code. Comment out this code +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). +builder.AddProject(KnownResourceNames.AspireDashboard); +#endif + +builder.Build().Run(); \ No newline at end of file diff --git a/playground/files/Files.AppHost/Properties/launchSettings.json b/playground/files/Files.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000000..83f6fa5386a --- /dev/null +++ b/playground/files/Files.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17222;http://localhost:15222", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21222", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22222" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15222", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19222", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20222" + } + } + } +} \ No newline at end of file diff --git a/playground/files/content/index.html b/playground/files/content/index.html new file mode 100644 index 00000000000..d29a438d32b --- /dev/null +++ b/playground/files/content/index.html @@ -0,0 +1,67 @@ + + + + + + Files Resource Demo + + + +
+

🗂️ Aspire Files Resource Demo

+ +

This is a simple HTML file that demonstrates the new Files resource in Aspire.Hosting.

+ +
+

📁 What is the Files Resource?

+

The Files resource is a lightweight primitive for representing and managing collections of files in Aspire applications. It's useful for:

+
    +
  • Configuration files
  • +
  • Static content and assets
  • +
  • Templates
  • +
  • Documentation
  • +
  • Any file-based resources your application needs
  • +
+
+ +
+

🛠️ How to Use

+

In your AppHost, you can add files resources like this:

+ builder.AddFiles("static-content").WithSource("content"); +
+ +

This file is located in the content/ directory and is referenced by the Files resource in the playground application.

+
+ + \ No newline at end of file diff --git a/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs b/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs index b3013343580..1ded8d823d9 100644 --- a/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs @@ -15,75 +15,28 @@ public static class FilesResourceBuilderExtensions /// /// The distributed application builder. /// The name of the files resource. - /// The collection of file paths to associate with this resource. - /// A resource builder for the files resource. - public static IResourceBuilder AddFiles(this IDistributedApplicationBuilder builder, [ResourceName] string name, IEnumerable files) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(name); - ArgumentNullException.ThrowIfNull(files); - - var filesResource = new FilesResource(name, files); - return builder.AddResource(filesResource); - } - - /// - /// Adds a files resource to the application with a single file. - /// - /// The distributed application builder. - /// The name of the files resource. - /// The file path to associate with this resource. - /// A resource builder for the files resource. - public static IResourceBuilder AddFiles(this IDistributedApplicationBuilder builder, [ResourceName] string name, string filePath) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(name); - ArgumentNullException.ThrowIfNull(filePath); - - return builder.AddFiles(name, [filePath]); - } - - /// - /// Adds a files resource to the application without any initial files. - /// - /// The distributed application builder. - /// The name of the files resource. /// A resource builder for the files resource. public static IResourceBuilder AddFiles(this IDistributedApplicationBuilder builder, [ResourceName] string name) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(name); - return builder.AddFiles(name, []); - } - - /// - /// Adds a file to an existing files resource. - /// - /// The resource builder for the files resource. - /// The file path to add. - /// The resource builder for the files resource. - public static IResourceBuilder WithFile(this IResourceBuilder builder, string filePath) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(filePath); - - builder.Resource.AddFile(filePath); - return builder; + var filesResource = new FilesResource(name, []); + return builder.AddResource(filesResource); } /// - /// Adds multiple files to an existing files resource. + /// Adds a source directory or file to an existing files resource. /// /// The resource builder for the files resource. - /// The file paths to add. + /// The source path (directory or file) to associate with this resource. /// The resource builder for the files resource. - public static IResourceBuilder WithFiles(this IResourceBuilder builder, IEnumerable filePaths) + public static IResourceBuilder WithSource(this IResourceBuilder builder, string source) { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(filePaths); + ArgumentNullException.ThrowIfNull(source); - builder.Resource.AddFiles(filePaths); + builder.Resource.AddFile(source); return builder; } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Tests/FilesResourceTests.cs b/tests/Aspire.Hosting.Tests/FilesResourceTests.cs index fcbeba6e979..610234353b6 100644 --- a/tests/Aspire.Hosting.Tests/FilesResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/FilesResourceTests.cs @@ -7,40 +7,6 @@ namespace Aspire.Hosting.Tests; public class FilesResourceTests { - [Fact] - public void AddFilesResourceWithMultipleFiles() - { - var appBuilder = DistributedApplication.CreateBuilder(); - var files = new[] { "/path/to/file1.txt", "/path/to/file2.txt" }; - - appBuilder.AddFiles("myfiles", files); - - using var app = appBuilder.Build(); - - var appModel = app.Services.GetRequiredService(); - var filesResource = Assert.Single(appModel.Resources.OfType()); - - Assert.Equal("myfiles", filesResource.Name); - Assert.Equal(files, filesResource.Files); - } - - [Fact] - public void AddFilesResourceWithSingleFile() - { - var appBuilder = DistributedApplication.CreateBuilder(); - var filePath = "/path/to/file.txt"; - - appBuilder.AddFiles("myfile", filePath); - - using var app = appBuilder.Build(); - - var appModel = app.Services.GetRequiredService(); - var filesResource = Assert.Single(appModel.Resources.OfType()); - - Assert.Equal("myfile", filesResource.Name); - Assert.Single(filesResource.Files, filePath); - } - [Fact] public void AddFilesResourceWithNoInitialFiles() { @@ -58,13 +24,13 @@ public void AddFilesResourceWithNoInitialFiles() } [Fact] - public void WithFileAddsFileToResource() + public void WithSourceAddsSourceToResource() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddFiles("myfiles") - .WithFile("/path/to/file1.txt") - .WithFile("/path/to/file2.txt"); + .WithSource("/path/to/directory") + .WithSource("/path/to/file.txt"); using var app = appBuilder.Build(); @@ -73,38 +39,16 @@ public void WithFileAddsFileToResource() Assert.Equal("myfiles", filesResource.Name); Assert.Equal(2, filesResource.Files.Count()); - Assert.Contains("/path/to/file1.txt", filesResource.Files); - Assert.Contains("/path/to/file2.txt", filesResource.Files); - } - - [Fact] - public void WithFilesAddsMultipleFilesToResource() - { - var appBuilder = DistributedApplication.CreateBuilder(); - var additionalFiles = new[] { "/path/to/file3.txt", "/path/to/file4.txt" }; - - appBuilder.AddFiles("myfiles", "/path/to/file1.txt") - .WithFiles(additionalFiles); - - using var app = appBuilder.Build(); - - var appModel = app.Services.GetRequiredService(); - var filesResource = Assert.Single(appModel.Resources.OfType()); - - Assert.Equal("myfiles", filesResource.Name); - Assert.Equal(3, filesResource.Files.Count()); - Assert.Contains("/path/to/file1.txt", filesResource.Files); - Assert.Contains("/path/to/file3.txt", filesResource.Files); - Assert.Contains("/path/to/file4.txt", filesResource.Files); + Assert.Contains("/path/to/directory", filesResource.Files); + Assert.Contains("/path/to/file.txt", filesResource.Files); } [Fact] public void FilesResourceImplementsIResourceWithFiles() { var appBuilder = DistributedApplication.CreateBuilder(); - var files = new[] { "/path/to/file1.txt" }; - appBuilder.AddFiles("myfiles", files); + appBuilder.AddFiles("myfiles").WithSource("/path/to/file1.txt"); using var app = appBuilder.Build(); @@ -113,7 +57,7 @@ public void FilesResourceImplementsIResourceWithFiles() Assert.IsAssignableFrom(filesResource); var resourceWithFiles = (IResourceWithFiles)filesResource; - Assert.Equal(files, resourceWithFiles.Files); + Assert.Single(resourceWithFiles.Files, "/path/to/file1.txt"); } [Fact] From 3618ccb3f2ca0e4929ad400e721717ee5255e2cd Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Sun, 7 Sep 2025 21:34:24 +1000 Subject: [PATCH 4/5] Tweaks. --- Aspire.slnx | 3 +++ playground/files/Files.AppHost/Program.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Aspire.slnx b/Aspire.slnx index b267e9afedc..4b4f436faea 100644 --- a/Aspire.slnx +++ b/Aspire.slnx @@ -189,6 +189,9 @@ + + + diff --git a/playground/files/Files.AppHost/Program.cs b/playground/files/Files.AppHost/Program.cs index e9bdb331b79..1085f76e8a7 100644 --- a/playground/files/Files.AppHost/Program.cs +++ b/playground/files/Files.AppHost/Program.cs @@ -5,7 +5,7 @@ // Add a files resource with a source directory containing static content builder.AddFiles("static-content") - .WithSource("content"); + .WithSource("../content"); #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging From 5dcaf5cedf63fde9822bf22b400f3f85d1159264 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Sep 2025 12:03:46 +0000 Subject: [PATCH 5/5] Wire up Files resource WithSource to OnResourceInitialize event Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- .../ApplicationModel/FilesSourceAnnotation.cs | 36 +++++++++++++ .../FilesResourceBuilderExtensions.cs | 50 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/Aspire.Hosting/ApplicationModel/FilesSourceAnnotation.cs diff --git a/src/Aspire.Hosting/ApplicationModel/FilesSourceAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/FilesSourceAnnotation.cs new file mode 100644 index 00000000000..287c5ec869c --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/FilesSourceAnnotation.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a source annotation for a files resource. +/// +[DebuggerDisplay("Type = {GetType().Name,nq}, Source = {Source}")] +public sealed class FilesSourceAnnotation : IResourceAnnotation +{ + /// + /// Initializes a new instance of the class. + /// + /// The source path for the files. + public FilesSourceAnnotation(string source) + { + ArgumentNullException.ThrowIfNull(source); + Source = source; + } + + /// + /// Gets the source path for the files. + /// + public string Source { get; } +} + +/// +/// Marker annotation to indicate that the files resource initialization handler has been registered. +/// +[DebuggerDisplay("Type = {GetType().Name,nq}")] +internal sealed class FilesInitializationHandlerRegisteredAnnotation : IResourceAnnotation +{ +} \ No newline at end of file diff --git a/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs b/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs index 1ded8d823d9..c0e7ac741c9 100644 --- a/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/FilesResourceBuilderExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.Logging; namespace Aspire.Hosting; @@ -36,7 +37,56 @@ public static IResourceBuilder WithSource(this IResourceBuilder().Any(); + + if (!handlerRegistered) + { + // Mark that we've registered the handler + builder.WithAnnotation(new FilesInitializationHandlerRegisteredAnnotation()); + + // Subscribe to the InitializeResourceEvent to process the source when the resource is initialized + builder.OnInitializeResource(async (filesResource, initEvent, ct) => + { + var sourceAnnotations = filesResource.Annotations.OfType(); + var validatedFiles = new List(); + + foreach (var sourceAnnotation in sourceAnnotations) + { + var sourcePath = sourceAnnotation.Source; + + // Verify that the path specified in WithSource(path) is a valid directory + if (Directory.Exists(sourcePath)) + { + validatedFiles.Add(sourcePath); + // Add all files within the directory to the validated list + var filesInDirectory = Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories); + validatedFiles.AddRange(filesInDirectory); + } + else + { + initEvent.Logger.LogWarning("Source path '{SourcePath}' for files resource '{ResourceName}' is not a valid directory.", sourcePath, filesResource.Name); + continue; + } + } + + // Fire the FilesProducedEvent with the validated files + if (validatedFiles.Count > 0) + { + await initEvent.Eventing.PublishAsync(new FilesProducedEvent(filesResource, initEvent.Services, validatedFiles), ct).ConfigureAwait(false); + } + + // Fire the ResourceReadyEvent + await initEvent.Eventing.PublishAsync(new ResourceReadyEvent(filesResource, initEvent.Services), ct).ConfigureAwait(false); + }); + } + return builder; } } \ No newline at end of file