55using System . Reflection . Emit ;
66using System . Text ;
77using Aspire . Hosting . Publishing ;
8+ using Aspire . Hosting . Publishing . Internal ;
9+ using Aspire . Hosting . UserSecrets ;
10+ using Microsoft . Extensions . Configuration ;
811using Microsoft . Extensions . Configuration . UserSecrets ;
9- using Microsoft . Extensions . SecretManager . Tools . Internal ;
1012
1113namespace Aspire . Hosting . Tests ;
1214
@@ -49,7 +51,7 @@ public void UserSecretsParameterDefault_GetDefaultValue_DoesntThrowIfSecretsFile
4951 {
5052 var userSecretsId = Guid . NewGuid ( ) . ToString ( "N" ) ;
5153 DeleteUserSecretsFile ( userSecretsId ) ;
52- var userSecretsPath = PathHelper . GetSecretsPathFromSecretsId ( userSecretsId ) ;
54+ var userSecretsPath = UserSecretsPathHelper . GetSecretsPathFromSecretsId ( userSecretsId ) ;
5355 if ( File . Exists ( userSecretsPath ) )
5456 {
5557 File . Delete ( userSecretsPath ) ;
@@ -71,6 +73,123 @@ public void UserSecretsParameterDefault_GetDefaultValue_DoesntThrowIfSecretsFile
7173 var _ = userSecretDefault . GetDefaultValue ( ) ;
7274 }
7375
76+ [ Fact ]
77+ public async Task TrySetUserSecret_ConcurrentWrites_PreservesAllSecrets ( )
78+ {
79+ var userSecretsId = Guid . NewGuid ( ) . ToString ( "N" ) ;
80+ ClearUsersSecrets ( userSecretsId ) ;
81+
82+ var testAssembly = AssemblyBuilder . DefineDynamicAssembly (
83+ new ( "TestAssembly" ) , AssemblyBuilderAccess . RunAndCollect , [ new CustomAttributeBuilder ( s_userSecretsIdAttrCtor , [ userSecretsId ] ) ] ) ;
84+
85+ // Simulate concurrent writes from multiple threads (like SQL Server and RabbitMQ generating passwords)
86+ var tasks = new List < Task < bool > > ( ) ;
87+ var secretsToWrite = new Dictionary < string , string >
88+ {
89+ [ "Parameters:sqlserver-password" ] = "SqlPassword123!" ,
90+ [ "Parameters:rabbitmq-password" ] = "RabbitPassword456!" ,
91+ [ "Parameters:redis-password" ] = "RedisPassword789!" ,
92+ [ "Parameters:postgres-password" ] = "PostgresPassword012!" ,
93+ } ;
94+
95+ foreach ( var kvp in secretsToWrite )
96+ {
97+ var key = kvp . Key ;
98+ var value = kvp . Value ;
99+ tasks . Add ( Task . Run ( ( ) =>
100+ {
101+ var manager = UserSecretsManagerFactory . Instance . Create ( testAssembly ) ;
102+ return manager ? . TrySetSecret ( key , value ) ?? false ;
103+ } ) ) ;
104+ }
105+
106+ var results = await Task . WhenAll ( tasks ) ;
107+
108+ // All writes should succeed
109+ Assert . All ( results , Assert . True ) ;
110+
111+ // All secrets should be preserved
112+ var userSecrets = GetUserSecrets ( userSecretsId ) ;
113+ foreach ( var kvp in secretsToWrite )
114+ {
115+ Assert . True ( userSecrets . ContainsKey ( kvp . Key ) , $ "Secret '{ kvp . Key } ' was not found in user secrets") ;
116+ Assert . Equal ( kvp . Value , userSecrets [ kvp . Key ] ) ;
117+ }
118+
119+ DeleteUserSecretsFile ( userSecretsId ) ;
120+ }
121+
122+ [ Fact ]
123+ public async Task TrySetUserSecret_SqlServerAndRabbitMQ_BothSecretsPreserved ( )
124+ {
125+ // This test specifically reproduces the issue described in the bug report
126+ var userSecretsId = Guid . NewGuid ( ) . ToString ( "N" ) ;
127+ ClearUsersSecrets ( userSecretsId ) ;
128+
129+ var testAssembly = AssemblyBuilder . DefineDynamicAssembly (
130+ new ( "TestAssembly" ) , AssemblyBuilderAccess . RunAndCollect , [ new CustomAttributeBuilder ( s_userSecretsIdAttrCtor , [ userSecretsId ] ) ] ) ;
131+
132+ // Simulate SQL Server and RabbitMQ generating passwords concurrently
133+ var sqlTask = Task . Run ( ( ) =>
134+ {
135+ var manager = UserSecretsManagerFactory . Instance . Create ( testAssembly ) ;
136+ return manager ? . TrySetSecret ( "Parameters:sql-password" , "SqlPassword123!" ) ?? false ;
137+ } ) ;
138+ var rabbitTask = Task . Run ( ( ) =>
139+ {
140+ var manager = UserSecretsManagerFactory . Instance . Create ( testAssembly ) ;
141+ return manager ? . TrySetSecret ( "Parameters:rabbit-password" , "RabbitPassword456!" ) ?? false ;
142+ } ) ;
143+
144+ var results = await Task . WhenAll ( sqlTask , rabbitTask ) ;
145+
146+ // Both writes should succeed
147+ Assert . All ( results , Assert . True ) ;
148+
149+ // Both secrets should be in the file
150+ var userSecrets = GetUserSecrets ( userSecretsId ) ;
151+ Assert . True ( userSecrets . ContainsKey ( "Parameters:sql-password" ) , "SQL Server password was not found" ) ;
152+ Assert . True ( userSecrets . ContainsKey ( "Parameters:rabbit-password" ) , "RabbitMQ password was not found" ) ;
153+ Assert . Equal ( "SqlPassword123!" , userSecrets [ "Parameters:sql-password" ] ) ;
154+ Assert . Equal ( "RabbitPassword456!" , userSecrets [ "Parameters:rabbit-password" ] ) ;
155+
156+ DeleteUserSecretsFile ( userSecretsId ) ;
157+ }
158+
159+ [ Fact ]
160+ public async Task TrySetUserSecret_ConcurrentWritesSameKey_LastWriteWins ( )
161+ {
162+ var userSecretsId = Guid . NewGuid ( ) . ToString ( "N" ) ;
163+ ClearUsersSecrets ( userSecretsId ) ;
164+
165+ var testAssembly = AssemblyBuilder . DefineDynamicAssembly (
166+ new ( "TestAssembly" ) , AssemblyBuilderAccess . RunAndCollect , [ new CustomAttributeBuilder ( s_userSecretsIdAttrCtor , [ userSecretsId ] ) ] ) ;
167+
168+ // Simulate concurrent writes to the same key
169+ var tasks = new List < Task < bool > > ( ) ;
170+ for ( int i = 0 ; i < 10 ; i ++ )
171+ {
172+ var value = $ "Value{ i } ";
173+ tasks . Add ( Task . Run ( ( ) =>
174+ {
175+ var manager = UserSecretsManagerFactory . Instance . Create ( testAssembly ) ;
176+ return manager ? . TrySetSecret ( "Parameters:test-key" , value ) ?? false ;
177+ } ) ) ;
178+ }
179+
180+ var results = await Task . WhenAll ( tasks ) ;
181+
182+ // All writes should succeed
183+ Assert . All ( results , Assert . True ) ;
184+
185+ // The key should exist with one of the values
186+ var userSecrets = GetUserSecrets ( userSecretsId ) ;
187+ Assert . True ( userSecrets . ContainsKey ( "Parameters:test-key" ) ) ;
188+ Assert . NotNull ( userSecrets [ "Parameters:test-key" ] ) ;
189+
190+ DeleteUserSecretsFile ( userSecretsId ) ;
191+ }
192+
74193 private static void EnsureUserSecretsDirectory ( string secretsFilePath )
75194 {
76195 var directoryName = Path . GetDirectoryName ( secretsFilePath ) ;
@@ -82,20 +201,47 @@ private static void EnsureUserSecretsDirectory(string secretsFilePath)
82201
83202 private static Dictionary < string , string ? > GetUserSecrets ( string userSecretsId )
84203 {
85- var secretsStore = new SecretsStore ( userSecretsId ) ;
86- return secretsStore . AsEnumerable ( ) . ToDictionary ( kvp => kvp . Key , kvp => kvp . Value ) ;
204+ var manager = UserSecretsManagerFactory . Instance . GetOrCreateFromId ( userSecretsId ) ;
205+ if ( manager == null )
206+ {
207+ return new Dictionary < string , string ? > ( ) ;
208+ }
209+
210+ // Read the secrets file directly
211+ var secrets = new Dictionary < string , string ? > ( ) ;
212+ if ( File . Exists ( manager . FilePath ) )
213+ {
214+ var json = File . ReadAllText ( manager . FilePath ) ;
215+ if ( ! string . IsNullOrWhiteSpace ( json ) )
216+ {
217+ var config = new ConfigurationBuilder ( )
218+ . AddJsonFile ( manager . FilePath , optional : true )
219+ . Build ( ) ;
220+
221+ foreach ( var kvp in config . AsEnumerable ( ) )
222+ {
223+ if ( kvp . Value != null )
224+ {
225+ secrets [ kvp . Key ] = kvp . Value ;
226+ }
227+ }
228+ }
229+ }
230+ return secrets ;
87231 }
88232
89233 private static void ClearUsersSecrets ( string userSecretsId )
90234 {
91- var secretsStore = new SecretsStore ( userSecretsId ) ;
92- secretsStore . Clear ( ) ;
93- secretsStore . Save ( ) ;
235+ var filePath = UserSecretsPathHelper . GetSecretsPathFromSecretsId ( userSecretsId ) ;
236+ if ( File . Exists ( filePath ) )
237+ {
238+ File . Delete ( filePath ) ;
239+ }
94240 }
95241
96242 private static void DeleteUserSecretsFile ( string userSecretsId )
97243 {
98- var userSecretsPath = PathHelper . GetSecretsPathFromSecretsId ( userSecretsId ) ;
244+ var userSecretsPath = UserSecretsPathHelper . GetSecretsPathFromSecretsId ( userSecretsId ) ;
99245 if ( File . Exists ( userSecretsPath ) )
100246 {
101247 File . Delete ( userSecretsPath ) ;
@@ -115,3 +261,4 @@ public override void WriteToManifest(ManifestPublishingContext context)
115261 }
116262 }
117263}
264+
0 commit comments