Skip to content

Commit 5b92c02

Browse files
Copilotdavidfowl
andauthored
Add IFileSystemService abstraction for simplified temp file management (#13282)
* Initial plan * Add IFileSystemService interfaces with adjusted APIs Co-authored-by: davidfowl <[email protected]> Update DistributedApplicationBuilder to register and expose IFileSystemService Co-authored-by: davidfowl <[email protected]> Update AspireStore and Locations to use IFileSystemService Co-authored-by: davidfowl <[email protected]> Add implementation status document with remaining work Co-authored-by: davidfowl <[email protected]> Fix invalid XML doc comment syntax for TempFile.Dispose Co-authored-by: davidfowl <[email protected]> Simplify CreateTempFile API to take no parameters Co-authored-by: davidfowl <[email protected]> Fix test compilation errors - add experimental warnings and fix constructor calls Co-authored-by: davidfowl <[email protected]> Update high priority files to use IFileSystemService Co-authored-by: davidfowl <[email protected]> Add optional fileName parameter to CreateTempFile Co-authored-by: davidfowl <[email protected]> Rename shared TempDirectory helper class to TestTempDirectory Co-authored-by: davidfowl <[email protected]> Refactor file handling to use IFileSystemService for temporary file management in Maui environment annotations and MySql builder extensions Update implementation status to reflect IFileSystemService integration across MySQL and MAUI components * Refactor user secrets management and file system service integration * Refactor tests to use TestTempDirectory instead of TempDirectory - Updated multiple test files across MySql, PostgreSQL, Python, and Yarp tests to replace instances of TempDirectory with TestTempDirectory for improved test isolation and resource management. - Ensured consistent usage of TestTempDirectory in various test scenarios, including configuration file generation and environment variable handling. * Add tests for FileSystemService methods and temp directory management * Remove implementation status document for IFileSystemService adjustments * Add IUserSecretsManager support and suppress related warnings * Add missing FileSystemService and UserSecretsManager properties to IDistributedApplicationTestingBuilder * Specify filename for temporary runtime config files in CreateCustomRuntimeConfig method --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: David Fowler <[email protected]>
1 parent 1c9617b commit 5b92c02

File tree

60 files changed

+845
-290
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+845
-290
lines changed

src/Aspire.Hosting.Azure/AzurePublishingContext.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
5+
46
using System.Diagnostics.CodeAnalysis;
57
using System.Runtime.CompilerServices;
68
using Aspire.Hosting.ApplicationModel;
79
using Aspire.Hosting.Pipelines;
10+
using Microsoft.Extensions.DependencyInjection;
811
using Azure.Provisioning;
912
using Azure.Provisioning.Expressions;
1013
using Azure.Provisioning.Primitives;
@@ -118,6 +121,9 @@ private async Task WriteAzureArtifactsOutputAsync(IReportingStep step, Distribut
118121
outputDirectory.Create();
119122
}
120123

124+
var fileSystemService = ServiceProvider.GetRequiredService<IFileSystemService>();
125+
var tempDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-bicep").Path;
126+
121127
var bicepResourcesToPublish = model.Resources.OfType<AzureBicepResource>()
122128
.Where(r => !r.IsExcludedFromPublish())
123129
.ToList();
@@ -145,7 +151,7 @@ private async Task WriteAzureArtifactsOutputAsync(IReportingStep step, Distribut
145151

146152
foreach (var resource in bicepResourcesToPublish)
147153
{
148-
var file = resource.GetBicepTemplateFile();
154+
var file = resource.GetBicepTemplateFile(tempDirectory);
149155

150156
var moduleDirectory = outputDirectory.CreateSubdirectory(resource.Name);
151157

@@ -336,7 +342,7 @@ void CaptureBicepOutputsFromParameters(IResourceWithParameters resource)
336342

337343
var modulePath = Path.Combine(moduleDirectory.FullName, $"{resource.Name}.bicep");
338344

339-
var file = br.GetBicepTemplateFile();
345+
var file = br.GetBicepTemplateFile(tempDirectory);
340346

341347
File.Copy(file.Path, modulePath, true);
342348

src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
5+
36
using System.Collections.Immutable;
47
using System.Diagnostics;
58
using System.Text.Json.Nodes;
@@ -23,6 +26,7 @@ internal sealed class BicepProvisioner(
2326
ISecretClientProvider secretClientProvider,
2427
IDeploymentStateManager deploymentStateManager,
2528
DistributedApplicationExecutionContext executionContext,
29+
IFileSystemService fileSystemService,
2630
ILogger<BicepProvisioner> logger) : IBicepProvisioner
2731
{
2832
/// <inheritdoc />
@@ -135,7 +139,8 @@ await notificationService.PublishUpdateAsync(resource, state => state with
135139
])
136140
}).ConfigureAwait(false);
137141

138-
var template = resource.GetBicepTemplateFile();
142+
var tempDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-bicep").Path;
143+
var template = resource.GetBicepTemplateFile(tempDirectory);
139144
var path = template.Path;
140145

141146
// GetBicepTemplateFile may have added new well-known parameters, so we need

src/Aspire.Hosting.Maui/Utilities/MauiAndroidEnvironmentAnnotation.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
5+
46
using Aspire.Hosting.ApplicationModel;
57
using Aspire.Hosting.Eventing;
68
using Aspire.Hosting.Lifecycle;
9+
using Microsoft.Extensions.DependencyInjection;
710
using Microsoft.Extensions.Logging;
811

912
namespace Aspire.Hosting.Maui.Utilities;
@@ -38,7 +41,8 @@ internal sealed class MauiAndroidEnvironmentProcessedAnnotation : IResourceAnnot
3841
internal sealed class MauiAndroidEnvironmentSubscriber(
3942
DistributedApplicationExecutionContext executionContext,
4043
ResourceLoggerService loggerService,
41-
ResourceNotificationService notificationService) : IDistributedApplicationEventingSubscriber
44+
ResourceNotificationService notificationService,
45+
IFileSystemService fileSystemService) : IDistributedApplicationEventingSubscriber
4246
{
4347
public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext execContext, CancellationToken cancellationToken)
4448
{
@@ -83,6 +87,7 @@ private async Task OnBeforeResourceStartedAsync(BeforeResourceStartedEvent @even
8387
if (generatedFilePath is null)
8488
{
8589
generatedFilePath = await MauiEnvironmentHelper.CreateAndroidEnvironmentTargetsFileAsync(
90+
fileSystemService,
8691
resource,
8792
executionContext,
8893
logger,

src/Aspire.Hosting.Maui/Utilities/MauiEnvironmentHelper.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
5+
46
using System.Globalization;
57
using System.Text;
68
using System.Xml.Linq;
@@ -22,12 +24,14 @@ internal static class MauiEnvironmentHelper
2224
/// <summary>
2325
/// Creates an MSBuild targets file for Android that sets environment variables.
2426
/// </summary>
27+
/// <param name="fileSystemService">The file system service for managing temp files.</param>
2528
/// <param name="resource">The resource to collect environment variables from.</param>
2629
/// <param name="executionContext">The execution context.</param>
2730
/// <param name="logger">Logger for diagnostic output.</param>
2831
/// <param name="cancellationToken">Cancellation token.</param>
2932
/// <returns>The path to the generated targets file, or null if no environment variables are present.</returns>
3033
public static async Task<string?> CreateAndroidEnvironmentTargetsFileAsync(
34+
IFileSystemService fileSystemService,
3135
IResource resource,
3236
DistributedApplicationExecutionContext executionContext,
3337
ILogger logger,
@@ -62,8 +66,7 @@ internal static class MauiEnvironmentHelper
6266
}
6367

6468
// Create a temporary targets file
65-
var tempDirectory = Path.Combine(Path.GetTempPath(), "aspire", "maui", "android-env");
66-
Directory.CreateDirectory(tempDirectory);
69+
var tempDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-maui-android-env").Path;
6770

6871
// Prune old targets files
6972
PruneOldTargets(tempDirectory, logger);
@@ -209,12 +212,14 @@ private static string EncodeSemicolons(string value, out bool wasEncoded)
209212
/// <summary>
210213
/// Creates an MSBuild targets file for iOS that sets environment variables.
211214
/// </summary>
215+
/// <param name="fileSystemService">The file system service for managing temp files.</param>
212216
/// <param name="resource">The resource to collect environment variables from.</param>
213217
/// <param name="executionContext">The execution context.</param>
214218
/// <param name="logger">Logger for diagnostic output.</param>
215219
/// <param name="cancellationToken">Cancellation token.</param>
216220
/// <returns>The path to the generated targets file, or null if no environment variables are present.</returns>
217221
public static async Task<string?> CreateiOSEnvironmentTargetsFileAsync(
222+
IFileSystemService fileSystemService,
218223
IResource resource,
219224
DistributedApplicationExecutionContext executionContext,
220225
ILogger logger,
@@ -232,8 +237,7 @@ private static string EncodeSemicolons(string value, out bool wasEncoded)
232237
}
233238

234239
// Create a temporary targets file
235-
var tempDirectory = Path.Combine(Path.GetTempPath(), "aspire", "maui", "mlaunch-env");
236-
Directory.CreateDirectory(tempDirectory);
240+
var tempDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-maui-mlaunch-env").Path;
237241

238242
// Prune old targets files
239243
PruneOldTargetsiOS(tempDirectory, logger);

src/Aspire.Hosting.Maui/Utilities/MauiiOSEnvironmentAnnotation.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
5+
46
using Aspire.Hosting.ApplicationModel;
57
using Aspire.Hosting.Eventing;
68
using Aspire.Hosting.Lifecycle;
9+
using Microsoft.Extensions.DependencyInjection;
710
using Microsoft.Extensions.Logging;
811

912
namespace Aspire.Hosting.Maui.Utilities;
@@ -38,7 +41,8 @@ internal sealed class MauiiOSEnvironmentProcessedAnnotation : IResourceAnnotatio
3841
internal sealed class MauiiOSEnvironmentSubscriber(
3942
DistributedApplicationExecutionContext executionContext,
4043
ResourceLoggerService loggerService,
41-
ResourceNotificationService notificationService) : IDistributedApplicationEventingSubscriber
44+
ResourceNotificationService notificationService,
45+
IFileSystemService fileSystemService) : IDistributedApplicationEventingSubscriber
4246
{
4347
public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext execContext, CancellationToken cancellationToken)
4448
{
@@ -83,6 +87,7 @@ private async Task OnBeforeResourceStartedAsync(BeforeResourceStartedEvent @even
8387
if (generatedFilePath is null)
8488
{
8589
generatedFilePath = await MauiEnvironmentHelper.CreateiOSEnvironmentTargetsFileAsync(
90+
fileSystemService,
8691
resource,
8792
executionContext,
8893
logger,

src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
5+
46
using Aspire.Hosting;
57
using Aspire.Hosting.ApplicationModel;
68
using Aspire.Hosting.MySql;
@@ -260,7 +262,8 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
260262
}
261263
else
262264
{
263-
var tempConfigFile = await WritePhpMyAdminConfiguration(mySqlInstances, ct).ConfigureAwait(false);
265+
var fileSystemService = e.Services.GetRequiredService<IFileSystemService>();
266+
var tempConfigFile = await WritePhpMyAdminConfiguration(fileSystemService, mySqlInstances, ct).ConfigureAwait(false);
264267

265268
try
266269
{
@@ -374,10 +377,10 @@ public static IResourceBuilder<MySqlServerResource> WithInitFiles(this IResource
374377
return builder.WithContainerFiles(initPath, importFullPath);
375378
}
376379

377-
private static async Task<string> WritePhpMyAdminConfiguration(IEnumerable<MySqlServerResource> mySqlInstances, CancellationToken cancellationToken)
380+
private static async Task<string> WritePhpMyAdminConfiguration(IFileSystemService fileSystemService, IEnumerable<MySqlServerResource> mySqlInstances, CancellationToken cancellationToken)
378381
{
379382
// This temporary file is not used by the container, it will be copied and then deleted
380-
var filePath = Path.GetTempFileName();
383+
var filePath = fileSystemService.TempDirectory.CreateTempFile().Path;
381384

382385
using var writer = new StreamWriter(filePath);
383386

src/Aspire.Hosting.Testing/DistributedApplicationTestingBuilder.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
#pragma warning disable ASPIREPIPELINES001
5+
#pragma warning disable ASPIREUSERSECRETS001
6+
#pragma warning disable ASPIREFILESYSTEM001
57

68
using System.Diagnostics;
79
using System.Diagnostics.CodeAnalysis;
810
using System.Reflection;
911
using Aspire.Hosting.ApplicationModel;
1012
using Aspire.Hosting.Eventing;
1113
using Aspire.Hosting.Pipelines;
14+
using Aspire.Hosting.UserSecrets;
1215
using Microsoft.Extensions.Configuration;
1316
using Microsoft.Extensions.DependencyInjection;
1417
using Microsoft.Extensions.Hosting;
@@ -244,6 +247,8 @@ private sealed class Builder(SuspendingDistributedApplicationFactory factory, Di
244247

245248
public IDistributedApplicationPipeline Pipeline => innerBuilder.Pipeline;
246249

250+
public IUserSecretsManager UserSecretsManager => innerBuilder.UserSecretsManager;
251+
247252
public IResourceBuilder<T> AddResource<T>(T resource) where T : IResource => innerBuilder.AddResource(resource);
248253

249254
public DistributedApplication Build() => BuildAsync(CancellationToken.None).Result;
@@ -396,6 +401,8 @@ static Assembly FindApplicationAssembly()
396401

397402
public IDistributedApplicationPipeline Pipeline => _innerBuilder.Pipeline;
398403

404+
public IUserSecretsManager UserSecretsManager => _innerBuilder.UserSecretsManager;
405+
399406
public IResourceBuilder<T> AddResource<T>(T resource) where T : IResource => _innerBuilder.AddResource(resource);
400407

401408
[MemberNotNull(nameof(_app))]
@@ -486,6 +493,12 @@ public interface IDistributedApplicationTestingBuilder : IDistributedApplication
486493
/// <inheritdoc cref="IDistributedApplicationBuilder.Resources" />
487494
new IResourceCollection Resources => ((IDistributedApplicationBuilder)this).Resources;
488495

496+
/// <inheritdoc cref="IDistributedApplicationBuilder.FileSystemService" />
497+
new IFileSystemService FileSystemService => ((IDistributedApplicationBuilder)this).FileSystemService;
498+
499+
/// <inheritdoc cref="IDistributedApplicationBuilder.UserSecretsManager" />
500+
new IUserSecretsManager UserSecretsManager => ((IDistributedApplicationBuilder)this).UserSecretsManager;
501+
489502
/// <inheritdoc cref="IDistributedApplicationBuilder.AddResource{T}(T)" />
490503
new IResourceBuilder<T> AddResource<T>(T resource) where T : IResource => ((IDistributedApplicationBuilder)this).AddResource(resource);
491504

src/Aspire.Hosting/ApplicationModel/AspireStore.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
5+
46
using System.IO.Hashing;
57
using Aspire.Hosting.Utils;
68

@@ -11,22 +13,26 @@ internal sealed class AspireStore : IAspireStore
1113
internal const string AspireStorePathKeyName = "Aspire:Store:Path";
1214

1315
private readonly string _basePath;
16+
private readonly IFileSystemService _directoryService;
1417

1518
/// <summary>
1619
/// Initializes a new instance of the <see cref="AspireStore"/> class with the specified base path.
1720
/// </summary>
1821
/// <param name="basePath">The base path for the store.</param>
22+
/// <param name="directoryService">The directory service for creating temp directories.</param>
1923
/// <returns>A new instance of <see cref="AspireStore"/>.</returns>
20-
public AspireStore(string basePath)
24+
public AspireStore(string basePath, IFileSystemService directoryService)
2125
{
2226
ArgumentNullException.ThrowIfNull(basePath);
27+
ArgumentNullException.ThrowIfNull(directoryService);
2328

2429
if (!Path.IsPathRooted(basePath))
2530
{
2631
throw new ArgumentException($"An absolute path is required: '${basePath}'", nameof(basePath));
2732
}
2833

2934
_basePath = basePath;
35+
_directoryService = directoryService;
3036
EnsureDirectory();
3137
}
3238

@@ -43,7 +49,7 @@ public string GetFileNameWithContent(string filenameTemplate, Stream contentStre
4349
filenameTemplate = Path.GetFileName(filenameTemplate);
4450

4551
// Create a temporary file to write the content to.
46-
var tempFileName = Path.GetTempFileName();
52+
var tempFileName = _directoryService.TempDirectory.CreateTempFile().Path;
4753

4854
// Fast, non-cryptographic hash.
4955
var hash = new XxHash3();

src/Aspire.Hosting/ApplicationModel/ProjectResource.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma warning disable ASPIREDOCKERFILEBUILDER001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
55
#pragma warning disable ASPIRECONTAINERRUNTIME001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
66
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
7+
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
78

89
// Licensed to the .NET Foundation under one or more agreements.
910
// The .NET Foundation licenses this file to you under the MIT license.
@@ -137,9 +138,10 @@ private async Task BuildProjectImage(PipelineStepContext ctx)
137138
// Add COPY --from: statements for each source
138139
stage.AddContainerFiles(this, containerWorkingDir, logger);
139140

140-
// Write the Dockerfile to a temporary location
141+
// Get the directory service to create temp Dockerfile
141142
var projectDir = Path.GetDirectoryName(projectMetadata.ProjectPath)!;
142-
var tempDockerfilePath = Path.GetTempFileName();
143+
var directoryService = ctx.Services.GetRequiredService<IFileSystemService>();
144+
var tempDockerfilePath = directoryService.TempDirectory.CreateTempFile().Path;
143145

144146
var builtSuccessfully = false;
145147
try

src/Aspire.Hosting/ApplicationModel/UserSecretsParameterDefault.cs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREUSERSECRETS001
5+
46
using System.Diagnostics;
5-
using System.Reflection;
67
using Aspire.Hosting.Publishing;
78
using Aspire.Hosting.UserSecrets;
89

@@ -11,27 +12,20 @@ namespace Aspire.Hosting.ApplicationModel;
1112
/// <summary>
1213
/// Wraps a <see cref="ParameterDefault"/> such that the default value is saved to the project's user secrets store.
1314
/// </summary>
14-
/// <param name="appHostAssembly">The app host assembly.</param>
1515
/// <param name="applicationName">The application name.</param>
1616
/// <param name="parameterName">The parameter name.</param>
1717
/// <param name="parameterDefault">The <see cref="ParameterDefault"/> that will produce the default value when it isn't found in the project's user secrets store.</param>
18-
/// <param name="factory">The factory to use for creating user secrets managers.</param>
19-
internal sealed class UserSecretsParameterDefault(Assembly appHostAssembly, string applicationName, string parameterName, ParameterDefault parameterDefault, UserSecretsManagerFactory factory)
18+
/// <param name="userSecretsManager">The user secrets manager.</param>
19+
internal sealed class UserSecretsParameterDefault(string applicationName, string parameterName, ParameterDefault parameterDefault, IUserSecretsManager userSecretsManager)
2020
: ParameterDefault
2121
{
22-
public UserSecretsParameterDefault(Assembly appHostAssembly, string applicationName, string parameterName, ParameterDefault parameterDefault)
23-
: this(appHostAssembly, applicationName, parameterName, parameterDefault, UserSecretsManagerFactory.Instance)
24-
{
25-
}
26-
2722
/// <inheritdoc/>
2823
public override string GetDefaultValue()
2924
{
3025
var value = parameterDefault.GetDefaultValue();
3126
var configurationKey = $"Parameters:{parameterName}";
32-
33-
var manager = factory.GetOrCreate(appHostAssembly);
34-
if (!manager.TrySetSecret(configurationKey, value))
27+
28+
if (!userSecretsManager.TrySetSecret(configurationKey, value))
3529
{
3630
// This is a best-effort operation, so we don't throw if it fails. Common reason for failure is that the user secrets ID is not set
3731
// in the application's assembly. Note there's no ILogger available this early in the application lifecycle.

0 commit comments

Comments
 (0)