diff --git a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
index 5f0d5c2c940f87..d841ffc7287cbb 100644
--- a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
+++ b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
@@ -199,6 +199,7 @@
+
diff --git a/src/coreclr/System.Private.CoreLib/src/System/Reflection/Metadata/RuntimeTypeMetadataUpdateHandler.cs b/src/coreclr/System.Private.CoreLib/src/System/Reflection/Metadata/RuntimeTypeMetadataUpdateHandler.cs
new file mode 100644
index 00000000000000..25f29ed8e596eb
--- /dev/null
+++ b/src/coreclr/System.Private.CoreLib/src/System/Reflection/Metadata/RuntimeTypeMetadataUpdateHandler.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection.Metadata;
+
+[assembly: MetadataUpdateHandler(typeof(RuntimeTypeMetadataUpdateHandler))]
+
+namespace System.Reflection.Metadata
+{
+ /// Metadata update handler used to clear a Type's reflection cache in response to a metadata update notification.
+ internal static class RuntimeTypeMetadataUpdateHandler
+ {
+ public static void BeforeUpdate(Type? type)
+ {
+ if (type is RuntimeType rt)
+ {
+ rt.ClearCache();
+ }
+
+ // TODO: https://github.com/dotnet/runtime/issues/50938
+ // Do we need to clear the cache on other types, e.g. ones derived from this one?
+ // Do we need to clear a cache on any other kinds of types?
+ }
+ }
+}
diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs
index a1d174667db10f..a6d28b77126915 100644
--- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs
+++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs
@@ -2418,6 +2418,37 @@ private RuntimeTypeCache InitializeCache()
return cache;
}
+ internal void ClearCache()
+ {
+ // If there isn't a GCHandle yet, there's nothing more to do.
+ if (Volatile.Read(ref m_cache) == IntPtr.Zero)
+ {
+ return;
+ }
+
+ // Loop until the cache is successfully zero'd out.
+ do
+ {
+ // If the GCHandle doesn't wrap a cache yet, there's nothing more to do.
+ RuntimeTypeCache? existingCache = (RuntimeTypeCache?)GCHandle.InternalGet(m_cache);
+ if (existingCache is null)
+ {
+ return;
+ }
+
+ // Create a new, empty cache to replace the old one and try to substitute it in.
+ var newCache = new RuntimeTypeCache(this);
+ if (ReferenceEquals(GCHandle.InternalCompareExchange(m_cache, newCache, existingCache), existingCache))
+ {
+ // We were successful, so there's nothing more to do.
+ return;
+ }
+
+ // We raced with someone else to initialize the cache. Try again.
+ }
+ while (true);
+ }
+
private string? GetDefaultMemberName()
{
return Cache.GetDefaultMemberName();
diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.LinkAttributes.Shared.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.LinkAttributes.Shared.xml
index 8df05e609708b6..d910ad1eb64186 100644
--- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.LinkAttributes.Shared.xml
+++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.LinkAttributes.Shared.xml
@@ -29,6 +29,11 @@
+
+
+
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 9256e41f8a6332..d8cb1adbc37bc2 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -616,6 +616,7 @@
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Metadata/MetadataUpdateHandlerAttribute.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Metadata/MetadataUpdateHandlerAttribute.cs
new file mode 100644
index 00000000000000..7af3be1644d8ac
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Metadata/MetadataUpdateHandlerAttribute.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Reflection.Metadata
+{
+ /// Specifies a type that should receive notifications of metadata updates.
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class MetadataUpdateHandlerAttribute : Attribute
+ {
+ /// Initializes the attribute.
+ /// A type that handles metadata updates and that should be notified when any occur.
+ public MetadataUpdateHandlerAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type handlerType) =>
+ HandlerType = handlerType;
+
+ /// Gets the type that handles metadata updates and that should be notified when any occur.
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
+ public Type HandlerType { get; }
+ }
+}
diff --git a/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs b/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs
index 8279cfa8b9c43e..e42a0642b32ea9 100644
--- a/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs
+++ b/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs
@@ -12,6 +12,13 @@ public static partial class AssemblyExtensions
public unsafe static bool TryGetRawMetadata(this System.Reflection.Assembly assembly, out byte* blob, out int length) { throw null; }
public static void ApplyUpdate(Assembly assembly, ReadOnlySpan metadataDelta, ReadOnlySpan ilDelta, ReadOnlySpan pdbDelta) { throw null; }
}
+ [System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class MetadataUpdateHandlerAttribute : System.Attribute
+ {
+ public MetadataUpdateHandlerAttribute([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type handlerType) { }
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)]
+ public System.Type HandlerType { get { throw null; } }
+ }
}
namespace System.Runtime.Loader
{
diff --git a/src/libraries/System.Runtime.Loader/tests/MetadataUpdateHandlerAttributeTest.cs b/src/libraries/System.Runtime.Loader/tests/MetadataUpdateHandlerAttributeTest.cs
new file mode 100644
index 00000000000000..59c61d7d8a65df
--- /dev/null
+++ b/src/libraries/System.Runtime.Loader/tests/MetadataUpdateHandlerAttributeTest.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace System.Reflection.Metadata
+{
+ public class MetadataUpdateHandlerAttributeTest
+ {
+ [Fact]
+ public void Ctor_RoundtripType()
+ {
+ Type t = typeof(MetadataUpdateHandlerAttributeTest);
+ var a = new MetadataUpdateHandlerAttribute(t);
+ Assert.Same(t, a.HandlerType);
+ }
+ }
+}
diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj
index 11c1b7577fff9d..1ab71b1f41abfe 100644
--- a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj
+++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj
@@ -13,6 +13,7 @@
+
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
index e4b0b3580f5853..dea6068d8881a6 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
@@ -206,6 +206,7 @@
+
diff --git a/src/libraries/System.Runtime/tests/System/Reflection/ReflectionCacheTests.cs b/src/libraries/System.Runtime/tests/System/Reflection/ReflectionCacheTests.cs
new file mode 100644
index 00000000000000..f295be7f73faf0
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/System/Reflection/ReflectionCacheTests.cs
@@ -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 Xunit;
+
+namespace System.Reflection.Tests
+{
+ public class ReflectionCacheTests
+ {
+ [Fact]
+ public void GetMethod_MultipleCalls_SameObjects()
+ {
+ MethodInfo mi1 = typeof(ReflectionCacheTests).GetMethod(nameof(GetMethod_MultipleCalls_SameObjects));
+ Assert.NotNull(mi1);
+
+ MethodInfo mi2 = typeof(ReflectionCacheTests).GetMethod(nameof(GetMethod_MultipleCalls_SameObjects));
+ Assert.NotNull(mi2);
+
+ Assert.Same(mi1, mi2);
+ }
+
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/50978", TestRuntimes.Mono)]
+ [Fact]
+ public void GetMethod_MultipleCalls_ClearCache_DifferentObjects()
+ {
+ Type updateHandler = typeof(Type).Assembly.GetType("System.Reflection.Metadata.RuntimeTypeMetadataUpdateHandler", throwOnError: true, ignoreCase: false);
+ MethodInfo beforeUpdate = updateHandler.GetMethod("BeforeUpdate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(Type) });
+ Assert.NotNull(beforeUpdate);
+
+ MethodInfo mi1 = typeof(ReflectionCacheTests).GetMethod(nameof(GetMethod_MultipleCalls_ClearCache_DifferentObjects));
+ Assert.NotNull(mi1);
+ Assert.Equal(nameof(GetMethod_MultipleCalls_ClearCache_DifferentObjects), mi1.Name);
+
+ beforeUpdate.Invoke(null, new object[] { typeof(ReflectionCacheTests) });
+
+ MethodInfo mi2 = typeof(ReflectionCacheTests).GetMethod(nameof(GetMethod_MultipleCalls_ClearCache_DifferentObjects));
+ Assert.NotNull(mi2);
+ Assert.Equal(nameof(GetMethod_MultipleCalls_ClearCache_DifferentObjects), mi2.Name);
+
+ Assert.NotSame(mi1, mi2);
+ }
+ }
+}