Refactor user secrets to DI-based factory with thread-safe synchronization#12732
Conversation
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
|
Discussed offline. Approved. @davidfowl please let me know when this is ready to go. |
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 12732Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 12732" |
There was a problem hiding this comment.
Pull Request Overview
This PR refactors the user secrets management infrastructure to fix concurrency issues and improve modularity. The main issue addressed is that concurrent writes to user secrets (e.g., when multiple resources generate passwords simultaneously) can result in data loss due to race conditions.
Key changes:
- Replaced the static
SecretsStoreclass with a new factory-basedIUserSecretsManagerinterface and implementation that uses aSemaphoreSlimfor thread-safe file operations - Moved JSON flattening logic from
DeploymentStateManagerBaseto a dedicatedJsonFlattenerutility class for better code organization - Updated all consumers of
SecretsStoreto use the newIUserSecretsManagerabstraction
Reviewed Changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/Shared/SecretsStore.cs |
Deleted the old SecretsStore class that had concurrency issues |
src/Aspire.Hosting/UserSecrets/UserSecretsManagerFactory.cs |
New factory class that creates and caches thread-safe IUserSecretsManager instances using SemaphoreSlim for synchronization |
src/Aspire.Hosting/UserSecrets/IUserSecretsManager.cs |
New interface defining the contract for user secrets operations |
src/Aspire.Hosting/UserSecrets/NoopUserSecretsManager.cs |
New no-op implementation used when user secrets are not configured |
src/Aspire.Hosting/Pipelines/Internal/JsonFlattener.cs |
Extracted JSON flattening logic into a dedicated utility class |
src/Aspire.Hosting/Pipelines/Internal/UserSecretsDeploymentStateManager.cs |
Updated to use IUserSecretsManager instead of direct file operations |
src/Aspire.Hosting/Pipelines/Internal/FileDeploymentStateManager.cs |
Updated to use JsonFlattener instead of the base class method |
src/Aspire.Hosting/Pipelines/Internal/DeploymentStateManagerBase.cs |
Removed JSON flattening methods (moved to JsonFlattener) |
src/Aspire.Hosting/DistributedApplicationBuilder.cs |
Updated to create and register IUserSecretsManager using the factory |
src/Aspire.Hosting/ApplicationModel/UserSecretsParameterDefault.cs |
Updated to use UserSecretsManagerFactory with optional factory parameter for testability |
src/Aspire.Hosting/VersionChecking/VersionCheckService.cs |
Updated to use IUserSecretsManager instead of static SecretsStore methods |
src/Aspire.Hosting/Aspire.Hosting.csproj |
Removed the link to the deleted SecretsStore.cs file |
tests/Aspire.Hosting.Tests/UserSecretsParameterDefaultTests.cs |
Added comprehensive concurrency tests and updated existing tests to use the new API |
tests/Aspire.Hosting.Tests/SecretsStoreTests.cs |
Updated tests to use IUserSecretsManager |
tests/Aspire.Hosting.Tests/JsonFlattenerTests.cs |
Updated tests to use JsonFlattener static class |
tests/Aspire.Hosting.Tests/VersionChecking/VersionCheckServiceTests.cs |
Updated test helper to accept and pass IUserSecretsManager parameter |
| // Dictionary to cache instances by file path | ||
| private readonly Dictionary<string, IUserSecretsManager> _managerCache = new(); | ||
| private readonly object _lock = new(); | ||
|
|
There was a problem hiding this comment.
The constructor lacks XML documentation. Add a summary explaining this is internal and used for creating isolated factory instances for testing.
| /// <summary> | |
| /// Initializes a new instance of the <see cref="UserSecretsManagerFactory"/> class. | |
| /// This constructor is internal and used for creating isolated factory instances for testing. | |
| /// </summary> |
| /// <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> | ||
| internal sealed class UserSecretsParameterDefault(Assembly appHostAssembly, string applicationName, string parameterName, ParameterDefault parameterDefault) | ||
| /// <param name="factory">The factory to use for creating user secrets managers.</param> | ||
| internal sealed class UserSecretsParameterDefault(Assembly appHostAssembly, string applicationName, string parameterName, ParameterDefault parameterDefault, UserSecretsManagerFactory factory) |
There was a problem hiding this comment.
The new constructor parameter factory is documented in the param tag, but the class-level summary on line 12-17 should be updated to mention that multiple constructors exist and explain when to use the factory parameter overload.
| var userSecretsId = assembly?.GetCustomAttribute<UserSecretsIdAttribute>()?.UserSecretsId; | ||
| return GetOrCreateFromId(userSecretsId); | ||
| } | ||
|
|
There was a problem hiding this comment.
The nested UserSecretsManager class lacks XML documentation. Add a summary explaining it's the concrete implementation that provides thread-safe file operations via SemaphoreSlim.
| /// <summary> | |
| /// Concrete <see cref="IUserSecretsManager"/> implementation that provides thread-safe file operations using <see cref="SemaphoreSlim"/>. | |
| /// </summary> |
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Extra blank line at the end of the file should be removed according to standard formatting conventions.
Description
Ports PR #12692 to release/13.0. Replaces static
SecretsStoremethods with a dependency injection-based factory pattern that provides thread-safe synchronization for user secrets file access.Architecture Changes
New Components:
IUserSecretsManager- interface for user secrets operationsUserSecretsManagerFactory- singleton factory with lock-based instance cachingNoopUserSecretsManager- null object pattern when user secrets not configuredJsonFlattener- utility for JSON flattening/unflattening (extracted fromDeploymentStateManagerBase)Thread Safety:
UserSecretsManagerinstance has its ownSemaphoreSlimCore Changes
DistributedApplicationBuilder- registersIUserSecretsManagersingleton in DI containerUserSecretsParameterDefault- factory-based constructor with optional custom factory for test isolationUserSecretsDeploymentStateManager- injectsIUserSecretsManagervia constructorVersionCheckService- injectsIUserSecretsManagerinstead of using static methodssrc/Shared/SecretsStore.csand static method callsTest Updates
JsonFlattenerTestsfrom Azure.Tests to Aspire.Hosting.TestsBefore:
After:
Checklist
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.