Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@
<Project Path="playground/ExternalServices/ExternalServices.AppHost/ExternalServices.AppHost.csproj" />
<Project Path="playground/ExternalServices/WebFrontEnd/WebFrontEnd.csproj" />
</Folder>
<Folder Name="/playground/files/">
<Project Path="playground/files/Files.AppHost/Files.AppHost.csproj" />
</Folder>
<Folder Name="/playground/HealthChecks/">
<Project Path="playground/HealthChecks/HealthChecksSandbox.AppHost/HealthChecksSandbox.AppHost.csproj" />
</Folder>
Expand Down
19 changes: 19 additions & 0 deletions playground/files/Files.AppHost/Files.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\..\KnownResourceNames.cs" Link="KnownResourceNames.cs" />
</ItemGroup>

<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Hosting.AppHost" />
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions playground/files/Files.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -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<Projects.Aspire_Dashboard>(KnownResourceNames.AspireDashboard);
#endif

builder.Build().Run();
29 changes: 29 additions & 0 deletions playground/files/Files.AppHost/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
67 changes: 67 additions & 0 deletions playground/files/content/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Files Resource Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 3px solid #007acc;
padding-bottom: 10px;
}
.highlight {
background-color: #e7f3ff;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
code {
background-color: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>🗂️ Aspire Files Resource Demo</h1>

<p>This is a simple HTML file that demonstrates the new <strong>Files resource</strong> in Aspire.Hosting.</p>

<div class="highlight">
<h3>📁 What is the Files Resource?</h3>
<p>The Files resource is a lightweight primitive for representing and managing collections of files in Aspire applications. It's useful for:</p>
<ul>
<li>Configuration files</li>
<li>Static content and assets</li>
<li>Templates</li>
<li>Documentation</li>
<li>Any file-based resources your application needs</li>
</ul>
</div>

<div class="highlight">
<h3>🛠️ How to Use</h3>
<p>In your AppHost, you can add files resources like this:</p>
<code>builder.AddFiles("static-content").WithSource("content");</code>
</div>

<p><em>This file is located in the <code>content/</code> directory and is referenced by the Files resource in the playground application.</em></p>
</div>
</body>
</html>
28 changes: 28 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/FilesCallbackAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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;

/// <summary>
/// Represents an annotation that provides a callback to enumerate files asynchronously.
/// </summary>
[DebuggerDisplay("Type = {GetType().Name,nq}")]
public sealed class FilesCallbackAnnotation : IResourceAnnotation
{
/// <summary>
/// Initializes a new instance of the <see cref="FilesCallbackAnnotation"/> class.
/// </summary>
/// <param name="callback">A callback that returns an async enumerable of file paths.</param>
public FilesCallbackAnnotation(Func<CancellationToken, IAsyncEnumerable<string>> callback)
{
ArgumentNullException.ThrowIfNull(callback);
Callback = callback;
}

/// <summary>
/// Gets the callback that returns an async enumerable of file paths.
/// </summary>
public Func<CancellationToken, IAsyncEnumerable<string>> Callback { get; }
}
39 changes: 39 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/FilesResource.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a files resource that can be used by an application.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="files">The collection of file paths associated with this resource.</param>
public class FilesResource(string name, IEnumerable<string> files) : Resource(name), IResourceWithFiles, IResourceWithoutLifetime
{
private readonly List<string> _files = files?.ToList() ?? [];

/// <summary>
/// Gets the collection of file paths associated with this resource.
/// </summary>
public IEnumerable<string> Files => _files;

/// <summary>
/// Adds a file path to the resource.
/// </summary>
/// <param name="filePath">The file path to add.</param>
public void AddFile(string filePath)
{
ArgumentNullException.ThrowIfNull(filePath);
_files.Add(filePath);
}

/// <summary>
/// Adds multiple file paths to the resource.
/// </summary>
/// <param name="filePaths">The file paths to add.</param>
public void AddFiles(IEnumerable<string> filePaths)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot introduce a new type called ResourceFile which is used instead of the string as the generic type for IAsyncEnumerable.

It should have two properties. FullPath and RelativePath.

The relative path should be the normalised path relative to the root of the entire set of files.

For example if AddFiles specifies a path it would be the path relative to that starting point. The full path would be the full path on the local system.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've introduced the ResourceFile type and updated the IAsyncEnumerable to use it in commit 0c03075. The ResourceFile class has both FullPath and RelativePath properties. For directories, the relative path calculation is based on the source directory as the root, and file paths are normalized using forward slashes for consistency.

{
ArgumentNullException.ThrowIfNull(filePaths);
_files.AddRange(filePaths);
}
}
15 changes: 15 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/IResourceWithFiles.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a resource that can work with files.
/// </summary>
public interface IResourceWithFiles
{
/// <summary>
/// Gets the collection of file paths associated with this resource.
/// </summary>
IEnumerable<string> Files { get; }
}
71 changes: 71 additions & 0 deletions src/Aspire.Hosting/FilesResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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;
using System.Runtime.CompilerServices;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding files resources to an application.
/// </summary>
public static class FilesResourceBuilderExtensions
{
/// <summary>
/// Adds a files resource to the application.
/// </summary>
/// <param name="builder">The distributed application builder.</param>
/// <param name="name">The name of the files resource.</param>
/// <returns>A resource builder for the files resource.</returns>
public static IResourceBuilder<FilesResource> AddFiles(this IDistributedApplicationBuilder builder, [ResourceName] string name)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);

var filesResource = new FilesResource(name, []);
return builder.AddResource(filesResource);
}

/// <summary>
/// Adds a source directory or file to an existing files resource.
/// </summary>
/// <param name="builder">The resource builder for the files resource.</param>
/// <param name="source">The source path (directory or file) to associate with this resource.</param>
/// <returns>The resource builder for the files resource.</returns>
public static IResourceBuilder<FilesResource> WithSource(this IResourceBuilder<FilesResource> builder, string source)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(source);

// Add the source immediately to maintain existing behavior
builder.Resource.AddFile(source);

// Add callback annotation that will enumerate files from the source
builder.WithAnnotation(new FilesCallbackAnnotation(cancellationToken => EnumerateFilesAsync(source, cancellationToken)));

return builder;
}

private static async IAsyncEnumerable<string> EnumerateFilesAsync(string source, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// If the source is a directory, enumerate all files in it
if (Directory.Exists(source))
{
yield return source;

var files = Directory.GetFiles(source, "*", SearchOption.AllDirectories);
foreach (var file in files)
{
cancellationToken.ThrowIfCancellationRequested();
yield return file;
}
}
else if (File.Exists(source))
{
// If it's a file, just return it
yield return source;
}

await Task.CompletedTask.ConfigureAwait(false); // Satisfy async enumerable requirements
}
}
Loading