1+ // Copyright (c) .NET Foundation and contributors. All rights reserved.
2+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+ using System . Diagnostics . CodeAnalysis ;
5+ using System . Runtime . CompilerServices ;
6+
7+ namespace System . CommandLine . Subsystems . Annotations ;
8+
9+ /// <summary>
10+ /// Handles storage of annotations associated with <see cref="CliSymbol"/> instances.
11+ /// </summary>
12+ public static partial class AnnotationStorageExtensions
13+ {
14+ // CliSymbol does not offer any PropertyBag-like storage of arbitrary annotations, so the only way to allow setting
15+ // subsystem-specific annotations on CliSymbol instances (such as help description, default value, etc) via simple
16+ // extension methods is to use a static field with a dictionary that associates annotations with CliSymbol instances.
17+ //
18+ // Using ConditionalWeakTable for this dictionary ensures that the symbols and annotations can be collected when the
19+ // symbols are no longer reachable. Although this is unlikely to happen in a CLI app, it is important not to create
20+ // unexpected, unfixable, unbounded memory leaks in apps that construct multiple grammars/pipelines.
21+ //
22+ // The main use case for System.CommandLine is for a CLI app to construct a single annotated grammar in its entry point,
23+ // construct a pipeline using that grammar, and use the pipeline/grammar only once to parse its arguments. However, it
24+ // is important to have well defined and reasonable threading behavior so that System.CommandLine does not behave in
25+ // surprising ways when used in more advanced cases:
26+ //
27+ // * There may be multiple threads constructing and using completely independent grammars/pipelines. This happens in
28+ // our own unit tests, but might happen e.g. in a multithreaded data processing app or web service that uses
29+ // System.CommandLine to process inputs.
30+ //
31+ // * The grammar/pipeline are reentrant; they do not store they do not store internal state, and may be used to parse
32+ // input multiple times. As this is the case, it is reasonable to expect a grammar/pipeline instance to be
33+ // constructed in one thread then used in multiple threads. This might be done by the aforementioned web service or
34+ // data processing app.
35+ //
36+ // The thread-safe behavior of ConditionalWeakTable ensures this works as expected without us having to worry about
37+ // taking locks directly, even though the instance is on a static field and shared between all threads. Note that
38+ // thread local storage is not useful for this, as that would create unexpected behaviors where a grammar constructed
39+ // in one thread would be missing its annotations when used in another thread.
40+ //
41+ // However, while getting values from ConditionalWeakTable is lock free, setting values internally uses an expensive
42+ // lock, so it is not ideal to store all individual annotations directly in the ConditionalWeakTable. This is especially
43+ // true as we do not want the common case of the CLI app entrypoint to have its performance impacted by multithreading
44+ // support more than absolutely necessary.
45+ //
46+ // Instead, we have a single static ConditionalWeakTable that maps each CliSymbol to an AnnotationStorage dictionary,
47+ // which is lazily created and added to the ConditionalWeakTable a single time for each CliSymbol. The individual
48+ // annotations are stored in the AnnotationStorage dictionary, which uses no locks, so is fast, but is not safe to be
49+ // modified from multiple threads.
50+ //
51+ // This is fine, as we will have the following well-defined threading behavior: an annotated grammar and pipeline may
52+ // only be constructed/modified from a single thread. Once the grammar/pipeline instance is fully constructed, it may
53+ // be safely used from multiple threads.
54+
55+ static readonly ConditionalWeakTable < CliSymbol , AnnotationStorage > symbolToAnnotationStorage = new ( ) ;
56+
57+ /// <summary>
58+ /// Sets the value for the annotation <paramref name="id"/> associated with the <paramref name="symbol"/> in the internal annotation storage.
59+ /// </summary>
60+ /// <typeparam name="TValue">The type of the annotation value</typeparam>
61+ /// <param name="symbol">The symbol that is annotated</param>
62+ /// <param name="id">
63+ /// The identifier for the annotation. For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
64+ /// </param>
65+ /// <param name="value">The annotation value</param>
66+ public static void SetAnnotation < TValue > ( this CliSymbol symbol , AnnotationId < TValue > annotationId , TValue value )
67+ {
68+ var storage = symbolToAnnotationStorage . GetValue ( symbol , static ( CliSymbol _ ) => new AnnotationStorage ( ) ) ;
69+ storage . Set ( symbol , annotationId , value ) ;
70+ }
71+
72+ /// <summary>
73+ /// Sets the value for the annotation <paramref name="id"/> associated with the <paramref name="symbol"/> in the internal annotation storage,
74+ /// and returns the <paramref name="symbol"> to enable fluent construction of symbols with annotations.
75+ /// </summary>
76+ /// <typeparam name="TValue">The type of the annotation value</typeparam>
77+ /// <param name="symbol">The symbol that is annotated</param>
78+ /// <param name="id">
79+ /// The identifier for the annotation. For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
80+ /// </param>
81+ /// <param name="value">The annotation value</param>
82+ public static TSymbol WithAnnotation < TSymbol , TValue > ( this TSymbol symbol , AnnotationId < TValue > annotationId , TValue value ) where TSymbol : CliSymbol
83+ {
84+ symbol . SetAnnotation ( annotationId , value ) ;
85+ return symbol ;
86+ }
87+
88+ /// <summary>
89+ /// Attempts to get the value for the annotation <paramref name="id"/> associated with the <paramref name="symbol"/>,
90+ /// first from the optional <paramref name="provider"/>, and falling back to the internal annotation storage used to
91+ /// store values set via <see cref="SetAnnotation{TValue}(CliSymbol, AnnotationId{TValue}, TValue)"/>.
92+ /// </summary>
93+ /// <typeparam name="TValue">The type of the annotation value</typeparam>
94+ /// <param name="symbol">The symbol that is annotated</param>
95+ /// <param name="id">
96+ /// The identifier for the annotation. For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
97+ /// </param>
98+ /// <param name="value">The annotation value, if successful, otherwise <c>default</c></param>
99+ /// <param name="provider">
100+ /// An optional annotation provider that may implement custom or lazy construction of annotation values. Annotation returned by an annotation
101+ /// provider take precedence over those stored in internal annotation storage.
102+ /// </param>
103+ /// <returns>True if successful</returns>
104+ public static bool TryGetAnnotation < TValue > ( this CliSymbol symbol , AnnotationId < TValue > annotationId , [ NotNullWhen ( true ) ] out TValue ? value , IAnnotationProvider ? provider = null )
105+ {
106+ if ( provider is not null && provider . TryGet ( symbol , annotationId , out value ) )
107+ {
108+ return true ;
109+ }
110+
111+ if ( symbolToAnnotationStorage . TryGetValue ( symbol , out var storage ) && storage . TryGet ( symbol , annotationId , out value ) )
112+ {
113+ return true ;
114+ }
115+
116+ value = default ;
117+ return false ;
118+ }
119+ /// <summary>
120+ /// Attempt to retrieve the <paramref name="symbol"/>'s value for the annotation <paramref name="id"/>
121+ /// from the optional <paramref name="provider"/> and the internal annotation storage.
122+ /// </summary>
123+ /// <typeparam name="TValue">The type of the annotation value</typeparam>
124+ /// <param name="symbol">The symbol that is annotated</param>
125+ /// <param name="id">
126+ /// The identifier for the annotation. For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
127+ /// </param>
128+ /// <param name="provider">
129+ /// An optional annotation provider that may implement custom or lazy construction of annotation values. Annotation returned by an annotation
130+ /// provider take precedence over those stored in internal annotation storage.
131+ /// </param>
132+ /// <returns>The annotation value, if successful, otherwise <c>default</c></returns>
133+ public static TValue ? GetAnnotationOrDefault < TValue > ( this CliSymbol symbol , AnnotationId < TValue > annotationId , IAnnotationProvider ? provider = null )
134+ {
135+ if ( symbol . TryGetAnnotation ( annotationId , out TValue ? value , provider ) )
136+ {
137+ return value ;
138+ }
139+
140+ return default ;
141+ }
142+ }
0 commit comments