This commit is contained in:
Alexey Zakharov 2025-07-30 16:14:43 +02:00 committed by GitHub
commit 76f1a452b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 428 additions and 194 deletions

View File

@ -1055,6 +1055,14 @@
<Build Solution="Checked|x64" Project="false" />
<Build Solution="Checked|x86" Project="false" />
</Project>
<Project Path="tests/UnloadableTestTypes/UnloadableTestTypes.csproj">
<BuildType Solution="Checked|*" Project="Release" />
<Build Solution="*|arm" Project="false" />
<Build Solution="*|arm64" Project="false" />
<Build Solution="Checked|Any CPU" Project="false" />
<Build Solution="Checked|x64" Project="false" />
<Build Solution="Checked|x86" Project="false" />
</Project>
</Folder>
<Folder Name="/tools/" />
<Folder Name="/tools/gen/">

View File

@ -15,6 +15,7 @@
<Compile Include="System\ComponentModel\ByteConverter.cs" />
<Compile Include="System\ComponentModel\CharConverter.cs" />
<Compile Include="System\ComponentModel\CollectionConverter.cs" />
<Compile Include="System\ComponentModel\CollectibleKeyConcurrentHashtable.cs" />
<Compile Include="System\ComponentModel\DateOnlyConverter.cs" />
<Compile Include="System\ComponentModel\DateTimeConverter.cs" />
<Compile Include="System\ComponentModel\DateTimeOffsetConverter.cs" />
@ -44,6 +45,7 @@
<Compile Include="System\ComponentModel\UInt64Converter.cs" />
<Compile Include="System\ComponentModel\UriTypeConverter.cs" />
<Compile Include="System\ComponentModel\VersionConverter.cs" />
<Compile Include="System\ComponentModel\CollectibleKeyHashtable.cs" />
<Compile Include="System\Timers\ElapsedEventArgs.cs" />
<Compile Include="System\Timers\ElapsedEventHandler.cs" />
<Compile Include="System\Timers\Timer.cs">

View File

@ -0,0 +1,138 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace System.ComponentModel
{
/// <summary>
/// Concurrent dictionary that maps MemberInfo object key to an object.
/// Uses ConditionalWeakTable for the collectible keys (if MemberInfo.IsCollectible is true) and
/// ConcurrentDictionary for non-collectible keys.
/// </summary>
internal sealed class CollectibleKeyConcurrentHashtable<TKey, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
where TKey : MemberInfo
where TValue : class?
{
private readonly ConcurrentDictionary<TKey, TValue> _defaultTable = new ConcurrentDictionary<TKey, TValue>();
private readonly ConditionalWeakTable<TKey, object?> _collectibleTable = new ConditionalWeakTable<TKey, object?>();
public TValue? this[TKey key]
{
get
{
return TryGetValue(key, out TValue? value) ? value : default;
}
set
{
if (!key.IsCollectible)
{
_defaultTable[key] = value!;
}
else
{
_collectibleTable.AddOrUpdate(key, value);
}
}
}
public bool ContainsKey(TKey key)
{
return !key.IsCollectible ? _defaultTable.ContainsKey(key) : _collectibleTable.TryGetValue(key, out _);
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
if (!key.IsCollectible)
return _defaultTable.TryGetValue(key, out value);
if (_collectibleTable.TryGetValue(key, out object? valueObj) && valueObj != null)
{
value = (TValue)valueObj;
return true;
}
value = default;
return false;
}
public bool TryAdd(TKey key, TValue value)
{
return !key.IsCollectible
? _defaultTable.TryAdd(key, value)
: _collectibleTable.TryAdd(key, value);
}
public void Clear()
{
_defaultTable.Clear();
_collectibleTable.Clear();
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() =>
new Enumerator(_defaultTable.GetEnumerator(), ((IEnumerable<KeyValuePair<TKey, object?>>)_collectibleTable).GetEnumerator());
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private sealed class Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
{
private readonly IEnumerator<KeyValuePair<TKey, TValue>> _defaultEnumerator;
private readonly IEnumerator<KeyValuePair<TKey, object?>> _collectibleEnumerator;
private bool _enumeratingCollectibleEnumerator;
public Enumerator(IEnumerator<KeyValuePair<TKey, TValue>> defaultEnumerator, IEnumerator<KeyValuePair<TKey, object?>> collectibleEnumerator)
{
_defaultEnumerator = defaultEnumerator;
_collectibleEnumerator = collectibleEnumerator;
_enumeratingCollectibleEnumerator = false;
}
public KeyValuePair<TKey, TValue> Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose()
{
_defaultEnumerator.Dispose();
_collectibleEnumerator.Dispose();
}
public bool MoveNext()
{
if (!_enumeratingCollectibleEnumerator && _defaultEnumerator.MoveNext())
{
Current = _defaultEnumerator.Current;
return true;
}
_enumeratingCollectibleEnumerator = true;
while (_collectibleEnumerator.MoveNext())
{
if (_collectibleEnumerator.Current.Value is TValue value)
{
Current = new KeyValuePair<TKey, TValue>(_collectibleEnumerator.Current.Key, value);
return true;
}
}
Current = default;
return false;
}
public void Reset()
{
_defaultEnumerator.Reset();
_collectibleEnumerator.Reset();
_enumeratingCollectibleEnumerator = false;
Current = default;
}
}
}
}

View File

@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace System.ComponentModel
{
/// <summary>
/// Hashtable that maps a <see cref="MemberInfo"/> object key to an associated value.
/// <para>
/// For keys where <see cref="MemberInfo.IsCollectible"/> is <c>false</c>, a standard <see cref="Hashtable"/> is used.
/// For keys where <see cref="MemberInfo.IsCollectible"/> is <c>true</c>, a <see cref="ConditionalWeakTable{TKey, TValue}"/> is used.
/// This ensures that collectible <see cref="MemberInfo"/> instances (such as those from collectible assemblies) do not prevent their assemblies from being unloaded.
/// </para>
/// </summary>
internal sealed class CollectibleKeyHashtable
{
private readonly Hashtable _defaultTable = new Hashtable();
private readonly ConditionalWeakTable<object, object?> _collectibleTable = new ConditionalWeakTable<object, object?>();
public object? this[MemberInfo key]
{
get
{
return !key.IsCollectible ? _defaultTable[key] : (_collectibleTable.TryGetValue(key, out object? value) ? value : null);
}
set
{
if (!key.IsCollectible)
{
_defaultTable[key] = value;
}
else
{
_collectibleTable.AddOrUpdate(key, value);
}
}
}
}
}

View File

@ -3,7 +3,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@ -24,7 +23,7 @@ namespace System.ComponentModel
internal sealed partial class ReflectTypeDescriptionProvider : TypeDescriptionProvider
{
// ReflectedTypeData contains all of the type information we have gathered for a given type.
private readonly ConcurrentDictionary<Type, ReflectedTypeData> _typeData = new ConcurrentDictionary<Type, ReflectedTypeData>();
private readonly CollectibleKeyConcurrentHashtable<Type, ReflectedTypeData> _typeData = new CollectibleKeyConcurrentHashtable<Type, ReflectedTypeData>();
// This is the signature we look for when creating types that are generic, but
// want to know what type they are dealing with. Enums are a good example of this;
@ -49,10 +48,10 @@ namespace System.ComponentModel
// on Control, Component and object are also automatically filled
// in. The keys to the property and event caches are types.
// The keys to the attribute cache are either MemberInfos or types.
private static Hashtable? s_propertyCache;
private static Hashtable? s_eventCache;
private static Hashtable? s_attributeCache;
private static Hashtable? s_extendedPropertyCache;
private static CollectibleKeyHashtable? s_propertyCache;
private static CollectibleKeyHashtable? s_eventCache;
private static CollectibleKeyHashtable? s_attributeCache;
private static CollectibleKeyHashtable? s_extendedPropertyCache;
// These are keys we stuff into our object cache. We use this
// cache data to store extender provider info for an object.
@ -193,13 +192,13 @@ namespace System.ComponentModel
Justification = "IntrinsicTypeConverters is marked with RequiresUnreferencedCode. It is the only place that should call this.")]
private static NullableConverter CreateNullableConverter(Type type) => new NullableConverter(type);
private static Hashtable PropertyCache => LazyInitializer.EnsureInitialized(ref s_propertyCache, () => new Hashtable());
private static CollectibleKeyHashtable PropertyCache => LazyInitializer.EnsureInitialized(ref s_propertyCache, () => new CollectibleKeyHashtable());
private static Hashtable EventCache => LazyInitializer.EnsureInitialized(ref s_eventCache, () => new Hashtable());
private static CollectibleKeyHashtable EventCache => LazyInitializer.EnsureInitialized(ref s_eventCache, () => new CollectibleKeyHashtable());
private static Hashtable AttributeCache => LazyInitializer.EnsureInitialized(ref s_attributeCache, () => new Hashtable());
private static CollectibleKeyHashtable AttributeCache => LazyInitializer.EnsureInitialized(ref s_attributeCache, () => new CollectibleKeyHashtable());
private static Hashtable ExtendedPropertyCache => LazyInitializer.EnsureInitialized(ref s_extendedPropertyCache, () => new Hashtable());
private static CollectibleKeyHashtable ExtendedPropertyCache => LazyInitializer.EnsureInitialized(ref s_extendedPropertyCache, () => new CollectibleKeyHashtable());
/// <summary>Clear the global caches this maintains on top of reflection.</summary>
internal static void ClearReflectionCaches()
@ -1096,7 +1095,7 @@ namespace System.ComponentModel
/// </summary>
internal static Attribute[] ReflectGetAttributes(Type type)
{
Hashtable attributeCache = AttributeCache;
CollectibleKeyHashtable attributeCache = AttributeCache;
Attribute[]? attrs = (Attribute[]?)attributeCache[type];
if (attrs != null)
{
@ -1124,7 +1123,7 @@ namespace System.ComponentModel
/// </summary>
internal static Attribute[] ReflectGetAttributes(MemberInfo member)
{
Hashtable attributeCache = AttributeCache;
CollectibleKeyHashtable attributeCache = AttributeCache;
Attribute[]? attrs = (Attribute[]?)attributeCache[member];
if (attrs != null)
{
@ -1152,7 +1151,7 @@ namespace System.ComponentModel
/// </summary>
private static EventDescriptor[] ReflectGetEvents(Type type)
{
Hashtable eventCache = EventCache;
CollectibleKeyHashtable eventCache = EventCache;
EventDescriptor[]? events = (EventDescriptor[]?)eventCache[type];
if (events != null)
{
@ -1252,7 +1251,7 @@ namespace System.ComponentModel
// property store.
//
Type providerType = provider.GetType();
Hashtable extendedPropertyCache = ExtendedPropertyCache;
CollectibleKeyHashtable extendedPropertyCache = ExtendedPropertyCache;
ReflectPropertyDescriptor[]? extendedProperties = (ReflectPropertyDescriptor[]?)extendedPropertyCache[providerType];
if (extendedProperties == null)
{
@ -1337,7 +1336,7 @@ namespace System.ComponentModel
private static PropertyDescriptor[] ReflectGetPropertiesImpl(Type type)
{
Hashtable propertyCache = PropertyCache;
CollectibleKeyHashtable propertyCache = PropertyCache;
PropertyDescriptor[]? properties = (PropertyDescriptor[]?)propertyCache[type];
if (properties != null)
{

View File

@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel.Design;
@ -67,12 +66,12 @@ namespace System.ComponentModel
internal static readonly object s_commonSyncObject = new object();
// A direct mapping from type to provider.
private static readonly ConcurrentDictionary<Type, TypeDescriptionNode> s_providerTypeTable = new ConcurrentDictionary<Type, TypeDescriptionNode>();
private static readonly CollectibleKeyConcurrentHashtable<Type, TypeDescriptionNode> s_providerTypeTable = new CollectibleKeyConcurrentHashtable<Type, TypeDescriptionNode>();
// Tracks DefaultTypeDescriptionProviderAttributes.
// A value of `null` indicates initialization is in progress.
// A value of s_initializedDefaultProvider indicates the provider is initialized.
private static readonly ConcurrentDictionary<Type, object?> s_defaultProviderInitialized = new ConcurrentDictionary<Type, object?>();
private static readonly CollectibleKeyConcurrentHashtable<Type, object?> s_defaultProviderInitialized = new CollectibleKeyConcurrentHashtable<Type, object?>();
private static readonly object s_initializedDefaultProvider = new object();
@ -293,7 +292,7 @@ namespace System.ComponentModel
refreshNeeded = s_providerTable.ContainsKey(instance);
TypeDescriptionNode node = NodeFor(instance, true);
var head = new TypeDescriptionNode(provider) { Next = node };
s_providerTable.SetWeak(instance, head);
s_providerTable[instance] = head;
s_providerTypeTable.Clear();
}
@ -433,7 +432,7 @@ namespace System.ComponentModel
if (associations == null)
{
associations = new ArrayList(4);
associationTable.SetWeak(primary, associations);
associationTable[primary] = associations;
}
}
}
@ -624,7 +623,7 @@ namespace System.ComponentModel
if (!type.IsInstanceOfType(primary))
{
// Check our association table for a match.
Hashtable assocTable = AssociationTable;
WeakHashtable assocTable = AssociationTable;
IList? associations = (IList?)assocTable?[primary];
if (associations != null)
{
@ -2334,16 +2333,12 @@ namespace System.ComponentModel
// ReflectTypeDescritionProvider is only bound to object, but we
// need go to through the entire table to try to find custom
// providers. If we find one, will clear our cache.
// Manual use of IDictionaryEnumerator instead of foreach to avoid
// DictionaryEntry box allocations.
IDictionaryEnumerator e = s_providerTable.GetEnumerator();
while (e.MoveNext())
foreach (KeyValuePair<object, object?> kvp in s_providerTable)
{
DictionaryEntry de = e.Entry;
Type? nodeType = de.Key as Type;
Type? nodeType = kvp.Key as Type;
if (nodeType != null && type.IsAssignableFrom(nodeType) || nodeType == typeof(object))
{
TypeDescriptionNode? node = (TypeDescriptionNode?)de.Value;
TypeDescriptionNode? node = (TypeDescriptionNode?)kvp.Value;
while (node != null && !(node.Provider is ReflectTypeDescriptionProvider))
{
found = true;
@ -2418,16 +2413,12 @@ namespace System.ComponentModel
// ReflectTypeDescritionProvider is only bound to object, but we
// need go to through the entire table to try to find custom
// providers. If we find one, will clear our cache.
// Manual use of IDictionaryEnumerator instead of foreach to avoid
// DictionaryEntry box allocations.
IDictionaryEnumerator e = s_providerTable.GetEnumerator();
while (e.MoveNext())
foreach (KeyValuePair<object, object?> kvp in s_providerTable)
{
DictionaryEntry de = e.Entry;
Type? nodeType = de.Key as Type;
Type? nodeType = kvp.Key as Type;
if (nodeType != null && type.IsAssignableFrom(nodeType) || nodeType == typeof(object))
{
TypeDescriptionNode? node = (TypeDescriptionNode?)de.Value;
TypeDescriptionNode? node = (TypeDescriptionNode?)kvp.Value;
while (node != null && !(node.Provider is ReflectTypeDescriptionProvider))
{
found = true;
@ -2480,15 +2471,12 @@ namespace System.ComponentModel
lock (s_commonSyncObject)
{
// Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations.
IDictionaryEnumerator e = s_providerTable.GetEnumerator();
while (e.MoveNext())
foreach (KeyValuePair<object, object?> kvp in s_providerTable)
{
DictionaryEntry de = e.Entry;
Type? nodeType = de.Key as Type;
Type? nodeType = kvp.Key as Type;
if (nodeType != null && nodeType.Module.Equals(module) || nodeType == typeof(object))
{
TypeDescriptionNode? node = (TypeDescriptionNode?)de.Value;
TypeDescriptionNode? node = (TypeDescriptionNode?)kvp.Value;
while (node != null && !(node.Provider is ReflectTypeDescriptionProvider))
{
refreshedTypes ??= new Hashtable();
@ -2630,7 +2618,7 @@ namespace System.ComponentModel
ArgumentNullException.ThrowIfNull(primary);
ArgumentNullException.ThrowIfNull(secondary);
Hashtable assocTable = AssociationTable;
WeakHashtable assocTable = AssociationTable;
IList? associations = (IList?)assocTable?[primary];
if (associations != null)
{

View File

@ -3,170 +3,31 @@
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace System.ComponentModel
{
/// <summary>
/// This is a hashtable that stores object keys as weak references.
/// It monitors memory usage and will periodically scavenge the
/// hash table to clean out dead references.
/// Provides a hashtable-like collection that stores keys and values using weak references.
/// This class is a thin wrapper around <see cref="ConditionalWeakTable{TKey, TValue}"/>
/// and is especially useful for associating data with objects from unloadable assemblies.
/// </summary>
internal sealed class WeakHashtable : Hashtable
internal sealed class WeakHashtable : IEnumerable<KeyValuePair<object, object?>>
{
private static readonly IEqualityComparer s_comparer = new WeakKeyComparer();
private readonly ConditionalWeakTable<object, object?> _hashtable = new ConditionalWeakTable<object, object?>();
private long _lastGlobalMem;
private int _lastHashCount;
internal WeakHashtable() : base(s_comparer)
public object? this[object key]
{
get => _hashtable.TryGetValue(key, out object? value) ? value : null;
set => _hashtable.AddOrUpdate(key, value);
}
/// <summary>
/// Override of Item that wraps a weak reference around the
/// key and performs a scavenge.
/// </summary>
public void SetWeak(object key, object value)
{
ScavengeKeys();
this[new EqualityWeakReference(key)] = value;
}
public bool ContainsKey(object key) => _hashtable.TryGetValue(key, out object? _);
/// <summary>
/// This method checks to see if it is necessary to
/// scavenge keys, and if it is it performs a scan
/// of all keys to see which ones are no longer valid.
/// To determine if we need to scavenge keys we need to
/// try to track the current GC memory. Our rule of
/// thumb is that if GC memory is decreasing and our
/// key count is constant we need to scavenge. We
/// will need to see if this is too often for extreme
/// use cases like the CompactFramework (they add
/// custom type data for every object at design time).
/// </summary>
private void ScavengeKeys()
{
int hashCount = Count;
public void Remove(object key) => _hashtable.Remove(key);
if (hashCount == 0)
{
return;
}
public IEnumerator<KeyValuePair<object, object?>> GetEnumerator() => ((IEnumerable<KeyValuePair<object, object?>>)_hashtable).GetEnumerator();
if (_lastHashCount == 0)
{
_lastHashCount = hashCount;
return;
}
long globalMem = GC.GetTotalMemory(false);
if (_lastGlobalMem == 0)
{
_lastGlobalMem = globalMem;
return;
}
float memDelta = (globalMem - _lastGlobalMem) / (float)_lastGlobalMem;
float hashDelta = (hashCount - _lastHashCount) / (float)_lastHashCount;
if (memDelta < 0 && hashDelta >= 0)
{
// Perform a scavenge through our keys, looking
// for dead references.
List<object>? cleanupList = null;
foreach (object o in Keys)
{
if (o is WeakReference wr && !wr.IsAlive)
{
cleanupList ??= new List<object>();
cleanupList.Add(wr);
}
}
if (cleanupList != null)
{
foreach (object o in cleanupList)
{
Remove(o);
}
}
}
_lastGlobalMem = globalMem;
_lastHashCount = hashCount;
}
private sealed class WeakKeyComparer : IEqualityComparer
{
bool IEqualityComparer.Equals(object? x, object? y)
{
if (x == null)
{
return y == null;
}
if (y != null && x.GetHashCode() == y.GetHashCode())
{
if (x is WeakReference wX)
{
if (!wX.IsAlive)
{
return false;
}
x = wX.Target;
}
if (y is WeakReference wY)
{
if (!wY.IsAlive)
{
return false;
}
y = wY.Target;
}
return object.ReferenceEquals(x, y);
}
return false;
}
int IEqualityComparer.GetHashCode(object obj) => obj.GetHashCode();
}
/// <summary>
/// A subclass of WeakReference that overrides GetHashCode and
/// Equals so that the weak reference returns the same equality
/// semantics as the object it wraps. This will always return
/// the object's hash code and will return True for a Equals
/// comparison of the object it is wrapping. If the object
/// it is wrapping has finalized, Equals always returns false.
/// </summary>
private sealed class EqualityWeakReference : WeakReference
{
private readonly int _hashCode;
internal EqualityWeakReference(object o) : base(o)
{
_hashCode = o.GetHashCode();
}
public override bool Equals(object? o)
{
if (o?.GetHashCode() != _hashCode)
{
return false;
}
if (o == this || (IsAlive && ReferenceEquals(o, Target)))
{
return true;
}
return false;
}
public override int GetHashCode() => _hashCode;
}
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<KeyValuePair<object, object?>>)_hashtable).GetEnumerator();
}
}

View File

@ -182,6 +182,9 @@
<LastGenOutput>TestResx.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="UnloadableTestTypes\UnloadableTestTypes.csproj" />
</ItemGroup>
<ItemGroup>
<TrimmerRootDescriptor Include="$(MSBuildThisFileDirectory)ILLink.Descriptors.xml" />
</ItemGroup>

View File

@ -5,7 +5,11 @@ using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
@ -1523,5 +1527,151 @@ namespace System.ComponentModel.Tests
public class MyInheritedClassWithCustomTypeDescriptionProviderConverter : TypeConverter
{
}
private class TestAssemblyLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public TestAssemblyLoadContext(string name, bool isCollectible, string mainAssemblyToLoadPath = null) : base(name, isCollectible)
{
if (!PlatformDetection.IsBrowser)
_resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath ?? Assembly.GetExecutingAssembly().Location);
}
protected override Assembly Load(AssemblyName name)
{
if (PlatformDetection.IsBrowser)
{
return base.Load(name);
}
string assemblyPath = _resolver.ResolveAssemblyToPath(name);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
}
// This method must be not inlined to ensure that the ALC is not captured on the caller's stack.
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
private static void ExecuteAndUnload(string assemblyfile, Action<Assembly> assemblyAction, out WeakReference alcWeakRef)
{
var fullPath = Path.GetFullPath(assemblyfile);
var alc = new TestAssemblyLoadContext("TypeDescriptorTests", true, fullPath);
alcWeakRef = new WeakReference(alc);
// Load assembly by path. By name, and it gets loaded in the default ALC.
var assembly = alc.LoadFromAssemblyPath(fullPath);
Assert.NotEqual(AssemblyLoadContext.GetLoadContext(assembly), AssemblyLoadContext.Default);
using (AssemblyLoadContext.ContextualReflectionScope scope = alc.EnterContextualReflection())
{
// Perform action on the assembly from ALC inside the reflection scope
assemblyAction(assembly);
}
// Unload the ALC
alc.Unload();
}
[Fact]
// Lack of AssemblyDependencyResolver results in assemblies that are not loaded by path to get
// loaded in the default ALC, which causes problems for this test.
[SkipOnPlatform(TestPlatforms.Browser, "AssemblyDependencyResolver not supported in wasm")]
[ActiveIssue("34072", TestRuntimes.Mono)]
public static void TypeDescriptor_WithDefaultProvider_UnloadsUnloadableTypes()
{
ExecuteAndUnload("UnloadableTestTypes.dll",
static (assembly) =>
{
// Ensure the type loaded in the intended non-Default ALC
Type? collectibleType = assembly.GetType("UnloadableTestTypes.SimpleType");
Assert.NotNull(collectibleType);
Type? collectibleAttributeType = assembly.GetType("UnloadableTestTypes.SimpleTypeAttribute");
Assert.NotNull(collectibleAttributeType);
Attribute? collectibleAttribute = collectibleType.GetCustomAttribute(collectibleAttributeType);
Assert.NotNull(collectibleAttribute);
Type? collectibleTypeDescriptionProviderType = assembly.GetType("UnloadableTestTypes.SimpleTypeDescriptionProvider");
Assert.NotNull(collectibleTypeDescriptionProviderType);
// Cache the type's cachable entities
AttributeCollection attributes = TypeDescriptor.GetAttributes(collectibleType);
Assert.True(attributes.Contains(collectibleAttribute));
EventDescriptorCollection events = TypeDescriptor.GetEvents(collectibleType);
Assert.Equal(1, events.Count);
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(collectibleType);
Assert.Equal(2, properties.Count);
},
out var weakRef);
// Force garbage collection to ensure the ALC is unloaded and the types are collected.
for (int i = 0; weakRef.IsAlive && i < 10; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
// Assert that the weak reference to the ALC is no longer alive,
// indicating that the unloaded AssemblyLoadContext has been at least collected.
Assert.True(!weakRef.IsAlive);
}
[Fact]
// Lack of AssemblyDependencyResolver results in assemblies that are not loaded by path to get
// loaded in the default ALC, which causes problems for this test.
[SkipOnPlatform(TestPlatforms.Browser, "AssemblyDependencyResolver not supported in wasm")]
[ActiveIssue("34072", TestRuntimes.Mono)]
public static void TypeDescriptor_WithCustomProvider_UnloadsUnloadableTypes()
{
ExecuteAndUnload("UnloadableTestTypes.dll",
static (assembly) =>
{
// Ensure the type loaded in the intended non-Default ALC
Type? collectibleType = assembly.GetType("UnloadableTestTypes.SimpleType");
Assert.NotNull(collectibleType);
Type? collectibleAttributeType = assembly.GetType("UnloadableTestTypes.SimpleTypeAttribute");
Assert.NotNull(collectibleAttributeType);
Attribute? collectibleAttribute = collectibleType.GetCustomAttribute(collectibleAttributeType);
Assert.NotNull(collectibleAttribute);
Type? collectibleTypeDescriptionProviderType = assembly.GetType("UnloadableTestTypes.SimpleTypeDescriptionProvider");
Assert.NotNull(collectibleTypeDescriptionProviderType);
// Add provider to ensure it is registered and stored in the TypeDescriptor cache
TypeDescriptionProvider collectibleProvider = (TypeDescriptionProvider)Activator.CreateInstance(collectibleTypeDescriptionProviderType);
TypeDescriptor.AddProvider(collectibleProvider, collectibleType);
// Test that the provider is the expected collectible one
AttributeCollection attributes = TypeDescriptor.GetAttributes(collectibleType);
Assert.Empty(attributes);
EventDescriptorCollection events = TypeDescriptor.GetEvents(collectibleType);
Assert.Empty(events);
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(collectibleType);
Assert.Empty(properties);
TypeDescriptionProvider provider = TypeDescriptor.GetProvider(collectibleType);
Assert.NotNull(provider);
Assert.True(provider.IsSupportedType(collectibleType));
Assert.False(provider.IsSupportedType(typeof(int)));
},
out var weakRef);
// Force garbage collection to ensure the ALC is unloaded and the types are collected.
for (int i = 0; weakRef.IsAlive && i < 10; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
// Assert that the weak reference to the ALC is no longer alive,
// indicating that the unloaded AssemblyLoadContext has been at least collected.
Assert.True(!weakRef.IsAlive);
}
}
}

View File

@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.ComponentModel;
namespace UnloadableTestTypes
{
[SimpleType]
public class SimpleType
{
public string P1 { get; set; }
public int P2 { get; set; }
public event Action ActionEvent;
public void OnActionEvent()
{
ActionEvent?.Invoke();
}
}
[AttributeUsage(AttributeTargets.All)]
public sealed class SimpleTypeAttribute : Attribute { }
public sealed class SimpleTypeDescriptionProvider : TypeDescriptionProvider
{
public override bool IsSupportedType(Type type) => type.AssemblyQualifiedName == typeof(SimpleType).AssemblyQualifiedName;
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance) => new SimpleTypeDescriptor();
public sealed class SimpleTypeDescriptor : CustomTypeDescriptor { }
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="UnloadableTestTypes.cs" />
</ItemGroup>
</Project>