Skip to content

Commit 6a8720f

Browse files
Use AssemblyLoadContext-aware caches in TypeDescriptor to support unloading of assemblies cached by TypeDescriptor (#114619)
* Use separate hashtables for collectible types to support assembly unloadability for the TypeDescriptor class * Addressed codereview feedback * Added a test for custom provider and updated WeakHashTable to use ConditionalWeakTable to ensure values can be also colelcted * Renamed ContextAwareHashtable to CollectibleKeyHashtable
1 parent e3caf9f commit 6a8720f

11 files changed

+428
-194
lines changed

src/libraries/System.ComponentModel.TypeConverter/System.ComponentModel.TypeConverter.slnx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,14 @@
10551055
<Build Solution="Checked|x64" Project="false" />
10561056
<Build Solution="Checked|x86" Project="false" />
10571057
</Project>
1058+
<Project Path="tests/UnloadableTestTypes/UnloadableTestTypes.csproj">
1059+
<BuildType Solution="Checked|*" Project="Release" />
1060+
<Build Solution="*|arm" Project="false" />
1061+
<Build Solution="*|arm64" Project="false" />
1062+
<Build Solution="Checked|Any CPU" Project="false" />
1063+
<Build Solution="Checked|x64" Project="false" />
1064+
<Build Solution="Checked|x86" Project="false" />
1065+
</Project>
10581066
</Folder>
10591067
<Folder Name="/tools/" />
10601068
<Folder Name="/tools/gen/">

src/libraries/System.ComponentModel.TypeConverter/src/System.ComponentModel.TypeConverter.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Compile Include="System\ComponentModel\ByteConverter.cs" />
1616
<Compile Include="System\ComponentModel\CharConverter.cs" />
1717
<Compile Include="System\ComponentModel\CollectionConverter.cs" />
18+
<Compile Include="System\ComponentModel\CollectibleKeyConcurrentHashtable.cs" />
1819
<Compile Include="System\ComponentModel\DateOnlyConverter.cs" />
1920
<Compile Include="System\ComponentModel\DateTimeConverter.cs" />
2021
<Compile Include="System\ComponentModel\DateTimeOffsetConverter.cs" />
@@ -44,6 +45,7 @@
4445
<Compile Include="System\ComponentModel\UInt64Converter.cs" />
4546
<Compile Include="System\ComponentModel\UriTypeConverter.cs" />
4647
<Compile Include="System\ComponentModel\VersionConverter.cs" />
48+
<Compile Include="System\ComponentModel\CollectibleKeyHashtable.cs" />
4749
<Compile Include="System\Timers\ElapsedEventArgs.cs" />
4850
<Compile Include="System\Timers\ElapsedEventHandler.cs" />
4951
<Compile Include="System\Timers\Timer.cs">
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Reflection;
9+
using System.Runtime.CompilerServices;
10+
11+
namespace System.ComponentModel
12+
{
13+
/// <summary>
14+
/// Concurrent dictionary that maps MemberInfo object key to an object.
15+
/// Uses ConditionalWeakTable for the collectible keys (if MemberInfo.IsCollectible is true) and
16+
/// ConcurrentDictionary for non-collectible keys.
17+
/// </summary>
18+
internal sealed class CollectibleKeyConcurrentHashtable<TKey, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
19+
where TKey : MemberInfo
20+
where TValue : class?
21+
{
22+
private readonly ConcurrentDictionary<TKey, TValue> _defaultTable = new ConcurrentDictionary<TKey, TValue>();
23+
private readonly ConditionalWeakTable<TKey, object?> _collectibleTable = new ConditionalWeakTable<TKey, object?>();
24+
25+
public TValue? this[TKey key]
26+
{
27+
get
28+
{
29+
return TryGetValue(key, out TValue? value) ? value : default;
30+
}
31+
32+
set
33+
{
34+
if (!key.IsCollectible)
35+
{
36+
_defaultTable[key] = value!;
37+
}
38+
else
39+
{
40+
_collectibleTable.AddOrUpdate(key, value);
41+
}
42+
}
43+
}
44+
45+
public bool ContainsKey(TKey key)
46+
{
47+
return !key.IsCollectible ? _defaultTable.ContainsKey(key) : _collectibleTable.TryGetValue(key, out _);
48+
}
49+
50+
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
51+
{
52+
if (!key.IsCollectible)
53+
return _defaultTable.TryGetValue(key, out value);
54+
55+
if (_collectibleTable.TryGetValue(key, out object? valueObj) && valueObj != null)
56+
{
57+
value = (TValue)valueObj;
58+
return true;
59+
}
60+
61+
value = default;
62+
return false;
63+
}
64+
65+
public bool TryAdd(TKey key, TValue value)
66+
{
67+
return !key.IsCollectible
68+
? _defaultTable.TryAdd(key, value)
69+
: _collectibleTable.TryAdd(key, value);
70+
}
71+
72+
public void Clear()
73+
{
74+
_defaultTable.Clear();
75+
_collectibleTable.Clear();
76+
}
77+
78+
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() =>
79+
new Enumerator(_defaultTable.GetEnumerator(), ((IEnumerable<KeyValuePair<TKey, object?>>)_collectibleTable).GetEnumerator());
80+
81+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
82+
83+
private sealed class Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
84+
{
85+
private readonly IEnumerator<KeyValuePair<TKey, TValue>> _defaultEnumerator;
86+
private readonly IEnumerator<KeyValuePair<TKey, object?>> _collectibleEnumerator;
87+
private bool _enumeratingCollectibleEnumerator;
88+
89+
public Enumerator(IEnumerator<KeyValuePair<TKey, TValue>> defaultEnumerator, IEnumerator<KeyValuePair<TKey, object?>> collectibleEnumerator)
90+
{
91+
_defaultEnumerator = defaultEnumerator;
92+
_collectibleEnumerator = collectibleEnumerator;
93+
_enumeratingCollectibleEnumerator = false;
94+
}
95+
96+
public KeyValuePair<TKey, TValue> Current { get; private set; }
97+
98+
object IEnumerator.Current => Current;
99+
100+
public void Dispose()
101+
{
102+
_defaultEnumerator.Dispose();
103+
_collectibleEnumerator.Dispose();
104+
}
105+
106+
public bool MoveNext()
107+
{
108+
if (!_enumeratingCollectibleEnumerator && _defaultEnumerator.MoveNext())
109+
{
110+
Current = _defaultEnumerator.Current;
111+
return true;
112+
}
113+
114+
_enumeratingCollectibleEnumerator = true;
115+
116+
while (_collectibleEnumerator.MoveNext())
117+
{
118+
if (_collectibleEnumerator.Current.Value is TValue value)
119+
{
120+
Current = new KeyValuePair<TKey, TValue>(_collectibleEnumerator.Current.Key, value);
121+
return true;
122+
}
123+
}
124+
125+
Current = default;
126+
return false;
127+
}
128+
129+
public void Reset()
130+
{
131+
_defaultEnumerator.Reset();
132+
_collectibleEnumerator.Reset();
133+
_enumeratingCollectibleEnumerator = false;
134+
Current = default;
135+
}
136+
}
137+
}
138+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Reflection;
6+
using System.Runtime.CompilerServices;
7+
8+
namespace System.ComponentModel
9+
{
10+
/// <summary>
11+
/// Hashtable that maps a <see cref="MemberInfo"/> object key to an associated value.
12+
/// <para>
13+
/// For keys where <see cref="MemberInfo.IsCollectible"/> is <c>false</c>, a standard <see cref="Hashtable"/> is used.
14+
/// For keys where <see cref="MemberInfo.IsCollectible"/> is <c>true</c>, a <see cref="ConditionalWeakTable{TKey, TValue}"/> is used.
15+
/// This ensures that collectible <see cref="MemberInfo"/> instances (such as those from collectible assemblies) do not prevent their assemblies from being unloaded.
16+
/// </para>
17+
/// </summary>
18+
internal sealed class CollectibleKeyHashtable
19+
{
20+
private readonly Hashtable _defaultTable = new Hashtable();
21+
private readonly ConditionalWeakTable<object, object?> _collectibleTable = new ConditionalWeakTable<object, object?>();
22+
23+
public object? this[MemberInfo key]
24+
{
25+
get
26+
{
27+
return !key.IsCollectible ? _defaultTable[key] : (_collectibleTable.TryGetValue(key, out object? value) ? value : null);
28+
}
29+
30+
set
31+
{
32+
if (!key.IsCollectible)
33+
{
34+
_defaultTable[key] = value;
35+
}
36+
else
37+
{
38+
_collectibleTable.AddOrUpdate(key, value);
39+
}
40+
}
41+
}
42+
}
43+
}

src/libraries/System.ComponentModel.TypeConverter/src/System/ComponentModel/ReflectTypeDescriptionProvider.cs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Collections;
55
using System.Collections.Generic;
6-
using System.Collections.Concurrent;
76
using System.ComponentModel.Design;
87
using System.Diagnostics;
98
using System.Diagnostics.CodeAnalysis;
@@ -24,7 +23,7 @@ namespace System.ComponentModel
2423
internal sealed partial class ReflectTypeDescriptionProvider : TypeDescriptionProvider
2524
{
2625
// ReflectedTypeData contains all of the type information we have gathered for a given type.
27-
private readonly ConcurrentDictionary<Type, ReflectedTypeData> _typeData = new ConcurrentDictionary<Type, ReflectedTypeData>();
26+
private readonly CollectibleKeyConcurrentHashtable<Type, ReflectedTypeData> _typeData = new CollectibleKeyConcurrentHashtable<Type, ReflectedTypeData>();
2827

2928
// This is the signature we look for when creating types that are generic, but
3029
// want to know what type they are dealing with. Enums are a good example of this;
@@ -49,10 +48,10 @@ internal sealed partial class ReflectTypeDescriptionProvider : TypeDescriptionPr
4948
// on Control, Component and object are also automatically filled
5049
// in. The keys to the property and event caches are types.
5150
// The keys to the attribute cache are either MemberInfos or types.
52-
private static Hashtable? s_propertyCache;
53-
private static Hashtable? s_eventCache;
54-
private static Hashtable? s_attributeCache;
55-
private static Hashtable? s_extendedPropertyCache;
51+
private static CollectibleKeyHashtable? s_propertyCache;
52+
private static CollectibleKeyHashtable? s_eventCache;
53+
private static CollectibleKeyHashtable? s_attributeCache;
54+
private static CollectibleKeyHashtable? s_extendedPropertyCache;
5655

5756
// These are keys we stuff into our object cache. We use this
5857
// cache data to store extender provider info for an object.
@@ -193,13 +192,13 @@ private static Dictionary<object, IntrinsicTypeConverterData> IntrinsicTypeConve
193192
Justification = "IntrinsicTypeConverters is marked with RequiresUnreferencedCode. It is the only place that should call this.")]
194193
private static NullableConverter CreateNullableConverter(Type type) => new NullableConverter(type);
195194

196-
private static Hashtable PropertyCache => LazyInitializer.EnsureInitialized(ref s_propertyCache, () => new Hashtable());
195+
private static CollectibleKeyHashtable PropertyCache => LazyInitializer.EnsureInitialized(ref s_propertyCache, () => new CollectibleKeyHashtable());
197196

198-
private static Hashtable EventCache => LazyInitializer.EnsureInitialized(ref s_eventCache, () => new Hashtable());
197+
private static CollectibleKeyHashtable EventCache => LazyInitializer.EnsureInitialized(ref s_eventCache, () => new CollectibleKeyHashtable());
199198

200-
private static Hashtable AttributeCache => LazyInitializer.EnsureInitialized(ref s_attributeCache, () => new Hashtable());
199+
private static CollectibleKeyHashtable AttributeCache => LazyInitializer.EnsureInitialized(ref s_attributeCache, () => new CollectibleKeyHashtable());
201200

202-
private static Hashtable ExtendedPropertyCache => LazyInitializer.EnsureInitialized(ref s_extendedPropertyCache, () => new Hashtable());
201+
private static CollectibleKeyHashtable ExtendedPropertyCache => LazyInitializer.EnsureInitialized(ref s_extendedPropertyCache, () => new CollectibleKeyHashtable());
203202

204203
/// <summary>Clear the global caches this maintains on top of reflection.</summary>
205204
internal static void ClearReflectionCaches()
@@ -1096,7 +1095,7 @@ internal bool IsPopulated(Type type)
10961095
/// </summary>
10971096
internal static Attribute[] ReflectGetAttributes(Type type)
10981097
{
1099-
Hashtable attributeCache = AttributeCache;
1098+
CollectibleKeyHashtable attributeCache = AttributeCache;
11001099
Attribute[]? attrs = (Attribute[]?)attributeCache[type];
11011100
if (attrs != null)
11021101
{
@@ -1124,7 +1123,7 @@ internal static Attribute[] ReflectGetAttributes(Type type)
11241123
/// </summary>
11251124
internal static Attribute[] ReflectGetAttributes(MemberInfo member)
11261125
{
1127-
Hashtable attributeCache = AttributeCache;
1126+
CollectibleKeyHashtable attributeCache = AttributeCache;
11281127
Attribute[]? attrs = (Attribute[]?)attributeCache[member];
11291128
if (attrs != null)
11301129
{
@@ -1152,7 +1151,7 @@ internal static Attribute[] ReflectGetAttributes(MemberInfo member)
11521151
/// </summary>
11531152
private static EventDescriptor[] ReflectGetEvents(Type type)
11541153
{
1155-
Hashtable eventCache = EventCache;
1154+
CollectibleKeyHashtable eventCache = EventCache;
11561155
EventDescriptor[]? events = (EventDescriptor[]?)eventCache[type];
11571156
if (events != null)
11581157
{
@@ -1252,7 +1251,7 @@ private static PropertyDescriptor[] ReflectGetExtendedProperties(IExtenderProvid
12521251
// property store.
12531252
//
12541253
Type providerType = provider.GetType();
1255-
Hashtable extendedPropertyCache = ExtendedPropertyCache;
1254+
CollectibleKeyHashtable extendedPropertyCache = ExtendedPropertyCache;
12561255
ReflectPropertyDescriptor[]? extendedProperties = (ReflectPropertyDescriptor[]?)extendedPropertyCache[providerType];
12571256
if (extendedProperties == null)
12581257
{
@@ -1337,7 +1336,7 @@ private static PropertyDescriptor[] ReflectGetPropertiesFromRegisteredType(Type
13371336

13381337
private static PropertyDescriptor[] ReflectGetPropertiesImpl(Type type)
13391338
{
1340-
Hashtable propertyCache = PropertyCache;
1339+
CollectibleKeyHashtable propertyCache = PropertyCache;
13411340
PropertyDescriptor[]? properties = (PropertyDescriptor[]?)propertyCache[type];
13421341
if (properties != null)
13431342
{

0 commit comments

Comments
 (0)