66using System . Diagnostics ;
77using System . Diagnostics . CodeAnalysis ;
88using System . Runtime . CompilerServices ;
9+ using System . Runtime . ExceptionServices ;
910using System . Text . Json . Serialization ;
1011using System . Text . Json . Serialization . Metadata ;
11- using System . Threading ;
1212
1313namespace System . Text . Json
1414{
@@ -62,13 +62,19 @@ public JsonTypeInfo GetTypeInfo(Type type)
6262 /// <summary>
6363 /// Same as GetTypeInfo but without validation and additional knobs.
6464 /// </summary>
65- internal JsonTypeInfo GetTypeInfoInternal ( Type type , bool ensureConfigured = true , bool resolveIfMutable = false )
65+ internal JsonTypeInfo GetTypeInfoInternal (
66+ Type type ,
67+ bool ensureConfigured = true ,
68+ bool resolveIfMutable = false ,
69+ bool fallBackToNearestAncestorType = false )
6670 {
71+ Debug . Assert ( ! fallBackToNearestAncestorType || IsReadOnly , "ancestor resolution should only be invoked in read-only options." ) ;
72+
6773 JsonTypeInfo ? typeInfo = null ;
6874
6975 if ( IsReadOnly )
7076 {
71- typeInfo = CacheContext . GetOrAddTypeInfo ( type ) ;
77+ typeInfo = CacheContext . GetOrAddTypeInfo ( type , fallBackToNearestAncestorType ) ;
7278 if ( ensureConfigured )
7379 {
7480 typeInfo ? . EnsureConfigured ( ) ;
@@ -95,21 +101,20 @@ internal bool TryGetTypeInfoCached(Type type, [NotNullWhen(true)] out JsonTypeIn
95101 return false ;
96102 }
97103
98- return _cachingContext . TryGetJsonTypeInfo ( type , out typeInfo ) ;
104+ return _cachingContext . TryGetTypeInfo ( type , out typeInfo ) ;
99105 }
100106
101107 /// <summary>
102108 /// Return the TypeInfo for root API calls.
103109 /// This has an LRU cache that is intended only for public API calls that specify the root type.
104110 /// </summary>
105- [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
106- internal JsonTypeInfo GetTypeInfoForRootType ( Type type )
111+ internal JsonTypeInfo GetTypeInfoForRootType ( Type type , bool fallBackToNearestAncestorType = false )
107112 {
108113 JsonTypeInfo ? jsonTypeInfo = _lastTypeInfo ;
109114
110115 if ( jsonTypeInfo ? . Type != type )
111116 {
112- _lastTypeInfo = jsonTypeInfo = GetTypeInfoInternal ( type ) ;
117+ _lastTypeInfo = jsonTypeInfo = GetTypeInfoInternal ( type , fallBackToNearestAncestorType : fallBackToNearestAncestorType ) ;
113118 }
114119
115120 return jsonTypeInfo ;
@@ -122,7 +127,7 @@ internal bool TryGetPolymorphicTypeInfoForRootType(object rootValue, [NotNullWhe
122127 Type runtimeType = rootValue . GetType ( ) ;
123128 if ( runtimeType != JsonTypeInfo . ObjectType )
124129 {
125- polymorphicTypeInfo = GetTypeInfoForRootType ( runtimeType ) ;
130+ polymorphicTypeInfo = GetTypeInfoForRootType ( runtimeType , fallBackToNearestAncestorType : true ) ;
126131 return true ;
127132 }
128133
@@ -157,39 +162,182 @@ internal void ClearCaches()
157162 /// </summary>
158163 internal sealed class CachingContext
159164 {
160- private readonly ConcurrentDictionary < Type , JsonTypeInfo ? > _jsonTypeInfoCache = new ( ) ;
165+ private readonly ConcurrentDictionary < Type , CacheEntry > _cache = new ( ) ;
161166#if ! NETCOREAPP
162- private readonly Func < Type , JsonTypeInfo ? > _jsonTypeInfoFactory ;
167+ private readonly Func < Type , CacheEntry > _cacheEntryFactory ;
163168#endif
164169
165170 public CachingContext ( JsonSerializerOptions options , int hashCode )
166171 {
167172 Options = options ;
168173 HashCode = hashCode ;
169174#if ! NETCOREAPP
170- _jsonTypeInfoFactory = Options . GetTypeInfoNoCaching ;
175+ _cacheEntryFactory = type => CreateCacheEntry ( type , this ) ;
171176#endif
172177 }
173178
174179 public JsonSerializerOptions Options { get ; }
175180 public int HashCode { get ; }
176181 // Property only accessed by reflection in testing -- do not remove.
177182 // If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date.
178- public int Count => _jsonTypeInfoCache . Count ;
183+ public int Count => _cache . Count ;
184+
185+ public JsonTypeInfo ? GetOrAddTypeInfo ( Type type , bool fallBackToNearestAncestorType = false )
186+ {
187+ CacheEntry entry = GetOrAddCacheEntry ( type ) ;
188+ return fallBackToNearestAncestorType && ! entry . HasResult
189+ ? FallBackToNearestAncestor ( type , entry )
190+ : entry . GetResult ( ) ;
191+ }
192+
193+ public bool TryGetTypeInfo ( Type type , [ NotNullWhen ( true ) ] out JsonTypeInfo ? typeInfo )
194+ {
195+ _cache . TryGetValue ( type , out CacheEntry ? entry ) ;
196+ typeInfo = entry ? . TypeInfo ;
197+ return typeInfo is not null ;
198+ }
179199
180- public JsonTypeInfo ? GetOrAddTypeInfo ( Type type ) =>
200+ public void Clear ( )
201+ {
202+ _cache . Clear ( ) ;
203+ }
204+
205+ private CacheEntry GetOrAddCacheEntry ( Type type )
206+ {
181207#if NETCOREAPP
182- _jsonTypeInfoCache . GetOrAdd ( type , static ( type , options ) => options . GetTypeInfoNoCaching ( type ) , Options ) ;
208+ return _cache . GetOrAdd ( type , CreateCacheEntry , this ) ;
183209#else
184- _jsonTypeInfoCache . GetOrAdd ( type , _jsonTypeInfoFactory ) ;
210+ return _cache . GetOrAdd ( type , _cacheEntryFactory ) ;
185211#endif
212+ }
186213
187- public bool TryGetJsonTypeInfo ( Type type , [ NotNullWhen ( true ) ] out JsonTypeInfo ? typeInfo )
188- => _jsonTypeInfoCache . TryGetValue ( type , out typeInfo ) ;
214+ private static CacheEntry CreateCacheEntry ( Type type , CachingContext context )
215+ {
216+ try
217+ {
218+ JsonTypeInfo ? typeInfo = context . Options . GetTypeInfoNoCaching ( type ) ;
219+ return new CacheEntry ( typeInfo ) ;
220+ }
221+ catch ( Exception ex )
222+ {
223+ ExceptionDispatchInfo edi = ExceptionDispatchInfo . Capture ( ex ) ;
224+ return new CacheEntry ( edi ) ;
225+ }
226+ }
189227
190- public void Clear ( )
228+ private JsonTypeInfo ? FallBackToNearestAncestor ( Type type , CacheEntry entry )
229+ {
230+ Debug . Assert ( ! entry . HasResult ) ;
231+
232+ CacheEntry ? nearestAncestor = entry . IsNearestAncestorResolved
233+ ? entry . NearestAncestor
234+ : DetermineNearestAncestor ( type , entry ) ;
235+
236+ return nearestAncestor ? . GetResult ( ) ;
237+ }
238+
239+ [ UnconditionalSuppressMessage ( "ReflectionAnalysis" , "IL2070:UnrecognizedReflectionPattern" ,
240+ Justification = "We only need to examine the interface types that are supported by the underlying resolver." ) ]
241+ private CacheEntry ? DetermineNearestAncestor ( Type type , CacheEntry entry )
242+ {
243+ // In cases where the underlying TypeInfoResolver returns `null` for a given type,
244+ // this method traverses the hierarchy above the given type to determine potential
245+ // ancestors for which the resolver does provide metadata. This can be useful in
246+ // cases where we're using a source generator and are trying to serialize private
247+ // implementations of an interface that is supported by the source generator.
248+ // NB this algorithm runs lazily and unsynchronized *after* the CacheEntry has been looked up
249+ // from the global cache, so care should be taken to avoid potential race conditions.
250+ //
251+ // IMPORTANT: nearest-ancestor resolution should be reserved for weakly-typed serialization.
252+ // Attempting to use it in strongly typed operations or deserialization will invariably
253+ // result in an invalid cast exception, so use with caution.
254+
255+ Debug . Assert ( ! entry . HasResult ) ;
256+ CacheEntry ? candidate = null ;
257+ Type ? candidateType = null ;
258+
259+ for ( Type ? current = type . BaseType ; current != null ; current = current . BaseType )
260+ {
261+ if ( current == JsonTypeInfo . ObjectType )
262+ {
263+ // Avoid falling back to the contract for object since it's polymorphic
264+ // and it would try to send us back to the runtime type that isn't supported.
265+ break ;
266+ }
267+
268+ candidate = GetOrAddCacheEntry ( current ) ;
269+ if ( candidate . HasResult )
270+ {
271+ // We found a type in the class hierarchy that has a contract -- stop looking further up.
272+ candidateType = current ;
273+ break ;
274+ }
275+ }
276+
277+ foreach ( Type interfaceType in type . GetInterfaces ( ) )
278+ {
279+ CacheEntry interfaceEntry = GetOrAddCacheEntry ( interfaceType ) ;
280+ if ( interfaceEntry . HasResult )
281+ {
282+ if ( candidateType != null )
283+ {
284+ if ( interfaceType . IsAssignableFrom ( candidateType ) )
285+ {
286+ // The previous candidate is more derived than the
287+ // current interface -- keep our previous choice.
288+ continue ;
289+ }
290+ else if ( candidateType . IsAssignableFrom ( interfaceType ) )
291+ {
292+ // The current interface is more derived than the
293+ // previous candidate -- replace the candidate value.
294+ }
295+ else
296+ {
297+ // We have found two possible ancestors that are not in subtype relationship.
298+ // This indicates we have encountered a diamond ambiguity -- abort search and record an exception.
299+ NotSupportedException nse = ThrowHelper . GetNotSupportedException_AmbiguousMetadataForType ( type , candidateType , interfaceType ) ;
300+ candidate = new CacheEntry ( ExceptionDispatchInfo . Capture ( nse ) ) ;
301+ break ;
302+ }
303+ }
304+
305+ candidate = interfaceEntry ;
306+ candidateType = interfaceType ;
307+ }
308+ }
309+
310+ entry . NearestAncestor = candidate ;
311+ entry . IsNearestAncestorResolved = true ;
312+ return candidate ;
313+ }
314+
315+ private sealed class CacheEntry
191316 {
192- _jsonTypeInfoCache . Clear ( ) ;
317+ public readonly bool HasResult ;
318+ public readonly JsonTypeInfo ? TypeInfo ;
319+ public readonly ExceptionDispatchInfo ? ExceptionDispatchInfo ;
320+
321+ public volatile bool IsNearestAncestorResolved ;
322+ public CacheEntry ? NearestAncestor ;
323+
324+ public CacheEntry ( JsonTypeInfo ? typeInfo )
325+ {
326+ TypeInfo = typeInfo ;
327+ HasResult = typeInfo is not null ;
328+ }
329+
330+ public CacheEntry ( ExceptionDispatchInfo exception )
331+ {
332+ ExceptionDispatchInfo = exception ;
333+ HasResult = true ;
334+ }
335+
336+ public JsonTypeInfo ? GetResult ( )
337+ {
338+ ExceptionDispatchInfo ? . Throw ( ) ;
339+ return TypeInfo ;
340+ }
193341 }
194342 }
195343
0 commit comments