1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
+ using System . Collections ;
4
5
using System . Diagnostics . CodeAnalysis ;
5
6
6
7
namespace Aspire . Hosting ;
@@ -80,9 +81,9 @@ public interface IInteractionService
80
81
/// <param name="options">Optional configuration for the input dialog interaction.</param>
81
82
/// <param name="cancellationToken">A token to cancel the operation.</param>
82
83
/// <returns>
83
- /// An <see cref="InteractionResult{T}"/> containing the user's inputs.
84
+ /// An <see cref="InteractionResult{T}"/> containing the user's inputs as an <see cref="InteractionInputCollection"/> .
84
85
/// </returns>
85
- Task < InteractionResult < IReadOnlyList < InteractionInput > > > PromptInputsAsync ( string title , string ? message , IReadOnlyList < InteractionInput > inputs , InputsDialogInteractionOptions ? options = null , CancellationToken cancellationToken = default ) ;
86
+ Task < InteractionResult < InteractionInputCollection > > PromptInputsAsync ( string title , string ? message , IReadOnlyList < InteractionInput > inputs , InputsDialogInteractionOptions ? options = null , CancellationToken cancellationToken = default ) ;
86
87
87
88
/// <summary>
88
89
/// Prompts the user with a notification.
@@ -103,6 +104,12 @@ public interface IInteractionService
103
104
[ Experimental ( InteractionService . DiagnosticId , UrlFormat = "https://aka.ms/aspire/diagnostics/{0}" ) ]
104
105
public sealed class InteractionInput
105
106
{
107
+ /// <summary>
108
+ /// Gets or sets the name for the input. Used for accessing inputs by name from a keyed collection.
109
+ /// If not specified, a name will be generated automatically.
110
+ /// </summary>
111
+ public string ? Name { get ; init ; }
112
+
106
113
/// <summary>
107
114
/// Gets or sets the label for the input.
108
115
/// </summary>
@@ -164,6 +171,183 @@ public int? MaxLength
164
171
internal List < string > ValidationErrors { get ; } = [ ] ;
165
172
}
166
173
174
+ /// <summary>
175
+ /// A collection of interaction inputs that supports both indexed and name-based access.
176
+ /// </summary>
177
+ [ Experimental ( InteractionService . DiagnosticId , UrlFormat = "https://aka.ms/aspire/diagnostics/{0}" ) ]
178
+ public sealed class InteractionInputCollection : IReadOnlyList < InteractionInput >
179
+ {
180
+ private readonly IReadOnlyList < InteractionInput > _inputs ;
181
+ private readonly IReadOnlyDictionary < string , InteractionInput > _inputsByName ;
182
+
183
+ internal InteractionInputCollection ( IReadOnlyList < InteractionInput > inputs )
184
+ {
185
+ // Create a new list with proper names assigned
186
+ var processedInputs = new List < InteractionInput > ( ) ;
187
+ var inputsByName = new Dictionary < string , InteractionInput > ( StringComparer . OrdinalIgnoreCase ) ;
188
+ var usedNames = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
189
+
190
+ // First pass: collect explicit names and check for duplicates
191
+ foreach ( var input in inputs )
192
+ {
193
+ if ( ! string . IsNullOrWhiteSpace ( input . Name ) )
194
+ {
195
+ if ( usedNames . Contains ( input . Name ) )
196
+ {
197
+ throw new InvalidOperationException ( $ "Duplicate input name '{ input . Name } ' found. Input names must be unique.") ;
198
+ }
199
+ usedNames . Add ( input . Name ) ;
200
+ }
201
+ }
202
+
203
+ // Second pass: create new inputs with generated names where needed
204
+ for ( int i = 0 ; i < inputs . Count ; i ++ )
205
+ {
206
+ var input = inputs [ i ] ;
207
+ string finalName ;
208
+
209
+ if ( ! string . IsNullOrWhiteSpace ( input . Name ) )
210
+ {
211
+ finalName = input . Name ;
212
+ }
213
+ else
214
+ {
215
+ // Generate a unique name based on the label or index
216
+ string baseName = GenerateBaseName ( input . Label ) ;
217
+ finalName = baseName ;
218
+ int suffix = 1 ;
219
+
220
+ while ( usedNames . Contains ( finalName ) )
221
+ {
222
+ finalName = $ "{ baseName } _{ suffix } ";
223
+ suffix ++ ;
224
+ }
225
+
226
+ usedNames . Add ( finalName ) ;
227
+ }
228
+
229
+ // Create a new input with the final name if it was generated
230
+ var finalInput = string . IsNullOrWhiteSpace ( input . Name ) ?
231
+ new InteractionInput
232
+ {
233
+ Name = finalName ,
234
+ Label = input . Label ,
235
+ Description = input . Description ,
236
+ EnableDescriptionMarkdown = input . EnableDescriptionMarkdown ,
237
+ InputType = input . InputType ,
238
+ Required = input . Required ,
239
+ Options = input . Options ,
240
+ Value = input . Value ,
241
+ Placeholder = input . Placeholder ,
242
+ MaxLength = input . MaxLength
243
+ } : input ;
244
+
245
+ processedInputs . Add ( finalInput ) ;
246
+ inputsByName [ finalName ] = finalInput ;
247
+ }
248
+
249
+ _inputs = processedInputs ;
250
+ _inputsByName = inputsByName ;
251
+ }
252
+
253
+ private static string GenerateBaseName ( string label )
254
+ {
255
+ if ( string . IsNullOrWhiteSpace ( label ) )
256
+ {
257
+ return "Input" ;
258
+ }
259
+
260
+ // Convert to a valid identifier-like name
261
+ var chars = label . ToCharArray ( ) ;
262
+ var result = new System . Text . StringBuilder ( ) ;
263
+
264
+ for ( int i = 0 ; i < chars . Length ; i ++ )
265
+ {
266
+ char c = chars [ i ] ;
267
+ if ( char . IsLetterOrDigit ( c ) )
268
+ {
269
+ result . Append ( c ) ;
270
+ }
271
+ else if ( result . Length > 0 && result [ result . Length - 1 ] != '_' )
272
+ {
273
+ result . Append ( '_' ) ;
274
+ }
275
+ }
276
+
277
+ // Ensure we have a valid name
278
+ string name = result . ToString ( ) . Trim ( '_' ) ;
279
+ return string . IsNullOrEmpty ( name ) ? "Input" : name ;
280
+ }
281
+
282
+ /// <summary>
283
+ /// Gets an input by its name.
284
+ /// </summary>
285
+ /// <param name="name">The name of the input.</param>
286
+ /// <returns>The input with the specified name.</returns>
287
+ /// <exception cref="KeyNotFoundException">Thrown when no input with the specified name exists.</exception>
288
+ public InteractionInput this [ string name ]
289
+ {
290
+ get
291
+ {
292
+ if ( _inputsByName . TryGetValue ( name , out var input ) )
293
+ {
294
+ return input ;
295
+ }
296
+ throw new KeyNotFoundException ( $ "No input with name '{ name } ' was found.") ;
297
+ }
298
+ }
299
+
300
+ /// <summary>
301
+ /// Gets an input by its index.
302
+ /// </summary>
303
+ /// <param name="index">The zero-based index of the input.</param>
304
+ /// <returns>The input at the specified index.</returns>
305
+ public InteractionInput this [ int index ] => _inputs [ index ] ;
306
+
307
+ /// <summary>
308
+ /// Gets the number of inputs in the collection.
309
+ /// </summary>
310
+ public int Count => _inputs . Count ;
311
+
312
+ /// <summary>
313
+ /// Tries to get an input by its name.
314
+ /// </summary>
315
+ /// <param name="name">The name of the input.</param>
316
+ /// <param name="input">When this method returns, contains the input with the specified name, if found; otherwise, null.</param>
317
+ /// <returns>true if an input with the specified name was found; otherwise, false.</returns>
318
+ public bool TryGetByName ( string name , out InteractionInput ? input )
319
+ {
320
+ return _inputsByName . TryGetValue ( name , out input ) ;
321
+ }
322
+
323
+ /// <summary>
324
+ /// Determines whether the collection contains an input with the specified name.
325
+ /// </summary>
326
+ /// <param name="name">The name to locate in the collection.</param>
327
+ /// <returns>true if the collection contains an input with the specified name; otherwise, false.</returns>
328
+ public bool ContainsName ( string name )
329
+ {
330
+ return _inputsByName . ContainsKey ( name ) ;
331
+ }
332
+
333
+ /// <summary>
334
+ /// Gets the names of all inputs in the collection.
335
+ /// </summary>
336
+ public IEnumerable < string > Names => _inputsByName . Keys ;
337
+
338
+ /// <summary>
339
+ /// Returns an enumerator that iterates through the collection.
340
+ /// </summary>
341
+ /// <returns>An enumerator that can be used to iterate through the collection.</returns>
342
+ public IEnumerator < InteractionInput > GetEnumerator ( ) => _inputs . GetEnumerator ( ) ;
343
+
344
+ /// <summary>
345
+ /// Returns an enumerator that iterates through the collection.
346
+ /// </summary>
347
+ /// <returns>An enumerator that can be used to iterate through the collection.</returns>
348
+ IEnumerator IEnumerable . GetEnumerator ( ) => _inputs . GetEnumerator ( ) ;
349
+ }
350
+
167
351
/// <summary>
168
352
/// Specifies the type of input for an <see cref="InteractionInput"/>.
169
353
/// </summary>
@@ -217,7 +401,7 @@ public sealed class InputsDialogValidationContext
217
401
/// <summary>
218
402
/// Gets the inputs that are being validated.
219
403
/// </summary>
220
- public required IReadOnlyList < InteractionInput > Inputs { get ; init ; }
404
+ public required InteractionInputCollection Inputs { get ; init ; }
221
405
222
406
/// <summary>
223
407
/// Gets the cancellation token for the validation operation.
0 commit comments