Refactor user secrets management to use DI-based factory pattern with thread-safe synchronization#12482
Refactor user secrets management to use DI-based factory pattern with thread-safe synchronization#12482
Conversation
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
…ites Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
|
@copilot Do we need a semaphore slim ? We can just use a dictionary of locks right |
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 12482Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 12482" |
|
@copilot We have one more problem, user secrets are being written to by another system in aspire the UserSecretsDeploymentStateManager @dotnet/aspire/files/src/Aspire.Hosting/Publishing/Internal/UserSecretsDeploymentStateManager.cs . We need to make sure that there's a single source of truth for synchronization for these files within the same process. The SecretStore and UserSecretsDeploymentStateManager need to synchronize access to the file and make sure that writes are serialized. |
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Created |
|
@copilot The implementation does not look great. Base UserSecretsFileLock on a SemaphoreSlim instead of a lock object. Make it possible for DeploymentStateManagerBase to overrride the synchronization of the file and have UserSecretsDeploymentStateManager use the same SemaphoreSlim based on the StateFilePath. SecretsStore should expose the same SemaphoreSlim instance to synchronize access. |
…erride Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Refactored the implementation based on feedback:
Both systems now use the same |
| var json = flattenedUserSecrets.ToJsonString(s_jsonSerializerOptions); | ||
|
|
||
| // Write synchronously to avoid async/await in the lock that's managed by the base class | ||
| await Task.Run(() => |
There was a problem hiding this comment.
Removed Task.Run and replaced with direct File.WriteAllTextAsync call. Since the base class manages synchronization via the StateLock semaphore, we can use async file operations directly. (482e759)
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Implemented as requested. |
|
@copilot var result = DeploymentStateManagerBase.FlattenJsonObject(userSecrets); The azure tests are failing with this, but they don't need to us that type. |
Extracted
Extracted |
…test dependency Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
|
@copilot A set of tests are breaking that look like this: failed Aspire.Hosting.MongoDB.Tests.MongoDbFunctionalTests.VerifyMongoDBResource (5ms) Look at the failing tests in the action https://github.com/dotnet/aspire/actions/runs/18967342489/job/54247686436?pr=12482 and fix them |
…ps without UserSecretsId Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Fixed the DI resolution issue by making |
|
@copilot If there's no user secrets id for the assembly then we should return a NoopIUserSecretsManager that logs a warning. That way callers don't need to handle null vs not null, we can tell the user via a log that user secrets is not enabled for this project. |
…warning logs Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Implemented
Implemented
Implemented |
|
@copilot src/Aspire.Hosting/UserSecrets/NoopUserSecretsManager.cs(12,48): error CS0535: (NETCORE_ENGINEERING_TELEMETRY=Build) 'NoopUserSecretsManager' does not implement interface member 'IUserSecretsManager.FilePath' |
…face members Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
|
|
||
| private readonly DistributedApplicationOptions _options; | ||
| private readonly HostApplicationBuilder _innerBuilder; | ||
| private readonly IUserSecretsManager? _userSecretsManager; |
Fix SecretsStore Concurrent Access Issue
This PR fixes the issue where multiple auto-generated secrets (e.g., SQL Server and RabbitMQ passwords) are not properly saved to user secrets because of concurrent access issues. The fix implements a complete architectural refactoring from static methods to a dependency injection-based factory pattern with proper synchronization.
Solution
Complete refactoring to DI-based architecture with:
IUserSecretsManagerinterface - Internal interface withIsAvailableproperty to check if user secrets are configuredUserSecretsManagerFactory- Singleton factory withConditionalWeakTablecaching and semaphore locking; returnsNoopUserSecretsManagerfor apps without UserSecretsId (never cached)NoopUserSecretsManager- Null object pattern that logs one warning in constructor, debug messages for operationsIUserSecretsManagerIUserSecretsManagervia constructorJsonFlattenerstatic class for reuseSemaphoreSlimlockingTechnical Details
NoopUserSecretsManagerwithIsAvailable = false, minimal logging (one warning in constructor, debug for operations)IUserSecretsManagervia DI or factoryNoopUserSecretsManageris never cached by factoryConditionalWeakTableensures real instances are reusedSemaphoreSlimlockingTest Updates
Tests assert manager is non-null
All UserSecrets tests pass (including 3 new concurrent write tests)
All functional tests resolve IUserSecretsManager properly
Fixes Auto generated secrets do not get written to the user secrets #12481
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.