From f5670fe80e66e08d6870440823fc06bc0025b8ea Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 00:44:59 -0800 Subject: [PATCH 01/11] feat: Implement AssemblyUtils for type retrieval and caching, refactor TypeUtils to utilize AssemblyUtils --- .../src/Utils/AssemblyUtilsTests.cs | 451 ++++++++++++++++++ .../src/Reflector/Reflector.FindMethod.cs | 4 +- ReflectorNet/src/Utils/AssemblyUtils.cs | 78 +++ ReflectorNet/src/Utils/TypeUtils.cs | 17 +- 4 files changed, 543 insertions(+), 7 deletions(-) create mode 100644 ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs create mode 100644 ReflectorNet/src/Utils/AssemblyUtils.cs diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs new file mode 100644 index 00000000..647ee909 --- /dev/null +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -0,0 +1,451 @@ +/* + * ReflectorNet + * Author: Ivan Murzak (https://github.com/IvanMurzak) + * Copyright (c) 2025 Ivan Murzak + * Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information. + */ + +using com.IvanMurzak.ReflectorNet.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace com.IvanMurzak.ReflectorNet.Tests.Utils +{ + public class AssemblyUtilsTests + { + #region GetAssemblyTypes Tests + + [Fact] + public void GetAssemblyTypes_CurrentAssembly_ReturnsTypes() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.NotNull(types); + Assert.NotEmpty(types); + Assert.Contains(typeof(AssemblyUtilsTests), types); + } + + [Fact] + public void GetAssemblyTypes_MscorlibAssembly_ReturnsTypes() + { + // Arrange + var assembly = typeof(object).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.NotNull(types); + Assert.NotEmpty(types); + Assert.Contains(typeof(object), types); + Assert.Contains(typeof(string), types); + } + + [Fact] + public void GetAssemblyTypes_SameAssembly_ReturnsSameInstance() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + + // Act + var types1 = AssemblyUtils.GetAssemblyTypes(assembly); + var types2 = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert - should return the exact same array instance (cached) + Assert.Same(types1, types2); + } + + [Fact] + public void GetAssemblyTypes_MultipleAssemblies_ReturnsDifferentArrays() + { + // Arrange + var testAssembly = typeof(AssemblyUtilsTests).Assembly; + var coreAssembly = typeof(object).Assembly; + + // Act + var testTypes = AssemblyUtils.GetAssemblyTypes(testAssembly); + var coreTypes = AssemblyUtils.GetAssemblyTypes(coreAssembly); + + // Assert + Assert.NotSame(testTypes, coreTypes); + } + + [Fact] + public void GetAssemblyTypes_ReturnsNonNullTypes() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.All(types, t => Assert.NotNull(t)); + } + + [Fact] + public void GetAssemblyTypes_ReflectorNetAssembly_ContainsExpectedTypes() + { + // Arrange + var assembly = typeof(AssemblyUtils).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.Contains(types, t => t.Name == "AssemblyUtils"); + Assert.Contains(types, t => t.Name == "TypeUtils"); + Assert.Contains(types, t => t.Name == "Reflector"); + } + + #endregion + + #region AllTypes Tests + + [Fact] + public void AllTypes_ReturnsTypes() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.NotEmpty(allTypes); + } + + [Fact] + public void AllTypes_ContainsCommonTypes() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(typeof(object), allTypes); + Assert.Contains(typeof(string), allTypes); + Assert.Contains(typeof(int), allTypes); + } + + [Fact] + public void AllTypes_ContainsTestTypes() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(typeof(AssemblyUtilsTests), allTypes); + } + + [Fact] + public void AllTypes_ContainsAssemblyUtilsType() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(typeof(AssemblyUtils), allTypes); + } + + [Fact] + public void AllTypes_AllTypesAreNotNull() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.All(allTypes, t => Assert.NotNull(t)); + } + + [Fact] + public void AllTypes_MultipleEnumerations_Succeed() + { + // Act - enumerate multiple times + var count1 = AssemblyUtils.AllTypes.Count(); + var count2 = AssemblyUtils.AllTypes.Count(); + + // Assert - both enumerations should return types + // Note: counts may differ slightly if new assemblies are loaded between enumerations + Assert.True(count1 > 0, "First enumeration should return types"); + Assert.True(count2 > 0, "Second enumeration should return types"); + // The counts should be reasonably close (within 10% or 500 types) + var difference = Math.Abs(count1 - count2); + var maxAllowedDifference = Math.Max(500, count1 / 10); + Assert.True(difference <= maxAllowedDifference, + $"Counts differ too much: {count1} vs {count2} (difference: {difference})"); + } + + [Fact] + public void AllTypes_CanIterateWithForeach() + { + // Act + var count = 0; + foreach (var type in AssemblyUtils.AllTypes) + { + Assert.NotNull(type); + count++; + if (count > 100) break; // Don't iterate all to keep test fast + } + + // Assert + Assert.True(count > 100); + } + + [Fact] + public void AllTypes_ContainsTypesFromMultipleAssemblies() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Get assemblies that contributed types + var assemblies = allTypes.Select(t => t.Assembly).Distinct().ToList(); + + // Assert - should have types from multiple assemblies + Assert.True(assemblies.Count > 1, "Should contain types from multiple assemblies"); + } + + #endregion + + #region Thread Safety Tests + + [Fact] + public void GetAssemblyTypes_ConcurrentAccess_ReturnsSameInstance() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + var results = new Type[10][]; + var barrier = new Barrier(10); + + // Act - access from multiple threads simultaneously + Parallel.For(0, 10, i => + { + barrier.SignalAndWait(); // Ensure all threads start at the same time + results[i] = AssemblyUtils.GetAssemblyTypes(assembly); + }); + + // Assert - all should be the same instance + for (int i = 1; i < results.Length; i++) + { + Assert.Same(results[0], results[i]); + } + } + + [Fact] + public void GetAssemblyTypes_ConcurrentAccessDifferentAssemblies_Succeeds() + { + // Arrange + var assemblies = AppDomain.CurrentDomain.GetAssemblies().Take(10).ToArray(); + var results = new Dictionary(); + var lockObj = new object(); + + // Act - access different assemblies from multiple threads + Parallel.ForEach(assemblies, assembly => + { + var types = AssemblyUtils.GetAssemblyTypes(assembly); + lock (lockObj) + { + results[assembly] = types; + } + }); + + // Assert + Assert.Equal(assemblies.Length, results.Count); + foreach (var kvp in results) + { + Assert.NotNull(kvp.Value); + } + } + + [Fact] + public void AllTypes_ConcurrentEnumeration_Succeeds() + { + // Arrange + var counts = new int[5]; + + // Act - enumerate from multiple threads simultaneously + Parallel.For(0, 5, i => + { + counts[i] = AssemblyUtils.AllTypes.Count(); + }); + + // Assert - all counts should be the same + for (int i = 1; i < counts.Length; i++) + { + Assert.Equal(counts[0], counts[i]); + } + } + + #endregion + + #region Edge Cases + + [Fact] + public void AllTypes_ContainsGenericTypeDefinitions() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert - should contain generic type definitions like List<> + Assert.Contains(allTypes, t => t.IsGenericTypeDefinition); + } + + [Fact] + public void AllTypes_ContainsNestedTypes() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert - should contain nested types + Assert.Contains(allTypes, t => t.IsNested); + } + + [Fact] + public void AllTypes_ContainsInterfaces() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(allTypes, t => t.IsInterface); + } + + [Fact] + public void AllTypes_ContainsAbstractClasses() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(allTypes, t => t.IsAbstract && t.IsClass); + } + + [Fact] + public void AllTypes_ContainsEnums() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(allTypes, t => t.IsEnum); + } + + [Fact] + public void AllTypes_ContainsStructs() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(allTypes, t => t.IsValueType && !t.IsEnum && !t.IsPrimitive); + } + + [Fact] + public void GetAssemblyTypes_SystemAssembly_ContainsExceptionTypes() + { + // Arrange + var assembly = typeof(Exception).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.Contains(types, t => typeof(Exception).IsAssignableFrom(t)); + } + + #endregion + + #region Caching Behavior Tests + + [Fact] + public void GetAssemblyTypes_AfterMultipleCalls_MaintainsCache() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + + // Act - call multiple times + var first = AssemblyUtils.GetAssemblyTypes(assembly); + var second = AssemblyUtils.GetAssemblyTypes(assembly); + var third = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert - all should be the same cached instance + Assert.Same(first, second); + Assert.Same(second, third); + } + + [Fact] + public void GetAssemblyTypes_DifferentAssemblies_HaveSeparateCacheEntries() + { + // Arrange + var testAssembly = typeof(AssemblyUtilsTests).Assembly; + var reflectorAssembly = typeof(AssemblyUtils).Assembly; + var systemAssembly = typeof(object).Assembly; + + // Act + var testTypes = AssemblyUtils.GetAssemblyTypes(testAssembly); + var reflectorTypes = AssemblyUtils.GetAssemblyTypes(reflectorAssembly); + var systemTypes = AssemblyUtils.GetAssemblyTypes(systemAssembly); + + // Assert - each should be cached separately + Assert.NotSame(testTypes, reflectorTypes); + Assert.NotSame(reflectorTypes, systemTypes); + Assert.NotSame(testTypes, systemTypes); + + // Verify caching still works + Assert.Same(testTypes, AssemblyUtils.GetAssemblyTypes(testAssembly)); + Assert.Same(reflectorTypes, AssemblyUtils.GetAssemblyTypes(reflectorAssembly)); + Assert.Same(systemTypes, AssemblyUtils.GetAssemblyTypes(systemAssembly)); + } + + #endregion + + #region Performance Sanity Tests + + [Fact] + public void GetAssemblyTypes_CachedAccess_IsFast() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + + // Warm up the cache + _ = AssemblyUtils.GetAssemblyTypes(assembly); + + // Act - measure cached access time + var sw = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = AssemblyUtils.GetAssemblyTypes(assembly); + } + sw.Stop(); + + // Assert - 10000 cached accesses should be very fast (< 100ms is generous) + Assert.True(sw.ElapsedMilliseconds < 100, + $"10000 cached accesses took {sw.ElapsedMilliseconds}ms, expected < 100ms"); + } + + [Fact] + public void AllTypes_SecondEnumeration_UsesCachedData() + { + // Arrange - first enumeration to populate cache + var firstEnumeration = AssemblyUtils.AllTypes.ToList(); + + // Act - measure second enumeration + var sw = System.Diagnostics.Stopwatch.StartNew(); + var secondEnumeration = AssemblyUtils.AllTypes.ToList(); + sw.Stop(); + + // Assert + Assert.Equal(firstEnumeration.Count, secondEnumeration.Count); + // Second enumeration should be reasonably fast due to caching + Assert.True(sw.ElapsedMilliseconds < 1000, + $"Second enumeration took {sw.ElapsedMilliseconds}ms"); + } + + #endregion + } +} diff --git a/ReflectorNet/src/Reflector/Reflector.FindMethod.cs b/ReflectorNet/src/Reflector/Reflector.FindMethod.cs index 27653a1f..2ac403a1 100644 --- a/ReflectorNet/src/Reflector/Reflector.FindMethod.cs +++ b/ReflectorNet/src/Reflector/Reflector.FindMethod.cs @@ -9,7 +9,7 @@ namespace com.IvanMurzak.ReflectorNet { public partial class Reflector { - public static IEnumerable AllMethods => TypeUtils.AllTypes + public static IEnumerable AllMethods => AssemblyUtils.AllTypes .SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) .Where(method => method.DeclaringType != null && !method.DeclaringType.IsAbstract); @@ -152,7 +152,7 @@ public IEnumerable FindMethod( // Prepare Namespace filter.Namespace = filter.Namespace?.Trim()?.Replace(StringUtils.Null, string.Empty); - var typesEnumerable = TypeUtils.AllTypes + var typesEnumerable = AssemblyUtils.AllTypes .Where(type => type.IsVisible) .Where(type => !type.IsInterface) .Where(type => !type.IsAbstract || type.IsSealed) diff --git a/ReflectorNet/src/Utils/AssemblyUtils.cs b/ReflectorNet/src/Utils/AssemblyUtils.cs new file mode 100644 index 00000000..7ea32e57 --- /dev/null +++ b/ReflectorNet/src/Utils/AssemblyUtils.cs @@ -0,0 +1,78 @@ +/* + * ReflectorNet + * Author: Ivan Murzak (https://github.com/IvanMurzak) + * Copyright (c) 2025 Ivan Murzak + * Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information. + */ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace com.IvanMurzak.ReflectorNet.Utils +{ + public static class AssemblyUtils + { + // Thread-safe cache for types per assembly + private static readonly ConcurrentDictionary _assemblyTypesCache = new(); + + public static IEnumerable AllTypes + { + get + { + Assembly[] assemblies; + try + { + assemblies = AppDomain.CurrentDomain.GetAssemblies(); + } + catch (AppDomainUnloadedException) + { + yield break; + } + + for (int i = 0; i < assemblies.Length; i++) + { + var types = GetAssemblyTypes(assemblies[i]); + for (int j = 0; j < types.Length; j++) + { + yield return types[j]; + } + } + } + } + + /// + /// Gets all types from an assembly with thread-safe caching. + /// + /// The assembly to get types from. + /// Array of types from the assembly. + public static Type[] GetAssemblyTypes(Assembly assembly) + { + return _assemblyTypesCache.GetOrAdd(assembly, asm => + { + try + { + return asm.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(t => t != null).ToArray()!; + } + catch + { + return Array.Empty(); + } + }); + } + + /// + /// Clears the assembly types cache. + /// + public static void ClearAssemblyTypesCache() + { + _assemblyTypesCache.Clear(); + } + } +} diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index dbb0adc4..44ccb24f 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -17,11 +17,18 @@ namespace com.IvanMurzak.ReflectorNet.Utils { public static partial class TypeUtils { - public static IEnumerable AllTypes => AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()); + public static IEnumerable AllTypes => AssemblyUtils.AllTypes; // Cache for resolved type names to avoid repeated AllTypes enumeration (thread-safe) - private static readonly ConcurrentDictionary _typeCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _typeCache = new(); + + /// + /// Clears the type name cache. + /// + public static void ClearTypeCache() + { + _typeCache.Clear(); + } public static Type? GetType(string? typeName) { @@ -74,7 +81,7 @@ public static partial class TypeUtils } // If Type.GetType() fails, try to find the type in all loaded assemblies - type = AllTypes.FirstOrDefault(t => + type = AssemblyUtils.AllTypes.FirstOrDefault(t => typeName == t.FullName || typeName == t.AssemblyQualifiedName || typeName == t.GetTypeId()); @@ -94,7 +101,7 @@ public static partial class TypeUtils if (type != null) return type; - return AllTypes.FirstOrDefault(t => + return AssemblyUtils.AllTypes.FirstOrDefault(t => name == t.AssemblyQualifiedName || name == t.FullName || name == t.Name); From f9b0453662e9d55d1a9e6b3137bcab3b488a65ed Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 00:47:54 -0800 Subject: [PATCH 02/11] feat: Enhance AssemblyUtils to include exception-safe retrieval of all loaded assemblies and update related tests --- .../src/Utils/AssemblyUtilsTests.cs | 132 ++++++++++++++++++ ReflectorNet/src/Utils/AssemblyUtils.cs | 25 +++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index 647ee909..af66bdcb 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -109,6 +109,138 @@ public void GetAssemblyTypes_ReflectorNetAssembly_ContainsExpectedTypes() #endregion + #region AllAssemblies Tests + + [Fact] + public void AllAssemblies_ReturnsAssemblies() + { + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.NotEmpty(allAssemblies); + } + + [Fact] + public void AllAssemblies_ContainsCurrentTestAssembly() + { + // Arrange + var testAssembly = typeof(AssemblyUtilsTests).Assembly; + + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.Contains(testAssembly, allAssemblies); + } + + [Fact] + public void AllAssemblies_ContainsReflectorNetAssembly() + { + // Arrange + var reflectorAssembly = typeof(AssemblyUtils).Assembly; + + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.Contains(reflectorAssembly, allAssemblies); + } + + [Fact] + public void AllAssemblies_ContainsCoreLibAssembly() + { + // Arrange + var coreLibAssembly = typeof(object).Assembly; + + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.Contains(coreLibAssembly, allAssemblies); + } + + [Fact] + public void AllAssemblies_AllAssembliesAreNotNull() + { + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.All(allAssemblies, a => Assert.NotNull(a)); + } + + [Fact] + public void AllAssemblies_MultipleEnumerations_Succeed() + { + // Act - enumerate multiple times + var count1 = AssemblyUtils.AllAssemblies.Count(); + var count2 = AssemblyUtils.AllAssemblies.Count(); + + // Assert - both enumerations should return assemblies + // Note: counts may differ slightly if new assemblies are loaded between enumerations + Assert.True(count1 > 0, "First enumeration should return assemblies"); + Assert.True(count2 > 0, "Second enumeration should return assemblies"); + // The counts should be reasonably close (within 50 assemblies) + var difference = Math.Abs(count1 - count2); + Assert.True(difference <= 50, + $"Counts differ too much: {count1} vs {count2} (difference: {difference})"); + } + + [Fact] + public void AllAssemblies_CanIterateWithForeach() + { + // Act + var count = 0; + foreach (var assembly in AssemblyUtils.AllAssemblies) + { + Assert.NotNull(assembly); + count++; + } + + // Assert + Assert.True(count > 0, "Should iterate at least one assembly"); + } + + [Fact] + public void AllAssemblies_ConcurrentEnumeration_Succeeds() + { + // Arrange + var counts = new int[5]; + + // Act - enumerate from multiple threads simultaneously + Parallel.For(0, 5, i => + { + counts[i] = AssemblyUtils.AllAssemblies.Count(); + }); + + // Assert - all counts should be positive + Assert.All(counts, c => Assert.True(c > 0)); + } + + [Fact] + public void AllAssemblies_ContainsMultipleAssemblies() + { + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert - should have multiple assemblies loaded + Assert.True(allAssemblies.Count > 5, $"Expected more than 5 assemblies, got {allAssemblies.Count}"); + } + + [Fact] + public void AllAssemblies_NoDuplicates() + { + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + var distinctCount = allAssemblies.Distinct().Count(); + + // Assert + Assert.Equal(allAssemblies.Count, distinctCount); + } + + #endregion + #region AllTypes Tests [Fact] diff --git a/ReflectorNet/src/Utils/AssemblyUtils.cs b/ReflectorNet/src/Utils/AssemblyUtils.cs index 7ea32e57..ea18d39a 100644 --- a/ReflectorNet/src/Utils/AssemblyUtils.cs +++ b/ReflectorNet/src/Utils/AssemblyUtils.cs @@ -18,7 +18,10 @@ public static class AssemblyUtils // Thread-safe cache for types per assembly private static readonly ConcurrentDictionary _assemblyTypesCache = new(); - public static IEnumerable AllTypes + /// + /// Gets all assemblies loaded in the current application domain with exception protection. + /// + public static IEnumerable AllAssemblies { get { @@ -34,10 +37,24 @@ public static IEnumerable AllTypes for (int i = 0; i < assemblies.Length; i++) { - var types = GetAssemblyTypes(assemblies[i]); - for (int j = 0; j < types.Length; j++) + yield return assemblies[i]; + } + } + } + + /// + /// Gets all types from all loaded assemblies with exception protection. + /// + public static IEnumerable AllTypes + { + get + { + foreach (var assembly in AllAssemblies) + { + var types = GetAssemblyTypes(assembly); + for (int i = 0; i < types.Length; i++) { - yield return types[j]; + yield return types[i]; } } } From 508a10b86a09f665a05bd4336bda63a80dccc5eb Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 00:51:38 -0800 Subject: [PATCH 03/11] chore: Bump version to 3.4.0 --- ReflectorNet/ReflectorNet.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReflectorNet/ReflectorNet.csproj b/ReflectorNet/ReflectorNet.csproj index 19e22289..b3a67747 100644 --- a/ReflectorNet/ReflectorNet.csproj +++ b/ReflectorNet/ReflectorNet.csproj @@ -9,7 +9,7 @@ com.IvanMurzak.ReflectorNet - 3.3.1 + 3.4.0 Ivan Murzak Copyright © Ivan Murzak 2025 ReflectorNet is an advanced .NET reflection toolkit designed for AI-driven scenarios. Effortlessly search for C# methods using natural language queries, invoke any method by supplying arguments as JSON, and receive results as JSON. The library also provides a powerful API to inspect, modify, and manage in-memory object instances dynamically via JSON data. Ideal for automation, testing, and AI integration workflows. From 5baacb50504d5c5ced4f311a3be65e40c3d3691e Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 01:00:28 -0800 Subject: [PATCH 04/11] feat: Improve exception handling in AssemblyUtils and add cache clearing methods in TypeUtils --- .../src/Utils/AssemblyUtilsTests.cs | 6 +++--- ReflectorNet/src/Utils/AssemblyUtils.cs | 16 +++++++++++++- ReflectorNet/src/Utils/TypeUtils.cs | 21 ++++++++++++++++++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index af66bdcb..4eceaae2 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -573,9 +573,9 @@ public void AllTypes_SecondEnumeration_UsesCachedData() // Assert Assert.Equal(firstEnumeration.Count, secondEnumeration.Count); - // Second enumeration should be reasonably fast due to caching - Assert.True(sw.ElapsedMilliseconds < 1000, - $"Second enumeration took {sw.ElapsedMilliseconds}ms"); + // Second enumeration should be fast due to caching (< 200ms is generous for cached data) + Assert.True(sw.ElapsedMilliseconds < 200, + $"Second enumeration took {sw.ElapsedMilliseconds}ms, expected < 200ms"); } #endregion diff --git a/ReflectorNet/src/Utils/AssemblyUtils.cs b/ReflectorNet/src/Utils/AssemblyUtils.cs index ea18d39a..55933658 100644 --- a/ReflectorNet/src/Utils/AssemblyUtils.cs +++ b/ReflectorNet/src/Utils/AssemblyUtils.cs @@ -63,8 +63,17 @@ public static IEnumerable AllTypes /// /// Gets all types from an assembly with thread-safe caching. /// + /// + /// This method handles exceptions gracefully to ensure robust type enumeration: + /// + /// : Returns only the types that loaded successfully. + /// This commonly occurs when an assembly references types from dependencies that are not loaded. + /// Other exceptions (e.g., , ): + /// Returns an empty array. These can occur with dynamic assemblies, native assemblies, or corrupted modules. + /// + /// /// The assembly to get types from. - /// Array of types from the assembly. + /// Array of types from the assembly. May be empty if the assembly cannot be inspected. public static Type[] GetAssemblyTypes(Assembly assembly) { return _assemblyTypesCache.GetOrAdd(assembly, asm => @@ -75,10 +84,15 @@ public static Type[] GetAssemblyTypes(Assembly assembly) } catch (ReflectionTypeLoadException ex) { + // Some types failed to load (e.g., missing dependencies). + // Return the types that did load successfully. return ex.Types.Where(t => t != null).ToArray()!; } catch { + // Other exceptions (FileNotFoundException, BadImageFormatException, etc.) + // can occur with dynamic assemblies, native assemblies, or corrupted modules. + // Return empty array to allow enumeration to continue with other assemblies. return Array.Empty(); } }); diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index 44ccb24f..3a500783 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -23,13 +23,32 @@ public static partial class TypeUtils private static readonly ConcurrentDictionary _typeCache = new(); /// - /// Clears the type name cache. + /// Clears the type name resolution cache. /// + /// + /// This only clears the type name to resolution cache in . + /// It does not clear the assembly types cache in . + /// To clear all reflection caches, use instead. + /// public static void ClearTypeCache() { _typeCache.Clear(); } + /// + /// Clears all reflection-related caches, including both the type name resolution cache + /// and the assembly types cache. + /// + /// + /// Use this method when you need to ensure completely fresh type enumeration, + /// such as after dynamically loading or unloading assemblies. + /// + public static void ClearAllCaches() + { + _typeCache.Clear(); + AssemblyUtils.ClearAssemblyTypesCache(); + } + public static Type? GetType(string? typeName) { if (string.IsNullOrWhiteSpace(typeName)) From d1cfebaaf303095de9d916cd65f7a8b70dd0d47a Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 01:01:24 -0800 Subject: [PATCH 05/11] feat: Enhance TypeUtils with additional type resolution and description methods --- ReflectorNet/src/Utils/TypeUtils.cs | 173 ++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index 3a500783..640de419 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -17,6 +17,14 @@ namespace com.IvanMurzak.ReflectorNet.Utils { public static partial class TypeUtils { + /// + /// Gets all types from all loaded assemblies. + /// + /// + /// This property delegates to and provides + /// exception-safe enumeration of types across all loaded assemblies. + /// Results are cached per assembly for performance. + /// public static IEnumerable AllTypes => AssemblyUtils.AllTypes; // Cache for resolved type names to avoid repeated AllTypes enumeration (thread-safe) @@ -49,6 +57,23 @@ public static void ClearAllCaches() AssemblyUtils.ClearAssemblyTypesCache(); } + /// + /// Resolves a from its string representation. + /// + /// + /// This method attempts to resolve types using multiple strategies in order: + /// + /// Built-in + /// Array type resolution (e.g., "Namespace.Type[]", "int[,]") + /// C#-style generic types (e.g., "List<int>", "Dictionary<string, int>") + /// CLR-style generic types (e.g., "System.Collections.Generic.List`1[[System.Int32]]") + /// Search across all loaded assemblies by FullName, AssemblyQualifiedName, or TypeId + /// + /// Results are cached for performance. Use to clear the cache. + /// + /// The type name to resolve. Can be a simple name, full name, assembly-qualified name, + /// or C#-style generic syntax. + /// The resolved , or null if the type cannot be found. public static Type? GetType(string? typeName) { if (string.IsNullOrWhiteSpace(typeName)) @@ -495,6 +520,12 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) return args.Count > 0 ? args.ToArray() : null; } + /// + /// Determines whether the specified type is a dictionary type. + /// + /// The type to check. + /// true if the type is , + /// , or implements . public static bool IsDictionary(Type type) { if (type.IsGenericType && @@ -508,6 +539,11 @@ public static bool IsDictionary(Type type) .Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); } + /// + /// Gets the key and value types from a dictionary type. + /// + /// The dictionary type to inspect. + /// An array containing [TKey, TValue] types, or null if the type is not a dictionary. public static Type[]? GetDictionaryGenericArguments(Type type) { if (type.IsGenericType && @@ -523,6 +559,12 @@ public static bool IsDictionary(Type type) return dictionaryInterface?.GetGenericArguments(); } + /// + /// Gets the description from a on a type. + /// + /// The type to get the description from. + /// The description string, or null if no description is found. + /// Falls back to the base type's description if the type itself has none. public static string? GetDescription(Type type) { return type @@ -532,6 +574,13 @@ public static bool IsDictionary(Type type) ? GetDescription(type.BaseType!) : null); } + + /// + /// Gets the description from a on a parameter. + /// + /// The parameter to get the description from. + /// The description string, or null if no description is found. + /// Falls back to the parameter type's description if the parameter itself has none. public static string? GetDescription(ParameterInfo? parameterInfo) { return parameterInfo @@ -541,6 +590,13 @@ public static bool IsDictionary(Type type) ? GetDescription(parameterInfo.ParameterType) : null); } + + /// + /// Gets the description from a on a member. + /// + /// The member to get the description from. + /// The description string, or null if no description is found. + /// For fields and properties, falls back to the member type's description. public static string? GetDescription(MemberInfo? memberInfo) { if (memberInfo == null) @@ -560,6 +616,13 @@ public static bool IsDictionary(Type type) _ => null }; } + + /// + /// Gets the description from a on a field. + /// + /// The field to get the description from. + /// The description string, or null if no description is found. + /// Falls back to the field type's description if the field itself has none. public static string? GetFieldDescription(FieldInfo? fieldInfo) { if (fieldInfo == null) @@ -572,6 +635,13 @@ public static bool IsDictionary(Type type) ? GetDescription(fieldInfo.FieldType) : null); } + + /// + /// Gets the description from a on a property. + /// + /// The property to get the description from. + /// The description string, or null if no description is found. + /// Falls back to the property type's description if the property itself has none. public static string? GetPropertyDescription(PropertyInfo? propertyInfo) { if (propertyInfo == null) @@ -584,12 +654,32 @@ public static bool IsDictionary(Type type) ? GetDescription(propertyInfo.PropertyType) : null); } + + /// + /// Gets the description from a on a property by name. + /// + /// The type containing the property. + /// The name of the property. + /// The description string, or null if the property is not found or has no description. public static string? GetPropertyDescription(Type type, string propertyName) { var propertyInfo = type.GetProperty(propertyName); return propertyInfo != null ? GetPropertyDescription(propertyInfo) : null; } #if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER + /// + /// Gets the description from a using JSON schema exporter context. + /// + /// + /// This method handles JSON naming policy transformations by attempting to match: + /// + /// Exact name match + /// PascalCase conversion from camelCase + /// Case-insensitive match + /// + /// + /// The JSON schema exporter context containing property information. + /// The description string, or null if no description is found. public static string? GetPropertyDescription(System.Text.Json.Schema.JsonSchemaExporterContext context) { if (context.PropertyInfo == null || context.PropertyInfo.DeclaringType == null) @@ -636,6 +726,13 @@ private static string ToPascalCase(string camelCase) return char.ToUpperInvariant(camelCase[0]) + camelCase.Substring(1); } + + /// + /// Gets the description from a on a field by name. + /// + /// The type containing the field. + /// The name of the field. + /// The description string, or null if the field is not found or has no description. public static string? GetFieldDescription(Type type, string fieldName) { var fieldInfo = type.GetField(fieldName); @@ -662,6 +759,21 @@ public static bool IsAssignableTo(object? obj, Type targetType) return targetType.IsAssignableFrom(obj.GetType()); } + /// + /// Determines whether a type can be cast to another type. + /// + /// + /// This method checks for: + /// + /// Direct assignability (including inheritance) + /// Primitive type conversions + /// String to object conversion + /// + /// Nullable types are unwrapped before comparison. + /// + /// The source type to cast from. + /// The target type to cast to. + /// true if the cast is possible; otherwise, false. public static bool IsCastable(Type? type, Type to) { if (type == null || to == null) @@ -688,6 +800,13 @@ public static bool IsCastable(Type? type, Type to) return false; } + /// + /// Calculates the inheritance distance between two types. + /// + /// The base type (ancestor). + /// The target type (descendant). + /// The number of inheritance levels between the types, or -1 if + /// does not inherit from . public static int GetInheritanceDistance(Type baseType, Type targetType) { if (!baseType.IsAssignableFrom(targetType)) @@ -702,6 +821,17 @@ public static int GetInheritanceDistance(Type baseType, Type targetType) } return current == baseType ? distance : -1; } + + /// + /// Determines whether a type is considered a primitive type for serialization purposes. + /// + /// + /// This includes CLR primitives, enums, and common value types: + /// , , , + /// , , and . + /// + /// The type to check. + /// true if the type is a primitive or primitive-like type. public static bool IsPrimitive(Type type) { return type.IsPrimitive || @@ -713,6 +843,13 @@ public static bool IsPrimitive(Type type) type == typeof(TimeSpan) || type == typeof(Guid); } + + /// + /// Recursively enumerates all generic type arguments from a type and its base types. + /// + /// The type to extract generic arguments from. + /// Optional set to track visited types and prevent infinite recursion. + /// An enumerable of all generic type arguments found in the type hierarchy. public static IEnumerable GetGenericTypes(Type type, HashSet? visited = null) { visited ??= new HashSet(); @@ -748,6 +885,12 @@ public static IEnumerable GetGenericTypes(Type type, HashSet? visited foreach (var baseGenericType in GetGenericTypes(type.BaseType, visited)) yield return baseGenericType; } + + /// + /// Determines whether a type implements . + /// + /// The type to check. + /// true if the type is an array or implements . public static bool IsIEnumerable(Type type) { if (type.IsArray) @@ -759,6 +902,12 @@ public static bool IsIEnumerable(Type type) return type.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); } + + /// + /// Gets the element type of an enumerable or array type. + /// + /// The enumerable type to inspect. + /// The element type (T in ), or null if the type is not enumerable. public static Type? GetEnumerableItemType(Type type) { if (type.IsArray) @@ -794,6 +943,14 @@ public static bool IsIEnumerable(Type type) return null; } + + /// + /// Resolves a type with priority given to the object's runtime type. + /// + /// The object whose runtime type to use (if not null). + /// The fallback type to use if the object is null. + /// Set to an error message if the type cannot be resolved; otherwise, null. + /// The resolved type, or null if resolution fails. public static Type? GetTypeWithObjectPriority(object? obj, Type? fallbackType, out string? error) { var type = obj?.GetType() ?? fallbackType; @@ -806,6 +963,14 @@ public static bool IsIEnumerable(Type type) error = null; return type; } + + /// + /// Resolves a type with priority given to the . + /// + /// The serialized member containing the type name. + /// The fallback type to use if the type name cannot be resolved. + /// Set to an error message if the type cannot be resolved; otherwise, null. + /// The resolved type, or null if resolution fails. public static Type? GetTypeWithNamePriority(SerializedMember? member, Type? fallbackType, out string? error) { if (StringUtils.IsNullOrEmpty(member?.typeName) && fallbackType == null) @@ -829,6 +994,14 @@ public static bool IsIEnumerable(Type type) error = null; return type; } + + /// + /// Resolves a type with priority given to the provided type parameter. + /// + /// The primary type to use (if not null). + /// The fallback serialized member to extract type name from. + /// Set to an error message if the type cannot be resolved; otherwise, null. + /// The resolved type, or null if resolution fails. public static Type? GetTypeWithValuePriority(Type? type, SerializedMember? fallbackMember, out string? error) { if (type == null) From aa1c80bcb7f74f51e81bb7a99bc21c44016627b8 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 01:02:56 -0800 Subject: [PATCH 06/11] test: Improve caching behavior tests in AllTypes method for accuracy and performance --- .../src/Utils/AssemblyUtilsTests.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index 4eceaae2..2c26ef45 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -563,19 +563,30 @@ public void GetAssemblyTypes_CachedAccess_IsFast() [Fact] public void AllTypes_SecondEnumeration_UsesCachedData() { - // Arrange - first enumeration to populate cache + // Arrange - clear all caches to ensure a cold start + TypeUtils.ClearAllCaches(); + + // Act - measure first enumeration (cold, no cache) + var swFirst = System.Diagnostics.Stopwatch.StartNew(); var firstEnumeration = AssemblyUtils.AllTypes.ToList(); + swFirst.Stop(); - // Act - measure second enumeration - var sw = System.Diagnostics.Stopwatch.StartNew(); + // Act - measure second enumeration (warm, cached) + var swSecond = System.Diagnostics.Stopwatch.StartNew(); var secondEnumeration = AssemblyUtils.AllTypes.ToList(); - sw.Stop(); - - // Assert - Assert.Equal(firstEnumeration.Count, secondEnumeration.Count); - // Second enumeration should be fast due to caching (< 200ms is generous for cached data) - Assert.True(sw.ElapsedMilliseconds < 200, - $"Second enumeration took {sw.ElapsedMilliseconds}ms, expected < 200ms"); + swSecond.Stop(); + + // Assert - counts may differ slightly if new assemblies are loaded between enumerations + // (e.g., by the test framework), so we allow some tolerance + var countDifference = Math.Abs(firstEnumeration.Count - secondEnumeration.Count); + var maxAllowedDifference = Math.Max(500, firstEnumeration.Count / 10); + Assert.True(countDifference <= maxAllowedDifference, + $"Counts differ too much: {firstEnumeration.Count} vs {secondEnumeration.Count} (difference: {countDifference})"); + + // Second enumeration should be faster due to caching (at least 10ms improvement) + var timeImprovement = swFirst.ElapsedMilliseconds - swSecond.ElapsedMilliseconds; + Assert.True(timeImprovement >= 10, + $"Caching should provide at least 10ms improvement. First: {swFirst.ElapsedMilliseconds}ms, Second: {swSecond.ElapsedMilliseconds}ms, Improvement: {timeImprovement}ms"); } #endregion From 57598261c990172294ebae67a0727ae5d421a203 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 01:09:17 -0800 Subject: [PATCH 07/11] test: Add cache-sensitive tests for AssemblyUtils and improve performance assertions --- .../src/Utils/AssemblyUtilsTests.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index 2c26ef45..8585c60f 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -535,8 +535,23 @@ public void GetAssemblyTypes_DifferentAssemblies_HaveSeparateCacheEntries() } #endregion + } + + #region Performance Sanity Tests - #region Performance Sanity Tests + /// + /// Collection definition that disables parallelization for cache-sensitive tests. + /// + [CollectionDefinition("CacheSensitive", DisableParallelization = true)] + public class CacheSensitiveCollection { } + + /// + /// Tests that require isolated execution due to cache manipulation. + /// These tests run sequentially and not in parallel with other tests. + /// + [Collection("CacheSensitive")] + public class AssemblyUtilsCacheSensitiveTests + { [Fact] public void GetAssemblyTypes_CachedAccess_IsFast() @@ -559,7 +574,6 @@ public void GetAssemblyTypes_CachedAccess_IsFast() Assert.True(sw.ElapsedMilliseconds < 100, $"10000 cached accesses took {sw.ElapsedMilliseconds}ms, expected < 100ms"); } - [Fact] public void AllTypes_SecondEnumeration_UsesCachedData() { @@ -583,12 +597,12 @@ public void AllTypes_SecondEnumeration_UsesCachedData() Assert.True(countDifference <= maxAllowedDifference, $"Counts differ too much: {firstEnumeration.Count} vs {secondEnumeration.Count} (difference: {countDifference})"); - // Second enumeration should be faster due to caching (at least 10ms improvement) + // Second enumeration should be faster due to caching (at least 1ms improvement) var timeImprovement = swFirst.ElapsedMilliseconds - swSecond.ElapsedMilliseconds; - Assert.True(timeImprovement >= 10, - $"Caching should provide at least 10ms improvement. First: {swFirst.ElapsedMilliseconds}ms, Second: {swSecond.ElapsedMilliseconds}ms, Improvement: {timeImprovement}ms"); + Assert.True(timeImprovement >= 1, + $"Caching should provide at least 1ms improvement. First: {swFirst.ElapsedMilliseconds}ms, Second: {swSecond.ElapsedMilliseconds}ms, Improvement: {timeImprovement}ms"); } - - #endregion } + + #endregion } From 4f8aecdf291c8ebc123574217ea8cd8fd7109e03 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 01:19:10 -0800 Subject: [PATCH 08/11] test: Improve caching behavior tests in AssemblyUtils and add cache invalidation checks --- .../src/Utils/AssemblyUtilsTests.cs | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index 8585c60f..1c0b39e1 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -552,7 +552,6 @@ public class CacheSensitiveCollection { } [Collection("CacheSensitive")] public class AssemblyUtilsCacheSensitiveTests { - [Fact] public void GetAssemblyTypes_CachedAccess_IsFast() { @@ -575,32 +574,54 @@ public void GetAssemblyTypes_CachedAccess_IsFast() $"10000 cached accesses took {sw.ElapsedMilliseconds}ms, expected < 100ms"); } [Fact] - public void AllTypes_SecondEnumeration_UsesCachedData() + public void AllTypes_CacheReturnsSameArrayInstances() { // Arrange - clear all caches to ensure a cold start TypeUtils.ClearAllCaches(); - // Act - measure first enumeration (cold, no cache) - var swFirst = System.Diagnostics.Stopwatch.StartNew(); - var firstEnumeration = AssemblyUtils.AllTypes.ToList(); - swFirst.Stop(); - - // Act - measure second enumeration (warm, cached) - var swSecond = System.Diagnostics.Stopwatch.StartNew(); - var secondEnumeration = AssemblyUtils.AllTypes.ToList(); - swSecond.Stop(); - - // Assert - counts may differ slightly if new assemblies are loaded between enumerations - // (e.g., by the test framework), so we allow some tolerance - var countDifference = Math.Abs(firstEnumeration.Count - secondEnumeration.Count); - var maxAllowedDifference = Math.Max(500, firstEnumeration.Count / 10); - Assert.True(countDifference <= maxAllowedDifference, - $"Counts differ too much: {firstEnumeration.Count} vs {secondEnumeration.Count} (difference: {countDifference})"); - - // Second enumeration should be faster due to caching (at least 1ms improvement) - var timeImprovement = swFirst.ElapsedMilliseconds - swSecond.ElapsedMilliseconds; - Assert.True(timeImprovement >= 1, - $"Caching should provide at least 1ms improvement. First: {swFirst.ElapsedMilliseconds}ms, Second: {swSecond.ElapsedMilliseconds}ms, Improvement: {timeImprovement}ms"); + // Get a few assemblies to test + var testAssembly = typeof(AssemblyUtilsCacheSensitiveTests).Assembly; + var reflectorAssembly = typeof(AssemblyUtils).Assembly; + var systemAssembly = typeof(object).Assembly; + + // Act - call GetAssemblyTypes twice for each assembly + var testTypes1 = AssemblyUtils.GetAssemblyTypes(testAssembly); + var testTypes2 = AssemblyUtils.GetAssemblyTypes(testAssembly); + + var reflectorTypes1 = AssemblyUtils.GetAssemblyTypes(reflectorAssembly); + var reflectorTypes2 = AssemblyUtils.GetAssemblyTypes(reflectorAssembly); + + var systemTypes1 = AssemblyUtils.GetAssemblyTypes(systemAssembly); + var systemTypes2 = AssemblyUtils.GetAssemblyTypes(systemAssembly); + + // Assert - same array instance should be returned (proves caching works) + Assert.Same(testTypes1, testTypes2); + Assert.Same(reflectorTypes1, reflectorTypes2); + Assert.Same(systemTypes1, systemTypes2); + + // Assert - arrays should contain types + Assert.NotEmpty(testTypes1); + Assert.NotEmpty(reflectorTypes1); + Assert.NotEmpty(systemTypes1); + } + + [Fact] + public void ClearAllCaches_InvalidatesCache() + { + // Arrange - ensure cache is populated + var assembly = typeof(AssemblyUtilsCacheSensitiveTests).Assembly; + var typesBefore = AssemblyUtils.GetAssemblyTypes(assembly); + + // Act - clear caches + TypeUtils.ClearAllCaches(); + + // Get types again after clearing + var typesAfter = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert - should be a different array instance (cache was cleared and repopulated) + // Note: Content should be the same, but it's a new array instance + Assert.NotSame(typesBefore, typesAfter); + Assert.Equal(typesBefore.Length, typesAfter.Length); } } From b75a4ac4076db27cd4a404d7f30610b9c3716e56 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 01:39:41 -0800 Subject: [PATCH 09/11] refactor: Improve exception handling in AssemblyUtils and remove caching logic --- .../src/Utils/AssemblyUtilsTests.cs | 153 ++---------------- ReflectorNet/src/Utils/AssemblyUtils.cs | 49 ++---- ReflectorNet/src/Utils/TypeUtils.cs | 20 --- 3 files changed, 30 insertions(+), 192 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index 1c0b39e1..247286f5 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -51,7 +51,7 @@ public void GetAssemblyTypes_MscorlibAssembly_ReturnsTypes() } [Fact] - public void GetAssemblyTypes_SameAssembly_ReturnsSameInstance() + public void GetAssemblyTypes_SameAssembly_ReturnsDifferentInstances() { // Arrange var assembly = typeof(AssemblyUtilsTests).Assembly; @@ -60,8 +60,9 @@ public void GetAssemblyTypes_SameAssembly_ReturnsSameInstance() var types1 = AssemblyUtils.GetAssemblyTypes(assembly); var types2 = AssemblyUtils.GetAssemblyTypes(assembly); - // Assert - should return the exact same array instance (cached) - Assert.Same(types1, types2); + // Assert - should return different array instances (no caching) + Assert.NotSame(types1, types2); + Assert.Equal(types1.Length, types2.Length); } [Fact] @@ -347,7 +348,7 @@ public void AllTypes_ContainsTypesFromMultipleAssemblies() #region Thread Safety Tests [Fact] - public void GetAssemblyTypes_ConcurrentAccess_ReturnsSameInstance() + public void GetAssemblyTypes_ConcurrentAccess_ReturnsValidTypes() { // Arrange var assembly = typeof(AssemblyUtilsTests).Assembly; @@ -361,10 +362,15 @@ public void GetAssemblyTypes_ConcurrentAccess_ReturnsSameInstance() results[i] = AssemblyUtils.GetAssemblyTypes(assembly); }); - // Assert - all should be the same instance - for (int i = 1; i < results.Length; i++) + // Assert - all should be valid and different instances + for (int i = 0; i < results.Length; i++) { - Assert.Same(results[0], results[i]); + Assert.NotNull(results[i]); + Assert.NotEmpty(results[i]); + if (i > 0) + { + Assert.NotSame(results[0], results[i]); + } } } @@ -492,138 +498,5 @@ public void GetAssemblyTypes_SystemAssembly_ContainsExceptionTypes() #endregion - #region Caching Behavior Tests - - [Fact] - public void GetAssemblyTypes_AfterMultipleCalls_MaintainsCache() - { - // Arrange - var assembly = typeof(AssemblyUtilsTests).Assembly; - - // Act - call multiple times - var first = AssemblyUtils.GetAssemblyTypes(assembly); - var second = AssemblyUtils.GetAssemblyTypes(assembly); - var third = AssemblyUtils.GetAssemblyTypes(assembly); - - // Assert - all should be the same cached instance - Assert.Same(first, second); - Assert.Same(second, third); - } - - [Fact] - public void GetAssemblyTypes_DifferentAssemblies_HaveSeparateCacheEntries() - { - // Arrange - var testAssembly = typeof(AssemblyUtilsTests).Assembly; - var reflectorAssembly = typeof(AssemblyUtils).Assembly; - var systemAssembly = typeof(object).Assembly; - - // Act - var testTypes = AssemblyUtils.GetAssemblyTypes(testAssembly); - var reflectorTypes = AssemblyUtils.GetAssemblyTypes(reflectorAssembly); - var systemTypes = AssemblyUtils.GetAssemblyTypes(systemAssembly); - - // Assert - each should be cached separately - Assert.NotSame(testTypes, reflectorTypes); - Assert.NotSame(reflectorTypes, systemTypes); - Assert.NotSame(testTypes, systemTypes); - - // Verify caching still works - Assert.Same(testTypes, AssemblyUtils.GetAssemblyTypes(testAssembly)); - Assert.Same(reflectorTypes, AssemblyUtils.GetAssemblyTypes(reflectorAssembly)); - Assert.Same(systemTypes, AssemblyUtils.GetAssemblyTypes(systemAssembly)); - } - - #endregion } - - #region Performance Sanity Tests - - /// - /// Collection definition that disables parallelization for cache-sensitive tests. - /// - [CollectionDefinition("CacheSensitive", DisableParallelization = true)] - public class CacheSensitiveCollection { } - - /// - /// Tests that require isolated execution due to cache manipulation. - /// These tests run sequentially and not in parallel with other tests. - /// - [Collection("CacheSensitive")] - public class AssemblyUtilsCacheSensitiveTests - { - [Fact] - public void GetAssemblyTypes_CachedAccess_IsFast() - { - // Arrange - var assembly = typeof(AssemblyUtilsTests).Assembly; - - // Warm up the cache - _ = AssemblyUtils.GetAssemblyTypes(assembly); - - // Act - measure cached access time - var sw = System.Diagnostics.Stopwatch.StartNew(); - for (int i = 0; i < 10000; i++) - { - _ = AssemblyUtils.GetAssemblyTypes(assembly); - } - sw.Stop(); - - // Assert - 10000 cached accesses should be very fast (< 100ms is generous) - Assert.True(sw.ElapsedMilliseconds < 100, - $"10000 cached accesses took {sw.ElapsedMilliseconds}ms, expected < 100ms"); - } - [Fact] - public void AllTypes_CacheReturnsSameArrayInstances() - { - // Arrange - clear all caches to ensure a cold start - TypeUtils.ClearAllCaches(); - - // Get a few assemblies to test - var testAssembly = typeof(AssemblyUtilsCacheSensitiveTests).Assembly; - var reflectorAssembly = typeof(AssemblyUtils).Assembly; - var systemAssembly = typeof(object).Assembly; - - // Act - call GetAssemblyTypes twice for each assembly - var testTypes1 = AssemblyUtils.GetAssemblyTypes(testAssembly); - var testTypes2 = AssemblyUtils.GetAssemblyTypes(testAssembly); - - var reflectorTypes1 = AssemblyUtils.GetAssemblyTypes(reflectorAssembly); - var reflectorTypes2 = AssemblyUtils.GetAssemblyTypes(reflectorAssembly); - - var systemTypes1 = AssemblyUtils.GetAssemblyTypes(systemAssembly); - var systemTypes2 = AssemblyUtils.GetAssemblyTypes(systemAssembly); - - // Assert - same array instance should be returned (proves caching works) - Assert.Same(testTypes1, testTypes2); - Assert.Same(reflectorTypes1, reflectorTypes2); - Assert.Same(systemTypes1, systemTypes2); - - // Assert - arrays should contain types - Assert.NotEmpty(testTypes1); - Assert.NotEmpty(reflectorTypes1); - Assert.NotEmpty(systemTypes1); - } - - [Fact] - public void ClearAllCaches_InvalidatesCache() - { - // Arrange - ensure cache is populated - var assembly = typeof(AssemblyUtilsCacheSensitiveTests).Assembly; - var typesBefore = AssemblyUtils.GetAssemblyTypes(assembly); - - // Act - clear caches - TypeUtils.ClearAllCaches(); - - // Get types again after clearing - var typesAfter = AssemblyUtils.GetAssemblyTypes(assembly); - - // Assert - should be a different array instance (cache was cleared and repopulated) - // Note: Content should be the same, but it's a new array instance - Assert.NotSame(typesBefore, typesAfter); - Assert.Equal(typesBefore.Length, typesAfter.Length); - } - } - - #endregion } diff --git a/ReflectorNet/src/Utils/AssemblyUtils.cs b/ReflectorNet/src/Utils/AssemblyUtils.cs index 55933658..ed0c371b 100644 --- a/ReflectorNet/src/Utils/AssemblyUtils.cs +++ b/ReflectorNet/src/Utils/AssemblyUtils.cs @@ -6,7 +6,6 @@ */ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -15,9 +14,6 @@ namespace com.IvanMurzak.ReflectorNet.Utils { public static class AssemblyUtils { - // Thread-safe cache for types per assembly - private static readonly ConcurrentDictionary _assemblyTypesCache = new(); - /// /// Gets all assemblies loaded in the current application domain with exception protection. /// @@ -61,7 +57,7 @@ public static IEnumerable AllTypes } /// - /// Gets all types from an assembly with thread-safe caching. + /// Gets all types from an assembly. /// /// /// This method handles exceptions gracefully to ensure robust type enumeration: @@ -76,34 +72,23 @@ public static IEnumerable AllTypes /// Array of types from the assembly. May be empty if the assembly cannot be inspected. public static Type[] GetAssemblyTypes(Assembly assembly) { - return _assemblyTypesCache.GetOrAdd(assembly, asm => + try { - try - { - return asm.GetTypes(); - } - catch (ReflectionTypeLoadException ex) - { - // Some types failed to load (e.g., missing dependencies). - // Return the types that did load successfully. - return ex.Types.Where(t => t != null).ToArray()!; - } - catch - { - // Other exceptions (FileNotFoundException, BadImageFormatException, etc.) - // can occur with dynamic assemblies, native assemblies, or corrupted modules. - // Return empty array to allow enumeration to continue with other assemblies. - return Array.Empty(); - } - }); - } - - /// - /// Clears the assembly types cache. - /// - public static void ClearAssemblyTypesCache() - { - _assemblyTypesCache.Clear(); + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + // Some types failed to load (e.g., missing dependencies). + // Return the types that did load successfully. + return ex.Types.Where(t => t != null).ToArray()!; + } + catch + { + // Other exceptions (FileNotFoundException, BadImageFormatException, etc.) + // can occur with dynamic assemblies, native assemblies, or corrupted modules. + // Return empty array to allow enumeration to continue with other assemblies. + return Array.Empty(); + } } } } diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index 640de419..e041e5dd 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -23,7 +23,6 @@ public static partial class TypeUtils /// /// This property delegates to and provides /// exception-safe enumeration of types across all loaded assemblies. - /// Results are cached per assembly for performance. /// public static IEnumerable AllTypes => AssemblyUtils.AllTypes; @@ -33,30 +32,11 @@ public static partial class TypeUtils /// /// Clears the type name resolution cache. /// - /// - /// This only clears the type name to resolution cache in . - /// It does not clear the assembly types cache in . - /// To clear all reflection caches, use instead. - /// public static void ClearTypeCache() { _typeCache.Clear(); } - /// - /// Clears all reflection-related caches, including both the type name resolution cache - /// and the assembly types cache. - /// - /// - /// Use this method when you need to ensure completely fresh type enumeration, - /// such as after dynamically loading or unloading assemblies. - /// - public static void ClearAllCaches() - { - _typeCache.Clear(); - AssemblyUtils.ClearAssemblyTypesCache(); - } - /// /// Resolves a from its string representation. /// From a917985ca4bdd8e129c5f0f43fe20211c721286c Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 01:51:38 -0800 Subject: [PATCH 10/11] fix: Enhance exception handling in AssemblyUtils and improve thread safety tests --- .../src/Utils/AssemblyUtilsTests.cs | 21 +++++++++++-------- ReflectorNet/src/Utils/AssemblyUtils.cs | 5 ++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index 247286f5..b776c8d8 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -353,14 +353,15 @@ public void GetAssemblyTypes_ConcurrentAccess_ReturnsValidTypes() // Arrange var assembly = typeof(AssemblyUtilsTests).Assembly; var results = new Type[10][]; - var barrier = new Barrier(10); - - // Act - access from multiple threads simultaneously - Parallel.For(0, 10, i => + using (var barrier = new Barrier(10)) { - barrier.SignalAndWait(); // Ensure all threads start at the same time - results[i] = AssemblyUtils.GetAssemblyTypes(assembly); - }); + // Act - access from multiple threads simultaneously + Parallel.For(0, 10, i => + { + barrier.SignalAndWait(); // Ensure all threads start at the same time + results[i] = AssemblyUtils.GetAssemblyTypes(assembly); + }); + } // Assert - all should be valid and different instances for (int i = 0; i < results.Length; i++) @@ -411,10 +412,12 @@ public void AllTypes_ConcurrentEnumeration_Succeeds() { counts[i] = AssemblyUtils.AllTypes.Count(); }); - - // Assert - all counts should be the same + // Assert - concurrent enumerations should produce similar counts, + // allowing for minor variations due to dynamic assembly loading for (int i = 1; i < counts.Length; i++) { + var diff = Math.Abs(counts[0] - counts[i]); + Assert.InRange(diff, 0, 50); Assert.Equal(counts[0], counts[i]); } } diff --git a/ReflectorNet/src/Utils/AssemblyUtils.cs b/ReflectorNet/src/Utils/AssemblyUtils.cs index ed0c371b..18c3b643 100644 --- a/ReflectorNet/src/Utils/AssemblyUtils.cs +++ b/ReflectorNet/src/Utils/AssemblyUtils.cs @@ -80,7 +80,10 @@ public static Type[] GetAssemblyTypes(Assembly assembly) { // Some types failed to load (e.g., missing dependencies). // Return the types that did load successfully. - return ex.Types.Where(t => t != null).ToArray()!; + return ex.Types + ?.Where(t => t != null) + .Select(x => x!) + .ToArray() ?? Array.Empty(); } catch { From 94b59eba603155de091828c535bc276c89cdc502 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 8 Jan 2026 09:57:06 -0800 Subject: [PATCH 11/11] fix: Refine thread safety tests in AssemblyUtils by removing redundant assertions --- ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index b776c8d8..e73240b2 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -412,12 +412,9 @@ public void AllTypes_ConcurrentEnumeration_Succeeds() { counts[i] = AssemblyUtils.AllTypes.Count(); }); - // Assert - concurrent enumerations should produce similar counts, - // allowing for minor variations due to dynamic assembly loading for (int i = 1; i < counts.Length; i++) { var diff = Math.Abs(counts[0] - counts[i]); - Assert.InRange(diff, 0, 50); Assert.Equal(counts[0], counts[i]); } }