Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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>
30 changes: 30 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/FilesProducedEvent.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Event that is raised when files are produced by a resource.
/// </summary>
/// <param name="resource">The resource that produced the files.</param>
/// <param name="services">The service provider for the app host.</param>
/// <param name="files">The collection of file paths that were produced.</param>
public class FilesProducedEvent(IResource resource, IServiceProvider services, IEnumerable<string> files) : IDistributedApplicationResourceEvent
{
/// <summary>
/// The resource that produced the files.
/// </summary>
public IResource Resource => resource;

/// <summary>
/// The service provider for the app host.
/// </summary>
public IServiceProvider Services => services;

/// <summary>
/// The collection of file paths that were produced.
/// </summary>
public IEnumerable<string> Files => files;
}
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)
{
ArgumentNullException.ThrowIfNull(filePaths);
_files.AddRange(filePaths);
}
}
36 changes: 36 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/FilesSourceAnnotation.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a source annotation for a files resource.
/// </summary>
[DebuggerDisplay("Type = {GetType().Name,nq}, Source = {Source}")]
public sealed class FilesSourceAnnotation : IResourceAnnotation
{
/// <summary>
/// Initializes a new instance of the <see cref="FilesSourceAnnotation"/> class.
/// </summary>
/// <param name="source">The source path for the files.</param>
public FilesSourceAnnotation(string source)
{
ArgumentNullException.ThrowIfNull(source);
Source = source;
}

/// <summary>
/// Gets the source path for the files.
/// </summary>
public string Source { get; }
}

/// <summary>
/// Marker annotation to indicate that the files resource initialization handler has been registered.
/// </summary>
[DebuggerDisplay("Type = {GetType().Name,nq}")]
internal sealed class FilesInitializationHandlerRegisteredAnnotation : IResourceAnnotation
{
}
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; }
}
92 changes: 92 additions & 0 deletions src/Aspire.Hosting/FilesResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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 Microsoft.Extensions.Logging;

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 source annotation to track the source path for validation during initialization
builder.WithAnnotation(new FilesSourceAnnotation(source));

// Check if we've already registered the initialization handler for this resource
var handlerRegistered = builder.Resource.Annotations.OfType<FilesInitializationHandlerRegisteredAnnotation>().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<FilesSourceAnnotation>();
var validatedFiles = new List<string>();

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;
}
}
Loading