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 index 25f29ed8e596eb..359653c22d716d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Reflection/Metadata/RuntimeTypeMetadataUpdateHandler.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Reflection/Metadata/RuntimeTypeMetadataUpdateHandler.cs @@ -1,6 +1,7 @@ // 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; using System.Reflection.Metadata; [assembly: MetadataUpdateHandler(typeof(RuntimeTypeMetadataUpdateHandler))] @@ -10,16 +11,76 @@ 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) + /// Clear type caches in response to an update notification. + /// The specific types to be cleared, or null to clear everything. + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Clearing the caches on a Type isn't affected if a Type is trimmed, or has any of its members trimmed.")] + public static void BeforeUpdate(Type[]? types) { - if (type is RuntimeType rt) + if (RequiresClearingAllTypes(types)) { - rt.ClearCache(); + // TODO: This should ideally be in a QCall in the runtime. As written here: + // - This materializes all types in the app, creating RuntimeTypes for them. + // - This force loads all assembly dependencies, which might not work well for packages that may depend on resolve events firing at the right moment. + // - This does not cover instantiated generic types. + + // Clear the caches on all loaded types. + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (!SkipAssembly(assembly)) + { + try + { + foreach (Type type in assembly.GetTypes()) + { + ClearCache(type); + } + } + catch (ReflectionTypeLoadException) { } + } + } + } + else + { + // Clear the caches on just the specified types. + foreach (Type type in types) + { + ClearCache(type); + } + } + } + + /// When clearing all types, determines whether we should skip types from the specified assembly. + private static bool SkipAssembly(Assembly assembly) => + // Ideally we'd skip all of the core libraries, which can't be edited. + // But we can easily skip corelib. + typeof(object).Assembly == assembly; + + /// Clears the cache on the specified type. + private static void ClearCache(Type type) => (type as RuntimeType)?.ClearCache(); + + /// Determines whether we need to clear the caches on all types or just the ones specified. + /// The types to examine. + /// true if we need to clear all types; false if we can clear just the ones specified. + private static bool RequiresClearingAllTypes([NotNullWhen(false)] Type[]? types) + { + // If we were handed null, assume we need to clear everything. + if (types is null) + { + return true; + } + + // If any type isn't sealed, we may need to clear a derived type, + // so clear everything. + foreach (Type type in types) + { + if (!type.IsSealed) + { + return true; + } } - // 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? + // We were handed types, and they were all sealed, so we can just clear them. + return false; } } } diff --git a/src/libraries/System.Runtime/tests/System/Reflection/ReflectionCacheTests.cs b/src/libraries/System.Runtime/tests/System/Reflection/ReflectionCacheTests.cs index f295be7f73faf0..4136b2e1d2d566 100644 --- a/src/libraries/System.Runtime/tests/System/Reflection/ReflectionCacheTests.cs +++ b/src/libraries/System.Runtime/tests/System/Reflection/ReflectionCacheTests.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Tests; using Xunit; namespace System.Reflection.Tests { + [Collection(nameof(NoParallelTests))] public class ReflectionCacheTests { [Fact] @@ -21,17 +23,28 @@ public void GetMethod_MultipleCalls_SameObjects() [ActiveIssue("https://github.com/dotnet/runtime/issues/50978", TestRuntimes.Mono)] [Fact] - public void GetMethod_MultipleCalls_ClearCache_DifferentObjects() + public void InvokeBeforeUpdate_NoExceptions() { - 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); + Action beforeUpdate = GetBeforeUpdateMethod(); + beforeUpdate(null); + beforeUpdate(new Type[0]); + beforeUpdate(new Type[] { typeof(ReflectionCacheTests) }); + beforeUpdate(new Type[] { typeof(string), typeof(int) }); + } + + [ActiveIssue("https://github.com/dotnet/runtime/issues/50978", TestRuntimes.Mono)] + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetMethod_MultipleCalls_ClearCache_DifferentObjects(bool justSpecificType) + { + Action beforeUpdate = GetBeforeUpdateMethod(); 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) }); + beforeUpdate(justSpecificType ? new[] { typeof(ReflectionCacheTests) } : null); MethodInfo mi2 = typeof(ReflectionCacheTests).GetMethod(nameof(GetMethod_MultipleCalls_ClearCache_DifferentObjects)); Assert.NotNull(mi2); @@ -39,5 +52,13 @@ public void GetMethod_MultipleCalls_ClearCache_DifferentObjects() Assert.NotSame(mi1, mi2); } + + private static Action GetBeforeUpdateMethod() + { + 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); + return beforeUpdate.CreateDelegate>(); + } } }