From 7e52a35f97c5d10f30ab176d38d6e06083315087 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 29 Jan 2026 21:45:49 -0800 Subject: [PATCH 01/17] feat: add assembly-prefixed blacklist methods for targeted type resolution --- .../ReflectorTests/IsTypeBlacklistedTests.cs | 461 ++++++++++++++++++ .../src/Reflector/Reflector.Registry.cs | 71 +++ 2 files changed, 532 insertions(+) diff --git a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs index a4871671..eee42744 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs @@ -1515,5 +1515,466 @@ public void IsTypeBlacklisted_CacheInvalidation_ReturnsCorrectResultAfterBlackli } #endregion + + #region Assembly-Prefixed Blacklist Method Tests + + [Fact] + public void BlacklistType_WithAssemblyPrefix_TypeInMatchingAssembly_IsBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var typeFullName = typeof(BlacklistedBaseClass).FullName!; + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistType(assemblyName, typeFullName); + + // Assert + Assert.True(result, "BlacklistType should return true when type is added"); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + _output.WriteLine($"Type '{typeFullName}' from assembly '{assemblyName}' is correctly blacklisted"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_TypeInNonMatchingAssembly_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var typeFullName = typeof(BlacklistedBaseClass).FullName!; + + // Act - Use a prefix that won't match the assembly containing BlacklistedBaseClass + var result = reflector.Converters.BlacklistType("NonExistentAssembly", typeFullName); + + // Assert + Assert.False(result, "BlacklistType should return false when assembly prefix doesn't match"); + Assert.False(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + _output.WriteLine("Type not blacklisted when assembly prefix doesn't match"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_PartialAssemblyName_MatchesAssembly() + { + // Arrange + var reflector = new Reflector(); + var typeFullName = typeof(BlacklistedBaseClass).FullName!; + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + // Use just the first part of the assembly name (e.g., "ReflectorNet" from "ReflectorNet.Tests") + var assemblyPrefix = assemblyName.Split('.')[0]; + + // Act + var result = reflector.Converters.BlacklistType(assemblyPrefix, typeFullName); + + // Assert + Assert.True(result, "BlacklistType should return true when assembly prefix matches"); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + _output.WriteLine($"Type blacklisted using assembly prefix '{assemblyPrefix}'"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_InvalidTypeName_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistType(assemblyName, "This.Type.Does.Not.Exist"); + + // Assert + Assert.False(result, "BlacklistType should return false for non-existent type"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Invalid type name returns false even with valid assembly prefix"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_EmptyAssemblyPrefix_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var typeFullName = typeof(BlacklistedBaseClass).FullName!; + + // Act + var result = reflector.Converters.BlacklistType("", typeFullName); + + // Assert + Assert.False(result, "BlacklistType should return false for empty assembly prefix"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Empty assembly prefix returns false"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_NullAssemblyPrefix_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var typeFullName = typeof(BlacklistedBaseClass).FullName!; + + // Act + var result = reflector.Converters.BlacklistType(null!, typeFullName); + + // Assert + Assert.False(result, "BlacklistType should return false for null assembly prefix"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Null assembly prefix returns false"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_EmptyTypeName_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistType(assemblyName, ""); + + // Assert + Assert.False(result, "BlacklistType should return false for empty type name"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Empty type name returns false"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_NullTypeName_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistType(assemblyName, null!); + + // Assert + Assert.False(result, "BlacklistType should return false for null type name"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Null type name returns false"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_AlreadyBlacklisted_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var typeFullName = typeof(BlacklistedBaseClass).FullName!; + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + reflector.Converters.BlacklistType(assemblyName, typeFullName); + + // Act - Try to add the same type again + var result = reflector.Converters.BlacklistType(assemblyName, typeFullName); + + // Assert + Assert.False(result, "BlacklistType should return false when type already blacklisted"); + _output.WriteLine("BlacklistType with assembly prefix returns false when type already blacklisted"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_DerivedTypesAreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var typeFullName = typeof(BlacklistedBaseClass).FullName!; + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistType(assemblyName, typeFullName); + + // Assert - Derived types should also be blacklisted + Assert.True(result, "BlacklistType should return true when type is added"); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(DerivedFromBlacklisted))); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(DeeplyDerivedFromBlacklisted))); + _output.WriteLine("Derived types are correctly blacklisted after blacklisting base via assembly prefix"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_CacheInvalidation_WorksCorrectly() + { + // Arrange + var reflector = new Reflector(); + var typeFullName = typeof(BlacklistedBaseClass).FullName!; + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Prime cache with false result + Assert.False(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + + // Act + var result = reflector.Converters.BlacklistType(assemblyName, typeFullName); + + // Assert - Cache should be invalidated + Assert.True(result, "BlacklistType should return true when type is added"); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + _output.WriteLine("Cache is correctly invalidated after assembly-prefixed blacklist"); + } + + [Fact] + public void BlacklistType_WithAssemblyPrefix_SystemAssembly_CanBlacklistSystemTypes() + { + // Arrange + var reflector = new Reflector(); + + // Act - System.String is in an assembly starting with "System" or "mscorlib" + // Note: In .NET Core/5+, System.String is in System.Private.CoreLib + var stringType = typeof(string); + var assemblyName = stringType.Assembly.GetName().Name!; + var result = reflector.Converters.BlacklistType(assemblyName, "System.String"); + + // Assert + Assert.True(result, "BlacklistType should return true when type is added"); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(string))); + _output.WriteLine($"System.String blacklisted via assembly prefix '{assemblyName}'"); + } + + #endregion + + #region Assembly-Prefixed Batch Blacklist Method Tests + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_MultipleTypes_AllAreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly( + assemblyName, + typeof(BlacklistedBaseClass).FullName!, + typeof(NonBlacklistedClass).FullName!, + typeof(IBlacklistedInterface).FullName!); + + // Assert + Assert.True(result, "BlacklistTypes should return true when types are added"); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(NonBlacklistedClass))); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(IBlacklistedInterface))); + _output.WriteLine($"Multiple types from assembly '{assemblyName}' are correctly blacklisted"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_MixedValidAndInvalid_OnlyValidAdded() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act - Mix of valid and invalid type names + var result = reflector.Converters.BlacklistTypesInAssembly( + assemblyName, + typeof(BlacklistedBaseClass).FullName!, + "This.Does.Not.Exist", + typeof(NonBlacklistedClass).FullName!); + + // Assert + Assert.True(result, "BlacklistTypes should return true when at least one type is added"); + Assert.Equal(2, reflector.Converters.GetAllBlacklistedTypes().Count); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(NonBlacklistedClass))); + _output.WriteLine("Only valid type names from batch are blacklisted"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_NonMatchingAssembly_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly( + "NonExistentAssembly", + typeof(BlacklistedBaseClass).FullName!, + typeof(NonBlacklistedClass).FullName!); + + // Assert + Assert.False(result, "BlacklistTypes should return false when assembly doesn't match"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("No types blacklisted when assembly prefix doesn't match any assembly"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_EmptyArray_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly(assemblyName, Array.Empty()); + + // Assert + Assert.False(result, "BlacklistTypes should return false when no types are provided"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Empty type array returns false"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_EmptyAssemblyPrefix_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly( + "", + typeof(BlacklistedBaseClass).FullName!); + + // Assert + Assert.False(result, "BlacklistTypes should return false for empty assembly prefix"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Empty assembly prefix returns false"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_NullAssemblyPrefix_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly( + null!, + typeof(BlacklistedBaseClass).FullName!); + + // Assert + Assert.False(result, "BlacklistTypes should return false for null assembly prefix"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Null assembly prefix returns false"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_DuplicateTypes_AddsOnlyOnce() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + var typeName = typeof(BlacklistedBaseClass).FullName!; + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly( + assemblyName, + typeName, + typeName, + typeName); + + // Assert + Assert.True(result, "BlacklistTypes should return true when at least one type is added"); + Assert.Single(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("Duplicate types in batch are correctly deduplicated"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_AlreadyBlacklisted_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + var typeName = typeof(BlacklistedBaseClass).FullName!; + reflector.Converters.BlacklistTypesInAssembly(assemblyName, typeName); + + // Act - Try to add the same type again + var result = reflector.Converters.BlacklistTypesInAssembly(assemblyName, typeName); + + // Assert + Assert.False(result, "BlacklistTypes should return false when all types already blacklisted"); + _output.WriteLine("BlacklistTypes returns false when all types already blacklisted"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_WithNullTypeNames_IgnoresNulls() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly( + assemblyName, + typeof(BlacklistedBaseClass).FullName!, + null!, + typeof(NonBlacklistedClass).FullName!); + + // Assert + Assert.True(result, "BlacklistTypes should return true when at least one type is added"); + Assert.Equal(2, reflector.Converters.GetAllBlacklistedTypes().Count); + _output.WriteLine("Null type names in batch are correctly ignored"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_WithEmptyTypeNames_IgnoresEmpty() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly( + assemblyName, + typeof(BlacklistedBaseClass).FullName!, + "", + typeof(NonBlacklistedClass).FullName!); + + // Assert + Assert.True(result, "BlacklistTypes should return true when at least one type is added"); + Assert.Equal(2, reflector.Converters.GetAllBlacklistedTypes().Count); + _output.WriteLine("Empty type names in batch are correctly ignored"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_PartialAssemblyName_MatchesMultipleAssemblies() + { + // Arrange + var reflector = new Reflector(); + // Use "ReflectorNet" which should match both "ReflectorNet.Tests" and potentially other assemblies + var assemblyPrefix = "ReflectorNet"; + var typeName = typeof(BlacklistedBaseClass).FullName!; + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly(assemblyPrefix, typeName); + + // Assert + Assert.True(result, "BlacklistTypes should return true when type is found in matching assembly"); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + _output.WriteLine($"Type found using partial assembly prefix '{assemblyPrefix}'"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_CacheInvalidation_WorksCorrectly() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Prime cache with false result + Assert.False(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + + // Act + var result = reflector.Converters.BlacklistTypesInAssembly( + assemblyName, + typeof(BlacklistedBaseClass).FullName!); + + // Assert - Cache should be invalidated + Assert.True(result, "BlacklistTypes should return true when type is added"); + Assert.True(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); + _output.WriteLine("Cache is correctly invalidated after assembly-prefixed batch blacklist"); + } + + [Fact] + public void BlacklistTypes_WithAssemblyPrefix_AllInvalid_ReturnsFalse() + { + // Arrange + var reflector = new Reflector(); + var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; + + // Act - All invalid type names + var result = reflector.Converters.BlacklistTypesInAssembly( + assemblyName, + "This.Does.Not.Exist", + "Neither.Does.This"); + + // Assert + Assert.False(result, "BlacklistTypes should return false when no types could be resolved"); + Assert.Empty(reflector.Converters.GetAllBlacklistedTypes()); + _output.WriteLine("BlacklistTypes returns false when all type names are invalid"); + } + + #endregion } } diff --git a/ReflectorNet/src/Reflector/Reflector.Registry.cs b/ReflectorNet/src/Reflector/Reflector.Registry.cs index 5e8ef54b..cbc7554c 100644 --- a/ReflectorNet/src/Reflector/Reflector.Registry.cs +++ b/ReflectorNet/src/Reflector/Reflector.Registry.cs @@ -156,6 +156,49 @@ public bool BlacklistType(string typeFullName) return false; } + /// + /// Adds a type to the blacklist by its full name, searching only in assemblies whose name starts with the specified prefix. + /// This allows for more targeted type resolution when the same type name might exist in multiple assemblies. + /// + /// The prefix that assembly names must start with (e.g., "MyCompany.MyProduct"). + /// The full name of the type to blacklist (e.g., "MyCompany.MyProduct.SomeClass"). + /// True if the type was resolved and added; false if the type could not be resolved or was already blacklisted. + public bool BlacklistType(string assemblyNamePrefix, string typeFullName) + { + if (string.IsNullOrEmpty(assemblyNamePrefix) || string.IsNullOrEmpty(typeFullName)) + return false; + + var type = FindTypeInAssemblies(assemblyNamePrefix, typeFullName); + if (type != null) + return BlacklistType(type); + return false; + } + + /// + /// Finds a type by its full name in assemblies whose name starts with the specified prefix. + /// + /// The prefix that assembly names must start with. + /// The full name of the type to find. + /// The found type, or null if not found. + private static Type? FindTypeInAssemblies(string assemblyNamePrefix, string typeFullName) + { + foreach (var assembly in AssemblyUtils.AllAssemblies) + { + var assemblyName = assembly.GetName().Name; + if (assemblyName == null || !assemblyName.StartsWith(assemblyNamePrefix, StringComparison.Ordinal)) + continue; + + var types = AssemblyUtils.GetAssemblyTypes(assembly); + for (int i = 0; i < types.Length; i++) + { + var type = types[i]; + if (type.FullName == typeFullName) + return type; + } + } + return null; + } + /// /// Adds multiple types to the blacklist by their full names, preventing them from being processed by any converter. /// Types are resolved using . The blacklist cache is only invalidated @@ -177,6 +220,34 @@ public bool BlacklistTypes(params string[] typeFullNames) return changed; } + /// + /// Adds multiple types to the blacklist by their full names, searching only in assemblies whose name starts with the specified prefix. + /// This allows for more targeted type resolution when the same type name might exist in multiple assemblies. + /// The blacklist cache is only invalidated if at least one new type was successfully resolved and added. + /// + /// The prefix that assembly names must start with (e.g., "MyCompany.MyProduct"). + /// The full names of the types to blacklist. + /// True if at least one type was resolved and added; false if all types could not be resolved or were already blacklisted. + public bool BlacklistTypesInAssembly(string assemblyNamePrefix, params string[] typeFullNames) + { + if (string.IsNullOrEmpty(assemblyNamePrefix)) + return false; + + var changed = false; + foreach (var typeFullName in typeFullNames) + { + if (string.IsNullOrEmpty(typeFullName)) + continue; + + var type = FindTypeInAssemblies(assemblyNamePrefix, typeFullName); + if (type != null && _blacklistedTypes.TryAdd(type, 0)) + changed = true; + } + if (changed) + _blacklistCache = new ConcurrentDictionary(); // Invalidate cache when blacklist changes + return changed; + } + /// /// Checks if a type is blacklisted. This includes: /// - The type itself being blacklisted From c5679fa847f535e0dee752f4c12a8f93ca1d3140 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 29 Jan 2026 22:11:30 -0800 Subject: [PATCH 02/17] feat: implement assembly-prefixed type resolution methods and tests --- .../src/Utils/AssemblyUtilsTests.cs | 197 ++++++++ .../src/Reflector/Reflector.Registry.cs | 37 +- ReflectorNet/src/Utils/AssemblyUtils.cs | 37 ++ ReflectorNet/src/Utils/TypeUtils.cs | 431 ++++++++++++++++++ 4 files changed, 696 insertions(+), 6 deletions(-) diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs index e73240b2..aac0fcaa 100644 --- a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -498,5 +498,202 @@ public void GetAssemblyTypes_SystemAssembly_ContainsExceptionTypes() #endregion + #region GetAssembliesStartingWith - Valid Prefix Tests + + [Fact] + public void GetAssembliesStartingWith_SystemPrefix_ReturnsSystemAssemblies() + { + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith("System").ToList(); + + // Assert + Assert.NotEmpty(assemblies); + Assert.All(assemblies, a => Assert.StartsWith("System", a.GetName().Name)); + } + + [Fact] + public void GetAssembliesStartingWith_ReflectorNetPrefix_ReturnsReflectorNetAssemblies() + { + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith("ReflectorNet").ToList(); + + // Assert + Assert.NotEmpty(assemblies); + Assert.All(assemblies, a => Assert.StartsWith("ReflectorNet", a.GetName().Name)); + } + + [Fact] + public void GetAssembliesStartingWith_ExactAssemblyName_ReturnsSingleAssembly() + { + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith("ReflectorNet.Tests").ToList(); + + // Assert + Assert.NotEmpty(assemblies); + Assert.Contains(assemblies, a => a.GetName().Name == "ReflectorNet.Tests"); + } + + #endregion + + #region GetAssembliesStartingWith - Empty/Null Prefix Tests + + [Fact] + public void GetAssembliesStartingWith_NullPrefix_ReturnsEmpty() + { + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith(null!).ToList(); + + // Assert + Assert.Empty(assemblies); + } + + [Fact] + public void GetAssembliesStartingWith_EmptyPrefix_ReturnsEmpty() + { + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith(string.Empty).ToList(); + + // Assert + Assert.Empty(assemblies); + } + + #endregion + + #region GetAssembliesStartingWith - No Match Tests + + [Fact] + public void GetAssembliesStartingWith_NonExistentPrefix_ReturnsEmpty() + { + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith("NonExistentAssemblyPrefix12345").ToList(); + + // Assert + Assert.Empty(assemblies); + } + + [Fact] + public void GetAssembliesStartingWith_PartialMatchInMiddle_ReturnsEmpty() + { + // "ystem" is part of "System" but not at the start + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith("ystem").ToList(); + + // Assert + Assert.Empty(assemblies); + } + + #endregion + + #region GetAssembliesStartingWith - Case Sensitivity Tests + + [Fact] + public void GetAssembliesStartingWith_DefaultComparison_IsCaseSensitive() + { + // Act + var upperCaseAssemblies = AssemblyUtils.GetAssembliesStartingWith("SYSTEM").ToList(); + var properCaseAssemblies = AssemblyUtils.GetAssembliesStartingWith("System").ToList(); + + // Assert - Default is Ordinal (case-sensitive), so "SYSTEM" should not match "System.*" + Assert.Empty(upperCaseAssemblies); + Assert.NotEmpty(properCaseAssemblies); + } + + [Fact] + public void GetAssembliesStartingWith_OrdinalIgnoreCase_MatchesRegardlessOfCase() + { + // Act + var upperCaseAssemblies = AssemblyUtils.GetAssembliesStartingWith("SYSTEM", StringComparison.OrdinalIgnoreCase).ToList(); + var lowerCaseAssemblies = AssemblyUtils.GetAssembliesStartingWith("system", StringComparison.OrdinalIgnoreCase).ToList(); + var mixedCaseAssemblies = AssemblyUtils.GetAssembliesStartingWith("SyStEm", StringComparison.OrdinalIgnoreCase).ToList(); + + // Assert + Assert.NotEmpty(upperCaseAssemblies); + Assert.NotEmpty(lowerCaseAssemblies); + Assert.NotEmpty(mixedCaseAssemblies); + Assert.Equal(upperCaseAssemblies.Count, lowerCaseAssemblies.Count); + Assert.Equal(upperCaseAssemblies.Count, mixedCaseAssemblies.Count); + } + + [Fact] + public void GetAssembliesStartingWith_Ordinal_IsCaseSensitive() + { + // Act + var upperCaseAssemblies = AssemblyUtils.GetAssembliesStartingWith("SYSTEM", StringComparison.Ordinal).ToList(); + var properCaseAssemblies = AssemblyUtils.GetAssembliesStartingWith("System", StringComparison.Ordinal).ToList(); + + // Assert + Assert.Empty(upperCaseAssemblies); + Assert.NotEmpty(properCaseAssemblies); + } + + #endregion + + #region GetAssembliesStartingWith - Enumerable Behavior Tests + + [Fact] + public void GetAssembliesStartingWith_ReturnsLazyEnumerable_CanBeIteratedMultipleTimes() + { + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith("System"); + + var firstIteration = assemblies.ToList(); + var secondIteration = assemblies.ToList(); + + // Assert + Assert.Equal(firstIteration.Count, secondIteration.Count); + Assert.All(firstIteration, a => Assert.Contains(a, secondIteration)); + } + + [Fact] + public void GetAssembliesStartingWith_ReturnsDistinctAssemblies() + { + // Act + var assemblies = AssemblyUtils.GetAssembliesStartingWith("System").ToList(); + var distinctAssemblies = assemblies.Distinct().ToList(); + + // Assert + Assert.Equal(assemblies.Count, distinctAssemblies.Count); + } + + [Fact] + public void GetAssembliesStartingWith_ConcurrentEnumeration_Succeeds() + { + // Arrange + var counts = new int[5]; + + // Act - enumerate from multiple threads simultaneously + Parallel.For(0, 5, i => + { + counts[i] = AssemblyUtils.GetAssembliesStartingWith("System").Count(); + }); + + // Assert - all counts should be positive and equal + Assert.All(counts, c => Assert.True(c > 0)); + Assert.All(counts, c => Assert.Equal(counts[0], c)); + } + + #endregion + + #region GetAssembliesStartingWith - Different StringComparison Options + + [Theory] + [InlineData(StringComparison.Ordinal)] + [InlineData(StringComparison.OrdinalIgnoreCase)] + [InlineData(StringComparison.CurrentCulture)] + [InlineData(StringComparison.CurrentCultureIgnoreCase)] + [InlineData(StringComparison.InvariantCulture)] + [InlineData(StringComparison.InvariantCultureIgnoreCase)] + public void GetAssembliesStartingWith_AllComparisonTypes_DoNotThrow(StringComparison comparison) + { + // Act + var exception = Record.Exception(() => + AssemblyUtils.GetAssembliesStartingWith("System", comparison).ToList()); + + // Assert + Assert.Null(exception); + } + + #endregion + } } diff --git a/ReflectorNet/src/Reflector/Reflector.Registry.cs b/ReflectorNet/src/Reflector/Reflector.Registry.cs index cbc7554c..f00a1fb5 100644 --- a/ReflectorNet/src/Reflector/Reflector.Registry.cs +++ b/ReflectorNet/src/Reflector/Reflector.Registry.cs @@ -182,12 +182,8 @@ public bool BlacklistType(string assemblyNamePrefix, string typeFullName) /// The found type, or null if not found. private static Type? FindTypeInAssemblies(string assemblyNamePrefix, string typeFullName) { - foreach (var assembly in AssemblyUtils.AllAssemblies) + foreach (var assembly in AssemblyUtils.GetAssembliesStartingWith(assemblyNamePrefix)) { - var assemblyName = assembly.GetName().Name; - if (assemblyName == null || !assemblyName.StartsWith(assemblyNamePrefix, StringComparison.Ordinal)) - continue; - var types = AssemblyUtils.GetAssemblyTypes(assembly); for (int i = 0; i < types.Length; i++) { @@ -199,6 +195,27 @@ public bool BlacklistType(string assemblyNamePrefix, string typeFullName) return null; } + /// + /// Finds a type by its full name in a pre-collected list of type arrays from assemblies. + /// + /// The pre-collected list of type arrays from matching assemblies. + /// The full name of the type to find. + /// The found type, or null if not found. + private static Type? FindTypeInCachedAssemblies(List typesFromAssemblies, string typeFullName) + { + for (int i = 0; i < typesFromAssemblies.Count; i++) + { + var types = typesFromAssemblies[i]; + for (int j = 0; j < types.Length; j++) + { + var type = types[j]; + if (type.FullName == typeFullName) + return type; + } + } + return null; + } + /// /// Adds multiple types to the blacklist by their full names, preventing them from being processed by any converter. /// Types are resolved using . The blacklist cache is only invalidated @@ -233,13 +250,21 @@ public bool BlacklistTypesInAssembly(string assemblyNamePrefix, params string[] if (string.IsNullOrEmpty(assemblyNamePrefix)) return false; + // Collect matching assemblies and their types once + var matchingAssemblies = new List(); + foreach (var assembly in AssemblyUtils.GetAssembliesStartingWith(assemblyNamePrefix)) + matchingAssemblies.Add(AssemblyUtils.GetAssemblyTypes(assembly)); + + if (matchingAssemblies.Count == 0) + return false; + var changed = false; foreach (var typeFullName in typeFullNames) { if (string.IsNullOrEmpty(typeFullName)) continue; - var type = FindTypeInAssemblies(assemblyNamePrefix, typeFullName); + var type = FindTypeInCachedAssemblies(matchingAssemblies, typeFullName); if (type != null && _blacklistedTypes.TryAdd(type, 0)) changed = true; } diff --git a/ReflectorNet/src/Utils/AssemblyUtils.cs b/ReflectorNet/src/Utils/AssemblyUtils.cs index 18c3b643..ac2215ed 100644 --- a/ReflectorNet/src/Utils/AssemblyUtils.cs +++ b/ReflectorNet/src/Utils/AssemblyUtils.cs @@ -38,6 +38,25 @@ public static IEnumerable AllAssemblies } } + /// + /// Gets all assemblies whose names start with the specified prefix. + /// + /// The prefix to match against assembly names. + /// The string comparison type to use. Defaults to . + /// An enumerable of assemblies whose names start with the specified prefix. + public static IEnumerable GetAssembliesStartingWith(string prefix, StringComparison comparison = StringComparison.Ordinal) + { + if (string.IsNullOrEmpty(prefix)) + yield break; + + foreach (var assembly in AllAssemblies) + { + var name = assembly.GetName().Name; + if (name != null && name.StartsWith(prefix, comparison)) + yield return assembly; + } + } + /// /// Gets all types from all loaded assemblies with exception protection. /// @@ -56,6 +75,24 @@ public static IEnumerable AllTypes } } + /// + /// Gets all types from assemblies whose names start with the specified prefix. + /// + /// The prefix to match against assembly names. + /// The string comparison type to use. Defaults to . + /// An enumerable of types from assemblies whose names start with the specified prefix. + public static IEnumerable GetTypesStartingWith(string prefix, StringComparison comparison = StringComparison.Ordinal) + { + foreach (var assembly in GetAssembliesStartingWith(prefix, comparison)) + { + var types = GetAssemblyTypes(assembly); + for (int i = 0; i < types.Length; i++) + { + yield return types[i]; + } + } + } + /// /// Gets all types from an assembly. /// diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index ccb99d29..0ec1fc7a 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -39,6 +39,10 @@ public static partial class TypeUtils // LRU cache for resolved type names to avoid repeated AllTypes enumeration (thread-safe) private static readonly LruCache _typeCache = new(TypeCacheCapacity); + // LRU cache for assembly-prefixed type lookups (thread-safe) + // Key format: "assemblyPrefix|typeName" + private static readonly LruCache _assemblyTypeCache = new(TypeCacheCapacity); + // LRU cache for enumerable item types to avoid repeated interface/inheritance walks (thread-safe) private static readonly LruCache _enumerableItemTypeCache = new(EnumerableItemTypeCacheCapacity); @@ -62,6 +66,16 @@ public static void ClearEnumerableItemTypeCache(ILogger? logger = null) _enumerableItemTypeCache.Clear(); } + /// + /// Clears the assembly-prefixed type resolution cache. + /// + public static void ClearAssemblyTypeCache(ILogger? logger = null) + { + logger?.LogDebug("Clearing assembly-prefixed type resolution cache with {count} entries (capacity: {capacity}).", + _assemblyTypeCache.Count, _assemblyTypeCache.Capacity); + _assemblyTypeCache.Clear(); + } + /// /// Resolves a from its string representation. /// @@ -141,6 +155,104 @@ public static void ClearEnumerableItemTypeCache(ILogger? logger = null) return type; } + /// + /// Resolves a from its string representation, searching only in assemblies + /// whose names start with the specified prefix. + /// + /// + /// This method attempts to resolve types using multiple strategies in order: + /// + /// Built-in with assembly verification + /// 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 assemblies matching the prefix by FullName, AssemblyQualifiedName, or TypeId + /// + /// Results are cached for performance. Use to clear the cache. + /// + /// The assembly name prefix to filter assemblies. If null or empty, delegates to . + /// 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? assemblyName, string? typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + return null; + + // If no assembly filter, delegate to the standard method + if (string.IsNullOrEmpty(assemblyName)) + return GetType(typeName); + + // Check cache first (key format: "assemblyPrefix|typeName") + var cacheKey = $"{assemblyName}|{typeName}"; + if (_assemblyTypeCache.TryGetValue(cacheKey, out var cachedType)) + return cachedType; + + // First try built-in Type.GetType() which handles many formats + Type? type = null; + try + { + type = Type.GetType(typeName, throwOnError: false); + // Verify the resolved type is in a matching assembly + if (type != null && !IsTypeInMatchingAssembly(type, assemblyName)) + type = null; + } + catch + { + // Ignore exceptions (e.g. invalid assembly name) and try other resolution methods + } + + if (type != null) + { + _assemblyTypeCache[cacheKey] = type; + return type; + } + + // Try resolving array types (e.g., "Namespace.Type[]") + type = TryResolveArrayType(assemblyName, typeName); + if (type != null) + { + _assemblyTypeCache[cacheKey] = type; + return type; + } + + // Try resolving C#-style generic types (e.g., "Namespace.Generic" or "Namespace.Generic") + type = TryResolveCSharpGenericType(assemblyName, typeName); + if (type != null) + { + _assemblyTypeCache[cacheKey] = type; + return type; + } + + // Try resolving generic types (e.g., "Namespace.Generic`1[[TypeArg]]") + type = TryResolveClassicGenericType(assemblyName, typeName); + if (type != null) + { + _assemblyTypeCache[cacheKey] = type; + return type; + } + + // Search in types from matching assemblies only + type = AssemblyUtils.GetTypesStartingWith(assemblyName).FirstOrDefault(t => + typeName == t.FullName || + typeName == t.AssemblyQualifiedName || + typeName == t.GetTypeId()); + + // Caching the result (even if null) + _assemblyTypeCache[cacheKey] = type; + + return type; + } + + /// + /// Checks if a type belongs to an assembly whose name starts with the specified prefix. + /// + private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) + { + var assemblyName = type.Assembly.GetName().Name; + return assemblyName != null && assemblyName.StartsWith(assemblyPrefix, StringComparison.Ordinal); + } + /// /// Attempts to resolve a simple (non-generic, non-array) type by name. /// @@ -156,6 +268,22 @@ public static void ClearEnumerableItemTypeCache(ILogger? logger = null) name == t.Name); } + /// + /// Attempts to resolve a simple (non-generic, non-array) type by name, + /// searching only in assemblies whose names start with the specified prefix. + /// + private static Type? ResolveSimpleType(string assemblyPrefix, string name) + { + var type = Type.GetType(name, throwOnError: false); + if (type != null && IsTypeInMatchingAssembly(type, assemblyPrefix)) + return type; + + return AssemblyUtils.GetTypesStartingWith(assemblyPrefix).FirstOrDefault(t => + name == t.AssemblyQualifiedName || + name == t.FullName || + name == t.Name); + } + /// /// Attempts to resolve array type names (e.g., "Namespace.Type[]"). /// @@ -185,6 +313,36 @@ public static void ClearEnumerableItemTypeCache(ILogger? logger = null) : elementType.MakeArrayType(commas + 1); } + /// + /// Attempts to resolve array type names (e.g., "Namespace.Type[]"), + /// searching only in assemblies whose names start with the specified prefix. + /// + private static Type? TryResolveArrayType(string assemblyPrefix, string typeName) + { + if (!typeName.EndsWith("]")) + return null; + + var lastOpenBracket = typeName.LastIndexOf('['); + if (lastOpenBracket < 0) + return null; + + var suffix = typeName.Substring(lastOpenBracket); + // Check if content contains only commas + var content = suffix.Substring(1, suffix.Length - 2); + if (content.Length > 0 && content.Any(c => c != ',')) + return null; + + var commas = content.Length; + var elementTypeName = typeName.Substring(0, lastOpenBracket); + var elementType = GetType(assemblyPrefix, elementTypeName); + + if (elementType == null) return null; + + return commas == 0 + ? elementType.MakeArrayType() + : elementType.MakeArrayType(commas + 1); + } + /// /// Attempts to resolve C#-style generic types. /// Handles formats like: "Namespace.Generic<TypeArg>" or "Namespace.Generic<TypeArg1, TypeArg2>" @@ -350,6 +508,172 @@ public static void ClearEnumerableItemTypeCache(ILogger? logger = null) return currentType; } + /// + /// Attempts to resolve C#-style generic types, + /// searching only in assemblies whose names start with the specified prefix. + /// Handles formats like: "Namespace.Generic<TypeArg>" or "Namespace.Generic<TypeArg1, TypeArg2>" + /// Also handles nested types: "Namespace.Generic<TypeArg>+Nested" or "Namespace.Generic<TypeArg>+Nested<TypeArg2>" + /// Space after comma is optional. + /// + private static Type? TryResolveCSharpGenericType(string assemblyPrefix, string typeName) + { + // Find the opening angle bracket + var openBracketIndex = typeName.IndexOf('<'); + if (openBracketIndex < 0) + return null; + + // Find the matching closing angle bracket + var closeBracketIndex = FindMatchingCloseBracket(typeName, openBracketIndex); + if (closeBracketIndex < 0) + return null; + + // Extract the base type name (everything before '<') + var baseTypeName = typeName.Substring(0, openBracketIndex); + if (string.IsNullOrWhiteSpace(baseTypeName)) + return null; + + // Extract the type arguments string (between '<' and '>') + var typeArgsString = typeName.Substring(openBracketIndex + 1, closeBracketIndex - openBracketIndex - 1); + + // Parse the type arguments + var typeArgNames = ParseCSharpGenericArguments(typeArgsString); + if (typeArgNames == null || typeArgNames.Length == 0) + return null; + + // Construct the generic type definition name (e.g., "Namespace.Generic`2") + var genericDefName = $"{baseTypeName}`{typeArgNames.Length}"; + + // Resolve the generic type definition + var genericDef = ResolveSimpleType(assemblyPrefix, genericDefName); + if (genericDef == null || !genericDef.IsGenericTypeDefinition) + return null; + + // Resolve each type argument + var typeArgs = new Type[typeArgNames.Length]; + for (int i = 0; i < typeArgNames.Length; i++) + { + var argType = GetType(assemblyPrefix, typeArgNames[i].Trim()); + if (argType == null) + return null; + typeArgs[i] = argType; + } + + Type? currentType; + try + { + currentType = genericDef.MakeGenericType(typeArgs); + } + catch + { + return null; + } + + // Handle nested types appended after the generic arguments + var remaining = typeName.Substring(closeBracketIndex + 1); + while (!string.IsNullOrEmpty(remaining)) + { + if (!remaining.StartsWith("+") && !remaining.StartsWith(".")) + return null; + + remaining = remaining.Substring(1); // Remove separator + + // Check for generic args + var open = remaining.IndexOf('<'); + string nestedName; + Type[]? nestedArgs = null; + int nextRemainingIndex; + + if (open > 0) + { + var close = FindMatchingCloseBracket(remaining, open); + if (close < 0) return null; + + nestedName = remaining.Substring(0, open); + var argsStr = remaining.Substring(open + 1, close - open - 1); + var argNames = ParseCSharpGenericArguments(argsStr); + if (argNames == null) return null; + + nestedArgs = new Type[argNames.Length]; + for (int i = 0; i < argNames.Length; i++) + { + var tempType = GetType(assemblyPrefix, argNames[i]?.Trim()); + if (tempType == null) return null; + nestedArgs[i] = tempType; + } + + nextRemainingIndex = close + 1; + } + else + { + // No generic args, but check if there are more separators + var nextSep = remaining.IndexOfAny(new[] { '+', '.' }); + if (nextSep > 0) + { + nestedName = remaining.Substring(0, nextSep); + nextRemainingIndex = nextSep; + } + else + { + nestedName = remaining; + nextRemainingIndex = remaining.Length; + } + } + + // Find nested type + Type? nestedType; + Type[] allArgs; + + if (nestedArgs != null) + { + nestedType = currentType.GetNestedType($"{nestedName}`{nestedArgs.Length}"); + if (nestedType == null) return null; + + if (currentType.IsGenericType && !currentType.IsGenericTypeDefinition) + { + var parentArgs = currentType.GetGenericArguments(); + allArgs = new Type[parentArgs.Length + nestedArgs.Length]; + Array.Copy(parentArgs, allArgs, parentArgs.Length); + Array.Copy(nestedArgs, 0, allArgs, parentArgs.Length, nestedArgs.Length); + } + else + { + allArgs = nestedArgs; + } + } + else + { + nestedType = currentType.GetNestedType(nestedName); + if (nestedType == null) return null; + + allArgs = currentType.IsGenericType && !currentType.IsGenericTypeDefinition + ? currentType.GetGenericArguments() + : Type.EmptyTypes; + } + + if (nestedType.IsGenericTypeDefinition) + { + try + { + currentType = nestedType.GetGenericArguments().Length == allArgs.Length + ? nestedType.MakeGenericType(allArgs) + : nestedType; + } + catch + { + return null; + } + } + else + { + currentType = nestedType; + } + + remaining = remaining.Substring(nextRemainingIndex); + } + + return currentType; + } + /// /// Finds the matching closing angle bracket for an opening bracket. /// Handles nested generic types properly. @@ -459,6 +783,46 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) } } + /// + /// Attempts to resolve constructed generic types by parsing and reconstructing them, + /// searching only in assemblies whose names start with the specified prefix. + /// Handles formats like: "Namespace.Generic`1[[TypeArg, Assembly]]" + /// + private static Type? TryResolveClassicGenericType(string assemblyPrefix, string typeName) + { + // Find generic arity marker (backtick) + var backtickIndex = typeName.IndexOf('`'); + if (backtickIndex < 0) + return null; + + // Find the start of generic arguments [[...]] + var argsStart = typeName.IndexOf("[[", backtickIndex); + if (argsStart < 0) + return null; + + // Extract generic definition name (e.g., "Namespace.WrapperClass`1") + var genericDefName = typeName.Substring(0, argsStart); + + // Resolve the generic type definition + var genericDef = ResolveSimpleType(assemblyPrefix, genericDefName); + if (genericDef == null || !genericDef.IsGenericTypeDefinition) + return null; + + // Parse and resolve type arguments + var typeArgs = ParseGenericArguments(assemblyPrefix, typeName, argsStart); + if (typeArgs == null || typeArgs.Length != genericDef.GetGenericArguments().Length) + return null; + + try + { + return genericDef.MakeGenericType(typeArgs); + } + catch + { + return null; + } + } + /// /// Parses generic arguments from the [[Type1, Assembly], [Type2, Assembly]] format. /// The format uses double brackets where: @@ -525,6 +889,73 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) return args.Count > 0 ? args.ToArray() : null; } + /// + /// Parses generic arguments from the [[Type1, Assembly], [Type2, Assembly]] format, + /// searching only in assemblies whose names start with the specified prefix. + /// The format uses double brackets where: + /// - Outer [] wraps all type arguments + /// - Inner [] wraps each individual type argument + /// - Nested generic types have their own [[]] inside + /// + private static Type[]? ParseGenericArguments(string assemblyPrefix, string typeName, int startIndex) + { + var args = new List(); + var depth = 0; + var currentArg = new System.Text.StringBuilder(); + + for (int i = startIndex; i < typeName.Length; i++) + { + var c = typeName[i]; + + if (c == '[') + { + depth++; + // Only append brackets for nested generics (depth > 2) + // depth 1 = outer wrapper for all args + // depth 2 = wrapper for individual type arg (don't include) + // depth 3+ = nested generic brackets (include) + if (depth > 2) + currentArg.Append(c); + } + else if (c == ']') + { + depth--; + if (depth == 1) + { + // End of one type argument + var argTypeName = currentArg.ToString().Trim(); + if (!string.IsNullOrEmpty(argTypeName)) + { + var argType = GetType(assemblyPrefix, argTypeName); + if (argType == null) + return null; + args.Add(argType); + } + currentArg.Clear(); + } + else if (depth > 1) + { + // Append closing brackets for nested generics + currentArg.Append(c); + } + else if (depth == 0) + { + break; // End of all arguments + } + } + else if (c == ',' && depth == 1) + { + // Separator between type arguments at the top level - skip it + } + else if (depth > 1) + { + currentArg.Append(c); + } + } + + return args.Count > 0 ? args.ToArray() : null; + } + /// /// Determines whether the specified type is a dictionary type. /// From d4772667651230734c8dd281bae4046ab560b1bd Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 29 Jan 2026 22:16:03 -0800 Subject: [PATCH 03/17] feat: optimize nested type name separation by using a dedicated constant --- ReflectorNet/src/Utils/TypeUtils.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index 0ec1fc7a..ec8e21b6 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -36,6 +36,9 @@ public static partial class TypeUtils /// public static IEnumerable AllTypes => AssemblyUtils.AllTypes; + // Characters used to separate nested type names (e.g. `Outer+Inner` or `Outer.Inner`). + private static readonly char[] NestedTypeSeparators = new[] { '+', '.' }; + // LRU cache for resolved type names to avoid repeated AllTypes enumeration (thread-safe) private static readonly LruCache _typeCache = new(TypeCacheCapacity); @@ -440,7 +443,7 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) else { // No generic args, but check if there are more separators - var nextSep = remaining.IndexOfAny(new[] { '+', '.' }); + var nextSep = remaining.IndexOfAny(NestedTypeSeparators); if (nextSep > 0) { nestedName = remaining.Substring(0, nextSep); @@ -606,7 +609,7 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) else { // No generic args, but check if there are more separators - var nextSep = remaining.IndexOfAny(new[] { '+', '.' }); + var nextSep = remaining.IndexOfAny(NestedTypeSeparators); if (nextSep > 0) { nestedName = remaining.Substring(0, nextSep); From 104ae3debc93b69b9f14dab90f3c4a8b59349c20 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 29 Jan 2026 23:00:44 -0800 Subject: [PATCH 04/17] Refactor code structure for improved readability and maintainability --- ReflectorNet/src/Utils/TypeUtils.Cache.cs | 67 + .../src/Utils/TypeUtils.Collections.cs | 86 + .../src/Utils/TypeUtils.Description.cs | 136 ++ .../src/Utils/TypeUtils.GetType.Private.cs | 600 +++++++ ReflectorNet/src/Utils/TypeUtils.GetType.cs | 133 ++ ReflectorNet/src/Utils/TypeUtils.Helpers.cs | 161 ++ ReflectorNet/src/Utils/TypeUtils.cs | 1469 +---------------- 7 files changed, 1188 insertions(+), 1464 deletions(-) create mode 100644 ReflectorNet/src/Utils/TypeUtils.Cache.cs create mode 100644 ReflectorNet/src/Utils/TypeUtils.Collections.cs create mode 100644 ReflectorNet/src/Utils/TypeUtils.Description.cs create mode 100644 ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs create mode 100644 ReflectorNet/src/Utils/TypeUtils.GetType.cs create mode 100644 ReflectorNet/src/Utils/TypeUtils.Helpers.cs diff --git a/ReflectorNet/src/Utils/TypeUtils.Cache.cs b/ReflectorNet/src/Utils/TypeUtils.Cache.cs new file mode 100644 index 00000000..42b7e23a --- /dev/null +++ b/ReflectorNet/src/Utils/TypeUtils.Cache.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace com.IvanMurzak.ReflectorNet.Utils +{ + public static partial class TypeUtils + { + /// + /// Maximum capacity for the type name resolution cache. + /// + public const int TypeCacheCapacity = 1000; + + /// + /// Maximum capacity for the enumerable item type cache. + /// + public const int EnumerableItemTypeCacheCapacity = 500; + + /// + /// Gets all types from all loaded assemblies. + /// + public static IEnumerable AllTypes => AssemblyUtils.AllTypes; + + // Characters used to separate nested type names (e.g. `Outer+Inner` or `Outer.Inner`). + private static readonly char[] NestedTypeSeparators = new[] { '+', '.' }; + + // LRU cache for resolved type names to avoid repeated AllTypes enumeration (thread-safe) + private static readonly LruCache _typeCache = new(TypeCacheCapacity); + + // LRU cache for assembly-prefixed type lookups (thread-safe) + // Key format: "assemblyPrefix|typeName" + private static readonly LruCache _assemblyTypeCache = new(TypeCacheCapacity); + + // LRU cache for enumerable item types to avoid repeated interface/inheritance walks (thread-safe) + private static readonly LruCache _enumerableItemTypeCache = new(EnumerableItemTypeCacheCapacity); + + /// + /// Clears the type name resolution cache. + /// + public static void ClearTypeCache(ILogger? logger = null) + { + logger?.LogDebug("Clearing type resolution cache with {count} entries (capacity: {capacity}).", + _typeCache.Count, _typeCache.Capacity); + _typeCache.Clear(); + } + + /// + /// Clears the enumerable item type cache. + /// + public static void ClearEnumerableItemTypeCache(ILogger? logger = null) + { + logger?.LogDebug("Clearing enumerable item type cache with {count} entries (capacity: {capacity}).", + _enumerableItemTypeCache.Count, _enumerableItemTypeCache.Capacity); + _enumerableItemTypeCache.Clear(); + } + + /// + /// Clears the assembly-prefixed type resolution cache. + /// + public static void ClearAssemblyTypeCache(ILogger? logger = null) + { + logger?.LogDebug("Clearing assembly-prefixed type resolution cache with {count} entries (capacity: {capacity}).", + _assemblyTypeCache.Count, _assemblyTypeCache.Capacity); + _assemblyTypeCache.Clear(); + } + } +} diff --git a/ReflectorNet/src/Utils/TypeUtils.Collections.cs b/ReflectorNet/src/Utils/TypeUtils.Collections.cs new file mode 100644 index 00000000..7aa59276 --- /dev/null +++ b/ReflectorNet/src/Utils/TypeUtils.Collections.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace com.IvanMurzak.ReflectorNet.Utils +{ + public static partial class TypeUtils + { + public static bool IsDictionary(Type type) + { + if (type.IsGenericType && + (type.GetGenericTypeDefinition() == typeof(Dictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>))) + { + return true; + } + + return type.GetInterfaces() + .Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); + } + + public static Type[]? GetDictionaryGenericArguments(Type type) + { + if (type.IsGenericType && + (type.GetGenericTypeDefinition() == typeof(Dictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>))) + { + return type.GetGenericArguments(); + } + + var dictionaryInterface = type.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); + + return dictionaryInterface?.GetGenericArguments(); + } + + public static bool IsIEnumerable(Type type) + { + if (type.IsArray) + return true; + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + return true; + + return type.GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + } + + public static Type? GetEnumerableItemType(Type type) + { + return _enumerableItemTypeCache.GetOrAdd(type, GetEnumerableItemTypeInternal); + } + + private static Type? GetEnumerableItemTypeInternal(Type type) + { + if (type.IsArray) + return type.GetElementType(); + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + return type.GetGenericArguments().FirstOrDefault(); + + var enumerableInterface = type.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + + if (enumerableInterface != null) + return enumerableInterface.GetGenericArguments().FirstOrDefault(); + + var baseType = type.BaseType; + while (baseType != null && baseType != typeof(object)) + { + if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + return baseType.GetGenericArguments().FirstOrDefault(); + + enumerableInterface = baseType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + + if (enumerableInterface != null) + return enumerableInterface.GetGenericArguments().FirstOrDefault(); + + baseType = baseType.BaseType; + } + + return null; + } + } +} diff --git a/ReflectorNet/src/Utils/TypeUtils.Description.cs b/ReflectorNet/src/Utils/TypeUtils.Description.cs new file mode 100644 index 00000000..55ba8765 --- /dev/null +++ b/ReflectorNet/src/Utils/TypeUtils.Description.cs @@ -0,0 +1,136 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER +using System.Text.Json.Schema; +#endif + +namespace com.IvanMurzak.ReflectorNet.Utils +{ + public static partial class TypeUtils + { + public static string? GetDescription(Type type) + { + return type + .GetCustomAttribute(true) + ?.Description + ?? (type.BaseType != null + ? GetDescription(type.BaseType!) + : null); + } + + public static string? GetDescription(ParameterInfo? parameterInfo) + { + return parameterInfo + ?.GetCustomAttribute(true) + ?.Description + ?? (parameterInfo != null + ? GetDescription(parameterInfo.ParameterType) + : null); + } + + public static string? GetDescription(MemberInfo? memberInfo) + { + if (memberInfo == null) + return null; + + var description = memberInfo + .GetCustomAttribute(true) + ?.Description; + + if (description != null) + return description; + + return memberInfo.MemberType switch + { + MemberTypes.Field => GetFieldDescription((FieldInfo)memberInfo), + MemberTypes.Property => GetPropertyDescription((PropertyInfo)memberInfo), + _ => null + }; + } + + public static string? GetFieldDescription(FieldInfo? fieldInfo) + { + if (fieldInfo == null) + return null; + + return fieldInfo + .GetCustomAttribute(true) + ?.Description + ?? (fieldInfo.FieldType != null + ? GetDescription(fieldInfo.FieldType) + : null); + } + + public static string? GetPropertyDescription(PropertyInfo? propertyInfo) + { + if (propertyInfo == null) + return null; + + return propertyInfo + .GetCustomAttribute(true) + ?.Description + ?? (propertyInfo.PropertyType != null + ? GetDescription(propertyInfo.PropertyType) + : null); + } + + 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 + public static string? GetPropertyDescription(JsonSchemaExporterContext context) + { + if (context.PropertyInfo == null || context.PropertyInfo.DeclaringType == null) + return null; + + var memberInfo = context.PropertyInfo.DeclaringType + .GetMember( + name: context.PropertyInfo.Name, + bindingAttr: BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .FirstOrDefault(); + + if (memberInfo == null) + { + var pascalCaseName = ToPascalCase(context.PropertyInfo.Name); + memberInfo = context.PropertyInfo.DeclaringType + .GetMember( + name: pascalCaseName, + bindingAttr: BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .FirstOrDefault(); + } + + if (memberInfo == null) + { + var allMembers = context.PropertyInfo.DeclaringType.GetMembers(BindingFlags.Public | BindingFlags.Instance); + memberInfo = allMembers.FirstOrDefault(m => + string.Equals(m.Name, context.PropertyInfo.Name, StringComparison.OrdinalIgnoreCase)); + } + + if (memberInfo == null) + return null; + + return GetDescription(memberInfo); + } +#endif + + private static string ToPascalCase(string camelCase) + { + if (string.IsNullOrEmpty(camelCase)) + return camelCase; + + return char.ToUpperInvariant(camelCase[0]) + camelCase.Substring(1); + } + + public static string? GetFieldDescription(Type type, string fieldName) + { + var fieldInfo = type.GetField(fieldName); + return fieldInfo != null ? GetFieldDescription(fieldInfo) : null; + } + } +} diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs new file mode 100644 index 00000000..079e129e --- /dev/null +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs @@ -0,0 +1,600 @@ +using System; +using System.Linq; + +namespace com.IvanMurzak.ReflectorNet.Utils +{ + public static partial class TypeUtils + { + private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) + { + var assemblyName = type.Assembly.GetName().Name; + return assemblyName != null && assemblyName.StartsWith(assemblyPrefix, StringComparison.Ordinal); + } + + private static Type? ResolveSimpleType(string name) + { + var type = Type.GetType(name, throwOnError: false); + if (type != null) + return type; + + return AssemblyUtils.AllTypes.FirstOrDefault(t => + name == t.AssemblyQualifiedName || + name == t.FullName || + name == t.Name); + } + + private static Type? ResolveSimpleType(string assemblyPrefix, string name) + { + var type = Type.GetType(name, throwOnError: false); + if (type != null && IsTypeInMatchingAssembly(type, assemblyPrefix)) + return type; + + return AssemblyUtils.GetTypesStartingWith(assemblyPrefix).FirstOrDefault(t => + name == t.AssemblyQualifiedName || + name == t.FullName || + name == t.Name); + } + + private static Type? TryResolveArrayType(string typeName) + { + if (!typeName.EndsWith("]")) + return null; + + var lastOpenBracket = typeName.LastIndexOf('['); + if (lastOpenBracket < 0) + return null; + + var suffix = typeName.Substring(lastOpenBracket); + var content = suffix.Substring(1, suffix.Length - 2); + if (content.Length > 0 && content.Any(c => c != ',')) + return null; + + var commas = content.Length; + var elementTypeName = typeName.Substring(0, lastOpenBracket); + var elementType = GetType(elementTypeName); + + if (elementType == null) return null; + + return commas == 0 + ? elementType.MakeArrayType() + : elementType.MakeArrayType(commas + 1); + } + + private static Type? TryResolveArrayType(string assemblyPrefix, string typeName) + { + if (!typeName.EndsWith("]")) + return null; + + var lastOpenBracket = typeName.LastIndexOf('['); + if (lastOpenBracket < 0) + return null; + + var suffix = typeName.Substring(lastOpenBracket); + var content = suffix.Substring(1, suffix.Length - 2); + if (content.Length > 0 && content.Any(c => c != ',')) + return null; + + var commas = content.Length; + var elementTypeName = typeName.Substring(0, lastOpenBracket); + var elementType = GetType(assemblyPrefix, elementTypeName); + + if (elementType == null) return null; + + return commas == 0 + ? elementType.MakeArrayType() + : elementType.MakeArrayType(commas + 1); + } + + private static Type? TryResolveCSharpGenericType(string typeName) + { + var openBracketIndex = typeName.IndexOf('<'); + if (openBracketIndex < 0) + return null; + + var closeBracketIndex = FindMatchingCloseBracket(typeName, openBracketIndex); + if (closeBracketIndex < 0) + return null; + + var baseTypeName = typeName.Substring(0, openBracketIndex); + if (string.IsNullOrWhiteSpace(baseTypeName)) + return null; + + var typeArgsString = typeName.Substring(openBracketIndex + 1, closeBracketIndex - openBracketIndex - 1); + var typeArgNames = ParseCSharpGenericArguments(typeArgsString); + if (typeArgNames == null || typeArgNames.Length == 0) + return null; + + var genericDefName = $"{baseTypeName}`{typeArgNames.Length}"; + var genericDef = ResolveSimpleType(genericDefName); + if (genericDef == null || !genericDef.IsGenericTypeDefinition) + return null; + + var typeArgs = new Type[typeArgNames.Length]; + for (int i = 0; i < typeArgNames.Length; i++) + { + var argType = GetType(typeArgNames[i].Trim()); + if (argType == null) + return null; + typeArgs[i] = argType; + } + + Type? currentType; + try + { + currentType = genericDef.MakeGenericType(typeArgs); + } + catch + { + return null; + } + + var remaining = typeName.Substring(closeBracketIndex + 1); + while (!string.IsNullOrEmpty(remaining)) + { + if (!remaining.StartsWith("+") && !remaining.StartsWith(".")) + return null; + + remaining = remaining.Substring(1); + + var open = remaining.IndexOf('<'); + string nestedName; + Type[]? nestedArgs = null; + int nextRemainingIndex; + + if (open > 0) + { + var close = FindMatchingCloseBracket(remaining, open); + if (close < 0) return null; + + nestedName = remaining.Substring(0, open); + var argsStr = remaining.Substring(open + 1, close - open - 1); + var argNames = ParseCSharpGenericArguments(argsStr); + if (argNames == null) return null; + + nestedArgs = new Type[argNames.Length]; + for (int i = 0; i < argNames.Length; i++) + { + var tempType = GetType(argNames[i]?.Trim()); + if (tempType == null) return null; + nestedArgs[i] = tempType; + } + + nextRemainingIndex = close + 1; + } + else + { + var nextSep = remaining.IndexOfAny(NestedTypeSeparators); + if (nextSep > 0) + { + nestedName = remaining.Substring(0, nextSep); + nextRemainingIndex = nextSep; + } + else + { + nestedName = remaining; + nextRemainingIndex = remaining.Length; + } + } + + Type? nestedType; + Type[] allArgs; + + if (nestedArgs != null) + { + nestedType = currentType.GetNestedType($"{nestedName}`{nestedArgs.Length}"); + if (nestedType == null) return null; + + if (currentType.IsGenericType && !currentType.IsGenericTypeDefinition) + { + var parentArgs = currentType.GetGenericArguments(); + allArgs = new Type[parentArgs.Length + nestedArgs.Length]; + Array.Copy(parentArgs, allArgs, parentArgs.Length); + Array.Copy(nestedArgs, 0, allArgs, parentArgs.Length, nestedArgs.Length); + } + else + { + allArgs = nestedArgs; + } + } + else + { + nestedType = currentType.GetNestedType(nestedName); + if (nestedType == null) return null; + + allArgs = currentType.IsGenericType && !currentType.IsGenericTypeDefinition + ? currentType.GetGenericArguments() + : Type.EmptyTypes; + } + + if (nestedType.IsGenericTypeDefinition) + { + try + { + currentType = nestedType.GetGenericArguments().Length == allArgs.Length + ? nestedType.MakeGenericType(allArgs) + : nestedType; + } + catch + { + return null; + } + } + else + { + currentType = nestedType; + } + + remaining = remaining.Substring(nextRemainingIndex); + } + + return currentType; + } + + private static Type? TryResolveCSharpGenericType(string assemblyPrefix, string typeName) + { + var openBracketIndex = typeName.IndexOf('<'); + if (openBracketIndex < 0) + return null; + + var closeBracketIndex = FindMatchingCloseBracket(typeName, openBracketIndex); + if (closeBracketIndex < 0) + return null; + + var baseTypeName = typeName.Substring(0, openBracketIndex); + if (string.IsNullOrWhiteSpace(baseTypeName)) + return null; + + var typeArgsString = typeName.Substring(openBracketIndex + 1, closeBracketIndex - openBracketIndex - 1); + var typeArgNames = ParseCSharpGenericArguments(typeArgsString); + if (typeArgNames == null || typeArgNames.Length == 0) + return null; + + var genericDefName = $"{baseTypeName}`{typeArgNames.Length}"; + var genericDef = ResolveSimpleType(assemblyPrefix, genericDefName); + if (genericDef == null || !genericDef.IsGenericTypeDefinition) + return null; + + var typeArgs = new Type[typeArgNames.Length]; + for (int i = 0; i < typeArgNames.Length; i++) + { + var argType = GetType(assemblyPrefix, typeArgNames[i].Trim()); + if (argType == null) + return null; + typeArgs[i] = argType; + } + + Type? currentType; + try + { + currentType = genericDef.MakeGenericType(typeArgs); + } + catch + { + return null; + } + + var remaining = typeName.Substring(closeBracketIndex + 1); + while (!string.IsNullOrEmpty(remaining)) + { + if (!remaining.StartsWith("+") && !remaining.StartsWith(".")) + return null; + + remaining = remaining.Substring(1); + + var open = remaining.IndexOf('<'); + string nestedName; + Type[]? nestedArgs = null; + int nextRemainingIndex; + + if (open > 0) + { + var close = FindMatchingCloseBracket(remaining, open); + if (close < 0) return null; + + nestedName = remaining.Substring(0, open); + var argsStr = remaining.Substring(open + 1, close - open - 1); + var argNames = ParseCSharpGenericArguments(argsStr); + if (argNames == null) return null; + + nestedArgs = new Type[argNames.Length]; + for (int i = 0; i < argNames.Length; i++) + { + var tempType = GetType(assemblyPrefix, argNames[i]?.Trim()); + if (tempType == null) return null; + nestedArgs[i] = tempType; + } + + nextRemainingIndex = close + 1; + } + else + { + var nextSep = remaining.IndexOfAny(NestedTypeSeparators); + if (nextSep > 0) + { + nestedName = remaining.Substring(0, nextSep); + nextRemainingIndex = nextSep; + } + else + { + nestedName = remaining; + nextRemainingIndex = remaining.Length; + } + } + + Type? nestedType; + Type[] allArgs; + + if (nestedArgs != null) + { + nestedType = currentType.GetNestedType($"{nestedName}`{nestedArgs.Length}"); + if (nestedType == null) return null; + + if (currentType.IsGenericType && !currentType.IsGenericTypeDefinition) + { + var parentArgs = currentType.GetGenericArguments(); + allArgs = new Type[parentArgs.Length + nestedArgs.Length]; + Array.Copy(parentArgs, allArgs, parentArgs.Length); + Array.Copy(nestedArgs, 0, allArgs, parentArgs.Length, nestedArgs.Length); + } + else + { + allArgs = nestedArgs; + } + } + else + { + nestedType = currentType.GetNestedType(nestedName); + if (nestedType == null) return null; + + allArgs = currentType.IsGenericType && !currentType.IsGenericTypeDefinition + ? currentType.GetGenericArguments() + : Type.EmptyTypes; + } + + if (nestedType.IsGenericTypeDefinition) + { + try + { + currentType = nestedType.GetGenericArguments().Length == allArgs.Length + ? nestedType.MakeGenericType(allArgs) + : nestedType; + } + catch + { + return null; + } + } + else + { + currentType = nestedType; + } + + remaining = remaining.Substring(nextRemainingIndex); + } + + return currentType; + } + + private static int FindMatchingCloseBracket(string typeName, int openIndex) + { + var depth = 0; + for (int i = openIndex; i < typeName.Length; i++) + { + if (typeName[i] == '<') + depth++; + else if (typeName[i] == '>') + { + depth--; + if (depth == 0) + return i; + } + } + return -1; + } + + private static string[]? ParseCSharpGenericArguments(string argsString) + { + if (string.IsNullOrWhiteSpace(argsString)) + return null; + + var args = new System.Collections.Generic.List(); + var depth = 0; + var currentArg = new System.Text.StringBuilder(); + + for (int i = 0; i < argsString.Length; i++) + { + var c = argsString[i]; + + if (c == '<') + { + depth++; + currentArg.Append(c); + } + else if (c == '>') + { + depth--; + currentArg.Append(c); + } + else if (c == ',' && depth == 0) + { + var arg = currentArg.ToString().Trim(); + if (!string.IsNullOrEmpty(arg)) + args.Add(arg); + currentArg.Clear(); + } + else + { + currentArg.Append(c); + } + } + + var lastArg = currentArg.ToString().Trim(); + if (!string.IsNullOrEmpty(lastArg)) + args.Add(lastArg); + + return args.Count > 0 ? args.ToArray() : null; + } + + private static Type? TryResolveClassicGenericType(string typeName) + { + var backtickIndex = typeName.IndexOf('`'); + if (backtickIndex < 0) + return null; + + var argsStart = typeName.IndexOf("[[", backtickIndex); + if (argsStart < 0) + return null; + + var genericDefName = typeName.Substring(0, argsStart); + var genericDef = ResolveSimpleType(genericDefName); + if (genericDef == null || !genericDef.IsGenericTypeDefinition) + return null; + + var typeArgs = ParseGenericArguments(typeName, argsStart); + if (typeArgs == null || typeArgs.Length != genericDef.GetGenericArguments().Length) + return null; + + try + { + return genericDef.MakeGenericType(typeArgs); + } + catch + { + return null; + } + } + + private static Type? TryResolveClassicGenericType(string assemblyPrefix, string typeName) + { + var backtickIndex = typeName.IndexOf('`'); + if (backtickIndex < 0) + return null; + + var argsStart = typeName.IndexOf("[[", backtickIndex); + if (argsStart < 0) + return null; + + var genericDefName = typeName.Substring(0, argsStart); + var genericDef = ResolveSimpleType(assemblyPrefix, genericDefName); + if (genericDef == null || !genericDef.IsGenericTypeDefinition) + return null; + + var typeArgs = ParseGenericArguments(assemblyPrefix, typeName, argsStart); + if (typeArgs == null || typeArgs.Length != genericDef.GetGenericArguments().Length) + return null; + + try + { + return genericDef.MakeGenericType(typeArgs); + } + catch + { + return null; + } + } + + private static Type[]? ParseGenericArguments(string typeName, int startIndex) + { + var args = new System.Collections.Generic.List(); + var depth = 0; + var currentArg = new System.Text.StringBuilder(); + + for (int i = startIndex; i < typeName.Length; i++) + { + var c = typeName[i]; + + if (c == '[') + { + depth++; + if (depth > 2) + currentArg.Append(c); + } + else if (c == ']') + { + depth--; + if (depth == 1) + { + var argTypeName = currentArg.ToString().Trim(); + if (!string.IsNullOrEmpty(argTypeName)) + { + var argType = GetType(argTypeName); + if (argType == null) + return null; + args.Add(argType); + } + currentArg.Clear(); + } + else if (depth > 1) + { + currentArg.Append(c); + } + else if (depth == 0) + { + break; + } + } + else if (c == ',' && depth == 1) + { + } + else if (depth > 1) + { + currentArg.Append(c); + } + } + + return args.Count > 0 ? args.ToArray() : null; + } + + private static Type[]? ParseGenericArguments(string assemblyPrefix, string typeName, int startIndex) + { + var args = new System.Collections.Generic.List(); + var depth = 0; + var currentArg = new System.Text.StringBuilder(); + + for (int i = startIndex; i < typeName.Length; i++) + { + var c = typeName[i]; + + if (c == '[') + { + depth++; + if (depth > 2) + currentArg.Append(c); + } + else if (c == ']') + { + depth--; + if (depth == 1) + { + var argTypeName = currentArg.ToString().Trim(); + if (!string.IsNullOrEmpty(argTypeName)) + { + var argType = GetType(assemblyPrefix, argTypeName); + if (argType == null) + return null; + args.Add(argType); + } + currentArg.Clear(); + } + else if (depth > 1) + { + currentArg.Append(c); + } + else if (depth == 0) + { + break; + } + } + else if (c == ',' && depth == 1) + { + } + else if (depth > 1) + { + currentArg.Append(c); + } + } + + return args.Count > 0 ? args.ToArray() : null; + } + } +} diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.cs new file mode 100644 index 00000000..a40bf49f --- /dev/null +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.cs @@ -0,0 +1,133 @@ +using System; +using System.Linq; + +namespace com.IvanMurzak.ReflectorNet.Utils +{ + public static partial class TypeUtils + { + /// + /// Retrieves a by its name. + /// + /// The name of the type to retrieve. Can be a full name, assembly qualified name, or a custom identifier. + /// The corresponding to the specified name, or if the type cannot be found. + public static Type? GetType(string? typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + return null; + + if (_typeCache.TryGetValue(typeName, out var cachedType)) + return cachedType; + + Type? type = null; + try + { + type = Type.GetType(typeName, throwOnError: false); + } + catch + { + } + + if (type != null) + { + _typeCache[typeName] = type; + return type; + } + + type = TryResolveArrayType(typeName); + if (type != null) + { + _typeCache[typeName] = type; + return type; + } + + type = TryResolveCSharpGenericType(typeName); + if (type != null) + { + _typeCache[typeName] = type; + return type; + } + + type = TryResolveClassicGenericType(typeName); + if (type != null) + { + _typeCache[typeName] = type; + return type; + } + + type = AssemblyUtils.AllTypes.FirstOrDefault(t => + typeName == t.FullName || + typeName == t.AssemblyQualifiedName || + typeName == t.GetTypeId()); + + _typeCache[typeName] = type; + + return type; + } + + /// + /// Retrieves a by its name and assembly name. + /// + /// The name of the assembly containing the type. + /// The name of the type to retrieve. + /// The corresponding to the specified name and assembly, or if the type cannot be found. + public static Type? GetType(string? assemblyName, string? typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + return null; + + if (string.IsNullOrEmpty(assemblyName)) + return GetType(typeName); + + var cacheKey = $"{assemblyName}|{typeName}"; + if (_assemblyTypeCache.TryGetValue(cacheKey, out var cachedType)) + return cachedType; + + Type? type = null; + try + { + type = Type.GetType(typeName, throwOnError: false); + if (type != null && !IsTypeInMatchingAssembly(type, assemblyName)) + type = null; + } + catch + { + } + + if (type != null) + { + _assemblyTypeCache[cacheKey] = type; + return type; + } + + type = TryResolveArrayType(assemblyName, typeName); + if (type != null) + { + _assemblyTypeCache[cacheKey] = type; + return type; + } + + type = TryResolveCSharpGenericType(assemblyName, typeName); + if (type != null) + { + _assemblyTypeCache[cacheKey] = type; + return type; + } + + type = TryResolveClassicGenericType(assemblyName, typeName); + if (type != null) + { + _assemblyTypeCache[cacheKey] = type; + return type; + } + + type = AssemblyUtils.GetTypesStartingWith(assemblyName).FirstOrDefault(t => + typeName == t.FullName || + typeName == t.AssemblyQualifiedName || + typeName == t.GetTypeId()); + + _assemblyTypeCache[cacheKey] = type; + + return type; + } + } +} diff --git a/ReflectorNet/src/Utils/TypeUtils.Helpers.cs b/ReflectorNet/src/Utils/TypeUtils.Helpers.cs new file mode 100644 index 00000000..1ff5cbfd --- /dev/null +++ b/ReflectorNet/src/Utils/TypeUtils.Helpers.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet.Model; + +namespace com.IvanMurzak.ReflectorNet.Utils +{ + public static partial class TypeUtils + { + public static bool IsAssignableTo(object? obj, Type targetType) + { + if (targetType == null) + return false; + + if (obj == null) + return !targetType.IsValueType || Nullable.GetUnderlyingType(targetType) != null; + + return targetType.IsAssignableFrom(obj.GetType()); + } + + public static bool IsCastable(Type? type, Type to) + { + if (type == null || to == null) + return false; + + type = Nullable.GetUnderlyingType(type) ?? type; + to = Nullable.GetUnderlyingType(to) ?? to; + + if (to.IsAssignableFrom(type)) + return true; + + if (type.IsPrimitive && to.IsPrimitive) + return true; + + if (type == typeof(string) && to == typeof(object)) + return true; + + return false; + } + + public static int GetInheritanceDistance(Type baseType, Type targetType) + { + if (!baseType.IsAssignableFrom(targetType)) + return -1; + + var distance = 0; + var current = targetType; + while (current != null && current != baseType) + { + current = current.BaseType; + distance++; + } + return current == baseType ? distance : -1; + } + + public static bool IsPrimitive(Type type) + { + return type.IsPrimitive || + type.IsEnum || + type == typeof(string) || + type == typeof(decimal) || + type == typeof(DateTime) || + type == typeof(DateTimeOffset) || + type == typeof(TimeSpan) || + type == typeof(Guid); + } + + public static IEnumerable GetGenericTypes(Type type, HashSet? visited = null) + { + visited ??= new HashSet(); + if (visited.Contains(type.GetHashCode())) + yield break; + + if (type.IsGenericType) + { + var genericArguments = type.GetGenericArguments(); + if (genericArguments != null) + { + foreach (var genericArgument in genericArguments) + { + var compositeHashCode = type.GetHashCode() ^ (genericArgument.GetHashCode() * 397); + if (visited.Contains(compositeHashCode)) + continue; + + visited.Add(compositeHashCode); + yield return genericArgument; + + foreach (var nestedGenericType in GetGenericTypes(genericArgument, visited)) + yield return nestedGenericType; + } + } + } + + if (type.BaseType == null) + yield break; + if (visited.Contains(type.BaseType.GetHashCode())) + yield break; + + foreach (var baseGenericType in GetGenericTypes(type.BaseType, visited)) + yield return baseGenericType; + } + + public static Type? GetTypeWithObjectPriority(object? obj, Type? fallbackType, out string? error) + { + var type = obj?.GetType() ?? fallbackType; + if (type == null) + { + error = $"Object is null and type is unknown. Provide proper {nameof(SerializedMember.typeName)}."; + return null; + } + + error = null; + return type; + } + + public static Type? GetTypeWithNamePriority(SerializedMember? member, Type? fallbackType, out string? error) + { + if (StringUtils.IsNullOrEmpty(member?.typeName) && fallbackType == null) + { + error = $"{nameof(SerializedMember)}.{nameof(SerializedMember.typeName)} is null or empty. Provide proper {nameof(SerializedMember.typeName)}."; + return null; + } + + var type = GetType(member?.typeName); + if (type == null) + { + if (fallbackType == null) + { + error = $"Type '{member?.typeName}' not found."; + return null; + } + error = null; + return fallbackType; + } + + error = null; + return type; + } + + public static Type? GetTypeWithValuePriority(Type? type, SerializedMember? fallbackMember, out string? error) + { + if (type == null) + { + if (fallbackMember == null) + { + error = $"Type is unknown and {nameof(SerializedMember)}.{nameof(SerializedMember.typeName)} is null or empty."; + return null; + } + type = GetType(fallbackMember?.typeName); + if (type == null) + { + error = $"Type '{fallbackMember?.typeName}' not found."; + return null; + } + error = null; + } + + error = null; + return type; + } + } +} diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index ec8e21b6..97a6b018 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -5,1472 +5,13 @@ * Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information. */ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Reflection; -using com.IvanMurzak.ReflectorNet.Model; -using Microsoft.Extensions.Logging; - namespace com.IvanMurzak.ReflectorNet.Utils { + // This file remains as a minimal placeholder. Functionality has been split + // into smaller partial files: TypeUtils.Cache.cs, TypeUtils.Resolve.cs, + // TypeUtils.Collections.cs, TypeUtils.Description.cs, TypeUtils.Helpers.cs, + // and TypeUtils.Name.cs. public static partial class TypeUtils { - /// - /// Maximum capacity for the type name resolution cache. - /// - public const int TypeCacheCapacity = 1000; - - /// - /// Maximum capacity for the enumerable item type cache. - /// - public const int EnumerableItemTypeCacheCapacity = 500; - - /// - /// Gets all types from all loaded assemblies. - /// - /// - /// This property delegates to and provides - /// exception-safe enumeration of types across all loaded assemblies. - /// - public static IEnumerable AllTypes => AssemblyUtils.AllTypes; - - // Characters used to separate nested type names (e.g. `Outer+Inner` or `Outer.Inner`). - private static readonly char[] NestedTypeSeparators = new[] { '+', '.' }; - - // LRU cache for resolved type names to avoid repeated AllTypes enumeration (thread-safe) - private static readonly LruCache _typeCache = new(TypeCacheCapacity); - - // LRU cache for assembly-prefixed type lookups (thread-safe) - // Key format: "assemblyPrefix|typeName" - private static readonly LruCache _assemblyTypeCache = new(TypeCacheCapacity); - - // LRU cache for enumerable item types to avoid repeated interface/inheritance walks (thread-safe) - private static readonly LruCache _enumerableItemTypeCache = new(EnumerableItemTypeCacheCapacity); - - /// - /// Clears the type name resolution cache. - /// - public static void ClearTypeCache(ILogger? logger = null) - { - logger?.LogDebug("Clearing type resolution cache with {count} entries (capacity: {capacity}).", - _typeCache.Count, _typeCache.Capacity); - _typeCache.Clear(); - } - - /// - /// Clears the enumerable item type cache. - /// - public static void ClearEnumerableItemTypeCache(ILogger? logger = null) - { - logger?.LogDebug("Clearing enumerable item type cache with {count} entries (capacity: {capacity}).", - _enumerableItemTypeCache.Count, _enumerableItemTypeCache.Capacity); - _enumerableItemTypeCache.Clear(); - } - - /// - /// Clears the assembly-prefixed type resolution cache. - /// - public static void ClearAssemblyTypeCache(ILogger? logger = null) - { - logger?.LogDebug("Clearing assembly-prefixed type resolution cache with {count} entries (capacity: {capacity}).", - _assemblyTypeCache.Count, _assemblyTypeCache.Capacity); - _assemblyTypeCache.Clear(); - } - - /// - /// 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)) - return null; - - // Check cache first - if (_typeCache.TryGetValue(typeName, out var cachedType)) - return cachedType; - - // First try built-in Type.GetType() which handles many formats - Type? type = null; - try - { - type = Type.GetType(typeName, throwOnError: false); - } - catch - { - // Ignore exceptions (e.g. invalid assembly name) and try other resolution methods - } - - if (type != null) - { - _typeCache[typeName] = type; - return type; - } - - // Try resolving array types (e.g., "Namespace.Type[]") - type = TryResolveArrayType(typeName); - if (type != null) - { - _typeCache[typeName] = type; - return type; - } - - // Try resolving C#-style generic types (e.g., "Namespace.Generic" or "Namespace.Generic") - type = TryResolveCSharpGenericType(typeName); - if (type != null) - { - _typeCache[typeName] = type; - return type; - } - - // Try resolving generic types (e.g., "Namespace.Generic`1[[TypeArg]]") - type = TryResolveClassicGenericType(typeName); - if (type != null) - { - _typeCache[typeName] = type; - return type; - } - - // If Type.GetType() fails, try to find the type in all loaded assemblies - type = AssemblyUtils.AllTypes.FirstOrDefault(t => - typeName == t.FullName || - typeName == t.AssemblyQualifiedName || - typeName == t.GetTypeId()); - - // Caching the result (even if null) - _typeCache[typeName] = type; - - return type; - } - - /// - /// Resolves a from its string representation, searching only in assemblies - /// whose names start with the specified prefix. - /// - /// - /// This method attempts to resolve types using multiple strategies in order: - /// - /// Built-in with assembly verification - /// 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 assemblies matching the prefix by FullName, AssemblyQualifiedName, or TypeId - /// - /// Results are cached for performance. Use to clear the cache. - /// - /// The assembly name prefix to filter assemblies. If null or empty, delegates to . - /// 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? assemblyName, string? typeName) - { - if (string.IsNullOrWhiteSpace(typeName)) - return null; - - // If no assembly filter, delegate to the standard method - if (string.IsNullOrEmpty(assemblyName)) - return GetType(typeName); - - // Check cache first (key format: "assemblyPrefix|typeName") - var cacheKey = $"{assemblyName}|{typeName}"; - if (_assemblyTypeCache.TryGetValue(cacheKey, out var cachedType)) - return cachedType; - - // First try built-in Type.GetType() which handles many formats - Type? type = null; - try - { - type = Type.GetType(typeName, throwOnError: false); - // Verify the resolved type is in a matching assembly - if (type != null && !IsTypeInMatchingAssembly(type, assemblyName)) - type = null; - } - catch - { - // Ignore exceptions (e.g. invalid assembly name) and try other resolution methods - } - - if (type != null) - { - _assemblyTypeCache[cacheKey] = type; - return type; - } - - // Try resolving array types (e.g., "Namespace.Type[]") - type = TryResolveArrayType(assemblyName, typeName); - if (type != null) - { - _assemblyTypeCache[cacheKey] = type; - return type; - } - - // Try resolving C#-style generic types (e.g., "Namespace.Generic" or "Namespace.Generic") - type = TryResolveCSharpGenericType(assemblyName, typeName); - if (type != null) - { - _assemblyTypeCache[cacheKey] = type; - return type; - } - - // Try resolving generic types (e.g., "Namespace.Generic`1[[TypeArg]]") - type = TryResolveClassicGenericType(assemblyName, typeName); - if (type != null) - { - _assemblyTypeCache[cacheKey] = type; - return type; - } - - // Search in types from matching assemblies only - type = AssemblyUtils.GetTypesStartingWith(assemblyName).FirstOrDefault(t => - typeName == t.FullName || - typeName == t.AssemblyQualifiedName || - typeName == t.GetTypeId()); - - // Caching the result (even if null) - _assemblyTypeCache[cacheKey] = type; - - return type; - } - - /// - /// Checks if a type belongs to an assembly whose name starts with the specified prefix. - /// - private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) - { - var assemblyName = type.Assembly.GetName().Name; - return assemblyName != null && assemblyName.StartsWith(assemblyPrefix, StringComparison.Ordinal); - } - - /// - /// Attempts to resolve a simple (non-generic, non-array) type by name. - /// - private static Type? ResolveSimpleType(string name) - { - var type = Type.GetType(name, throwOnError: false); - if (type != null) - return type; - - return AssemblyUtils.AllTypes.FirstOrDefault(t => - name == t.AssemblyQualifiedName || - name == t.FullName || - name == t.Name); - } - - /// - /// Attempts to resolve a simple (non-generic, non-array) type by name, - /// searching only in assemblies whose names start with the specified prefix. - /// - private static Type? ResolveSimpleType(string assemblyPrefix, string name) - { - var type = Type.GetType(name, throwOnError: false); - if (type != null && IsTypeInMatchingAssembly(type, assemblyPrefix)) - return type; - - return AssemblyUtils.GetTypesStartingWith(assemblyPrefix).FirstOrDefault(t => - name == t.AssemblyQualifiedName || - name == t.FullName || - name == t.Name); - } - - /// - /// Attempts to resolve array type names (e.g., "Namespace.Type[]"). - /// - private static Type? TryResolveArrayType(string typeName) - { - if (!typeName.EndsWith("]")) - return null; - - var lastOpenBracket = typeName.LastIndexOf('['); - if (lastOpenBracket < 0) - return null; - - var suffix = typeName.Substring(lastOpenBracket); - // Check if content contains only commas - var content = suffix.Substring(1, suffix.Length - 2); - if (content.Length > 0 && content.Any(c => c != ',')) - return null; - - var commas = content.Length; - var elementTypeName = typeName.Substring(0, lastOpenBracket); - var elementType = GetType(elementTypeName); - - if (elementType == null) return null; - - return commas == 0 - ? elementType.MakeArrayType() - : elementType.MakeArrayType(commas + 1); - } - - /// - /// Attempts to resolve array type names (e.g., "Namespace.Type[]"), - /// searching only in assemblies whose names start with the specified prefix. - /// - private static Type? TryResolveArrayType(string assemblyPrefix, string typeName) - { - if (!typeName.EndsWith("]")) - return null; - - var lastOpenBracket = typeName.LastIndexOf('['); - if (lastOpenBracket < 0) - return null; - - var suffix = typeName.Substring(lastOpenBracket); - // Check if content contains only commas - var content = suffix.Substring(1, suffix.Length - 2); - if (content.Length > 0 && content.Any(c => c != ',')) - return null; - - var commas = content.Length; - var elementTypeName = typeName.Substring(0, lastOpenBracket); - var elementType = GetType(assemblyPrefix, elementTypeName); - - if (elementType == null) return null; - - return commas == 0 - ? elementType.MakeArrayType() - : elementType.MakeArrayType(commas + 1); - } - - /// - /// Attempts to resolve C#-style generic types. - /// Handles formats like: "Namespace.Generic<TypeArg>" or "Namespace.Generic<TypeArg1, TypeArg2>" - /// Also handles nested types: "Namespace.Generic<TypeArg>+Nested" or "Namespace.Generic<TypeArg>+Nested<TypeArg2>" - /// Space after comma is optional. - /// - private static Type? TryResolveCSharpGenericType(string typeName) - { - // Find the opening angle bracket - var openBracketIndex = typeName.IndexOf('<'); - if (openBracketIndex < 0) - return null; - - // Find the matching closing angle bracket - var closeBracketIndex = FindMatchingCloseBracket(typeName, openBracketIndex); - if (closeBracketIndex < 0) - return null; - - // Extract the base type name (everything before '<') - var baseTypeName = typeName.Substring(0, openBracketIndex); - if (string.IsNullOrWhiteSpace(baseTypeName)) - return null; - - // Extract the type arguments string (between '<' and '>') - var typeArgsString = typeName.Substring(openBracketIndex + 1, closeBracketIndex - openBracketIndex - 1); - - // Parse the type arguments - var typeArgNames = ParseCSharpGenericArguments(typeArgsString); - if (typeArgNames == null || typeArgNames.Length == 0) - return null; - - // Construct the generic type definition name (e.g., "Namespace.Generic`2") - var genericDefName = $"{baseTypeName}`{typeArgNames.Length}"; - - // Resolve the generic type definition - var genericDef = ResolveSimpleType(genericDefName); - if (genericDef == null || !genericDef.IsGenericTypeDefinition) - return null; - - // Resolve each type argument - var typeArgs = new Type[typeArgNames.Length]; - for (int i = 0; i < typeArgNames.Length; i++) - { - var argType = GetType(typeArgNames[i].Trim()); - if (argType == null) - return null; - typeArgs[i] = argType; - } - - Type? currentType; - try - { - currentType = genericDef.MakeGenericType(typeArgs); - } - catch - { - return null; - } - - // Handle nested types appended after the generic arguments - var remaining = typeName.Substring(closeBracketIndex + 1); - while (!string.IsNullOrEmpty(remaining)) - { - if (!remaining.StartsWith("+") && !remaining.StartsWith(".")) - return null; - - remaining = remaining.Substring(1); // Remove separator - - // Check for generic args - var open = remaining.IndexOf('<'); - string nestedName; - Type[]? nestedArgs = null; - int nextRemainingIndex; - - if (open > 0) - { - var close = FindMatchingCloseBracket(remaining, open); - if (close < 0) return null; - - nestedName = remaining.Substring(0, open); - var argsStr = remaining.Substring(open + 1, close - open - 1); - var argNames = ParseCSharpGenericArguments(argsStr); - if (argNames == null) return null; - - nestedArgs = new Type[argNames.Length]; - for (int i = 0; i < argNames.Length; i++) - { - var tempType = GetType(argNames[i]?.Trim()); - if (tempType == null) return null; - nestedArgs[i] = tempType; - } - - nextRemainingIndex = close + 1; - } - else - { - // No generic args, but check if there are more separators - var nextSep = remaining.IndexOfAny(NestedTypeSeparators); - if (nextSep > 0) - { - nestedName = remaining.Substring(0, nextSep); - nextRemainingIndex = nextSep; - } - else - { - nestedName = remaining; - nextRemainingIndex = remaining.Length; - } - } - - // Find nested type - Type? nestedType; - Type[] allArgs; - - if (nestedArgs != null) - { - nestedType = currentType.GetNestedType($"{nestedName}`{nestedArgs.Length}"); - if (nestedType == null) return null; - - if (currentType.IsGenericType && !currentType.IsGenericTypeDefinition) - { - var parentArgs = currentType.GetGenericArguments(); - allArgs = new Type[parentArgs.Length + nestedArgs.Length]; - Array.Copy(parentArgs, allArgs, parentArgs.Length); - Array.Copy(nestedArgs, 0, allArgs, parentArgs.Length, nestedArgs.Length); - } - else - { - allArgs = nestedArgs; - } - } - else - { - nestedType = currentType.GetNestedType(nestedName); - if (nestedType == null) return null; - - allArgs = currentType.IsGenericType && !currentType.IsGenericTypeDefinition - ? currentType.GetGenericArguments() - : Type.EmptyTypes; - } - - if (nestedType.IsGenericTypeDefinition) - { - try - { - currentType = nestedType.GetGenericArguments().Length == allArgs.Length - ? nestedType.MakeGenericType(allArgs) - : nestedType; - } - catch - { - return null; - } - } - else - { - currentType = nestedType; - } - - remaining = remaining.Substring(nextRemainingIndex); - } - - return currentType; - } - - /// - /// Attempts to resolve C#-style generic types, - /// searching only in assemblies whose names start with the specified prefix. - /// Handles formats like: "Namespace.Generic<TypeArg>" or "Namespace.Generic<TypeArg1, TypeArg2>" - /// Also handles nested types: "Namespace.Generic<TypeArg>+Nested" or "Namespace.Generic<TypeArg>+Nested<TypeArg2>" - /// Space after comma is optional. - /// - private static Type? TryResolveCSharpGenericType(string assemblyPrefix, string typeName) - { - // Find the opening angle bracket - var openBracketIndex = typeName.IndexOf('<'); - if (openBracketIndex < 0) - return null; - - // Find the matching closing angle bracket - var closeBracketIndex = FindMatchingCloseBracket(typeName, openBracketIndex); - if (closeBracketIndex < 0) - return null; - - // Extract the base type name (everything before '<') - var baseTypeName = typeName.Substring(0, openBracketIndex); - if (string.IsNullOrWhiteSpace(baseTypeName)) - return null; - - // Extract the type arguments string (between '<' and '>') - var typeArgsString = typeName.Substring(openBracketIndex + 1, closeBracketIndex - openBracketIndex - 1); - - // Parse the type arguments - var typeArgNames = ParseCSharpGenericArguments(typeArgsString); - if (typeArgNames == null || typeArgNames.Length == 0) - return null; - - // Construct the generic type definition name (e.g., "Namespace.Generic`2") - var genericDefName = $"{baseTypeName}`{typeArgNames.Length}"; - - // Resolve the generic type definition - var genericDef = ResolveSimpleType(assemblyPrefix, genericDefName); - if (genericDef == null || !genericDef.IsGenericTypeDefinition) - return null; - - // Resolve each type argument - var typeArgs = new Type[typeArgNames.Length]; - for (int i = 0; i < typeArgNames.Length; i++) - { - var argType = GetType(assemblyPrefix, typeArgNames[i].Trim()); - if (argType == null) - return null; - typeArgs[i] = argType; - } - - Type? currentType; - try - { - currentType = genericDef.MakeGenericType(typeArgs); - } - catch - { - return null; - } - - // Handle nested types appended after the generic arguments - var remaining = typeName.Substring(closeBracketIndex + 1); - while (!string.IsNullOrEmpty(remaining)) - { - if (!remaining.StartsWith("+") && !remaining.StartsWith(".")) - return null; - - remaining = remaining.Substring(1); // Remove separator - - // Check for generic args - var open = remaining.IndexOf('<'); - string nestedName; - Type[]? nestedArgs = null; - int nextRemainingIndex; - - if (open > 0) - { - var close = FindMatchingCloseBracket(remaining, open); - if (close < 0) return null; - - nestedName = remaining.Substring(0, open); - var argsStr = remaining.Substring(open + 1, close - open - 1); - var argNames = ParseCSharpGenericArguments(argsStr); - if (argNames == null) return null; - - nestedArgs = new Type[argNames.Length]; - for (int i = 0; i < argNames.Length; i++) - { - var tempType = GetType(assemblyPrefix, argNames[i]?.Trim()); - if (tempType == null) return null; - nestedArgs[i] = tempType; - } - - nextRemainingIndex = close + 1; - } - else - { - // No generic args, but check if there are more separators - var nextSep = remaining.IndexOfAny(NestedTypeSeparators); - if (nextSep > 0) - { - nestedName = remaining.Substring(0, nextSep); - nextRemainingIndex = nextSep; - } - else - { - nestedName = remaining; - nextRemainingIndex = remaining.Length; - } - } - - // Find nested type - Type? nestedType; - Type[] allArgs; - - if (nestedArgs != null) - { - nestedType = currentType.GetNestedType($"{nestedName}`{nestedArgs.Length}"); - if (nestedType == null) return null; - - if (currentType.IsGenericType && !currentType.IsGenericTypeDefinition) - { - var parentArgs = currentType.GetGenericArguments(); - allArgs = new Type[parentArgs.Length + nestedArgs.Length]; - Array.Copy(parentArgs, allArgs, parentArgs.Length); - Array.Copy(nestedArgs, 0, allArgs, parentArgs.Length, nestedArgs.Length); - } - else - { - allArgs = nestedArgs; - } - } - else - { - nestedType = currentType.GetNestedType(nestedName); - if (nestedType == null) return null; - - allArgs = currentType.IsGenericType && !currentType.IsGenericTypeDefinition - ? currentType.GetGenericArguments() - : Type.EmptyTypes; - } - - if (nestedType.IsGenericTypeDefinition) - { - try - { - currentType = nestedType.GetGenericArguments().Length == allArgs.Length - ? nestedType.MakeGenericType(allArgs) - : nestedType; - } - catch - { - return null; - } - } - else - { - currentType = nestedType; - } - - remaining = remaining.Substring(nextRemainingIndex); - } - - return currentType; - } - - /// - /// Finds the matching closing angle bracket for an opening bracket. - /// Handles nested generic types properly. - /// - private static int FindMatchingCloseBracket(string typeName, int openIndex) - { - var depth = 0; - for (int i = openIndex; i < typeName.Length; i++) - { - if (typeName[i] == '<') - depth++; - else if (typeName[i] == '>') - { - depth--; - if (depth == 0) - return i; - } - } - return -1; - } - - /// - /// Parses C#-style generic arguments from a string like "TypeArg1, TypeArg2" or "List<int>, string". - /// Handles nested generic types by tracking bracket depth. - /// - private static string[]? ParseCSharpGenericArguments(string argsString) - { - if (string.IsNullOrWhiteSpace(argsString)) - return null; - - var args = new List(); - var depth = 0; - var currentArg = new System.Text.StringBuilder(); - - for (int i = 0; i < argsString.Length; i++) - { - var c = argsString[i]; - - if (c == '<') - { - depth++; - currentArg.Append(c); - } - else if (c == '>') - { - depth--; - currentArg.Append(c); - } - else if (c == ',' && depth == 0) - { - // Top-level comma - separator between type arguments - var arg = currentArg.ToString().Trim(); - if (!string.IsNullOrEmpty(arg)) - args.Add(arg); - currentArg.Clear(); - } - else - { - currentArg.Append(c); - } - } - - // Add the last argument - var lastArg = currentArg.ToString().Trim(); - if (!string.IsNullOrEmpty(lastArg)) - args.Add(lastArg); - - return args.Count > 0 ? args.ToArray() : null; - } - - /// - /// Attempts to resolve constructed generic types by parsing and reconstructing them. - /// Handles formats like: "Namespace.Generic`1[[TypeArg, Assembly]]" - /// - private static Type? TryResolveClassicGenericType(string typeName) - { - // Find generic arity marker (backtick) - var backtickIndex = typeName.IndexOf('`'); - if (backtickIndex < 0) - return null; - - // Find the start of generic arguments [[...]] - var argsStart = typeName.IndexOf("[[", backtickIndex); - if (argsStart < 0) - return null; - - // Extract generic definition name (e.g., "Namespace.WrapperClass`1") - var genericDefName = typeName.Substring(0, argsStart); - - // Resolve the generic type definition - var genericDef = ResolveSimpleType(genericDefName); - if (genericDef == null || !genericDef.IsGenericTypeDefinition) - return null; - - // Parse and resolve type arguments - var typeArgs = ParseGenericArguments(typeName, argsStart); - if (typeArgs == null || typeArgs.Length != genericDef.GetGenericArguments().Length) - return null; - - try - { - return genericDef.MakeGenericType(typeArgs); - } - catch - { - return null; - } - } - - /// - /// Attempts to resolve constructed generic types by parsing and reconstructing them, - /// searching only in assemblies whose names start with the specified prefix. - /// Handles formats like: "Namespace.Generic`1[[TypeArg, Assembly]]" - /// - private static Type? TryResolveClassicGenericType(string assemblyPrefix, string typeName) - { - // Find generic arity marker (backtick) - var backtickIndex = typeName.IndexOf('`'); - if (backtickIndex < 0) - return null; - - // Find the start of generic arguments [[...]] - var argsStart = typeName.IndexOf("[[", backtickIndex); - if (argsStart < 0) - return null; - - // Extract generic definition name (e.g., "Namespace.WrapperClass`1") - var genericDefName = typeName.Substring(0, argsStart); - - // Resolve the generic type definition - var genericDef = ResolveSimpleType(assemblyPrefix, genericDefName); - if (genericDef == null || !genericDef.IsGenericTypeDefinition) - return null; - - // Parse and resolve type arguments - var typeArgs = ParseGenericArguments(assemblyPrefix, typeName, argsStart); - if (typeArgs == null || typeArgs.Length != genericDef.GetGenericArguments().Length) - return null; - - try - { - return genericDef.MakeGenericType(typeArgs); - } - catch - { - return null; - } - } - - /// - /// Parses generic arguments from the [[Type1, Assembly], [Type2, Assembly]] format. - /// The format uses double brackets where: - /// - Outer [] wraps all type arguments - /// - Inner [] wraps each individual type argument - /// - Nested generic types have their own [[]] inside - /// - private static Type[]? ParseGenericArguments(string typeName, int startIndex) - { - var args = new List(); - var depth = 0; - var currentArg = new System.Text.StringBuilder(); - - for (int i = startIndex; i < typeName.Length; i++) - { - var c = typeName[i]; - - if (c == '[') - { - depth++; - // Only append brackets for nested generics (depth > 2) - // depth 1 = outer wrapper for all args - // depth 2 = wrapper for individual type arg (don't include) - // depth 3+ = nested generic brackets (include) - if (depth > 2) - currentArg.Append(c); - } - else if (c == ']') - { - depth--; - if (depth == 1) - { - // End of one type argument - var argTypeName = currentArg.ToString().Trim(); - if (!string.IsNullOrEmpty(argTypeName)) - { - var argType = GetType(argTypeName); - if (argType == null) - return null; - args.Add(argType); - } - currentArg.Clear(); - } - else if (depth > 1) - { - // Append closing brackets for nested generics - currentArg.Append(c); - } - else if (depth == 0) - { - break; // End of all arguments - } - } - else if (c == ',' && depth == 1) - { - // Separator between type arguments at the top level - skip it - } - else if (depth > 1) - { - currentArg.Append(c); - } - } - - return args.Count > 0 ? args.ToArray() : null; - } - - /// - /// Parses generic arguments from the [[Type1, Assembly], [Type2, Assembly]] format, - /// searching only in assemblies whose names start with the specified prefix. - /// The format uses double brackets where: - /// - Outer [] wraps all type arguments - /// - Inner [] wraps each individual type argument - /// - Nested generic types have their own [[]] inside - /// - private static Type[]? ParseGenericArguments(string assemblyPrefix, string typeName, int startIndex) - { - var args = new List(); - var depth = 0; - var currentArg = new System.Text.StringBuilder(); - - for (int i = startIndex; i < typeName.Length; i++) - { - var c = typeName[i]; - - if (c == '[') - { - depth++; - // Only append brackets for nested generics (depth > 2) - // depth 1 = outer wrapper for all args - // depth 2 = wrapper for individual type arg (don't include) - // depth 3+ = nested generic brackets (include) - if (depth > 2) - currentArg.Append(c); - } - else if (c == ']') - { - depth--; - if (depth == 1) - { - // End of one type argument - var argTypeName = currentArg.ToString().Trim(); - if (!string.IsNullOrEmpty(argTypeName)) - { - var argType = GetType(assemblyPrefix, argTypeName); - if (argType == null) - return null; - args.Add(argType); - } - currentArg.Clear(); - } - else if (depth > 1) - { - // Append closing brackets for nested generics - currentArg.Append(c); - } - else if (depth == 0) - { - break; // End of all arguments - } - } - else if (c == ',' && depth == 1) - { - // Separator between type arguments at the top level - skip it - } - else if (depth > 1) - { - currentArg.Append(c); - } - } - - 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 && - (type.GetGenericTypeDefinition() == typeof(Dictionary<,>) || - type.GetGenericTypeDefinition() == typeof(IDictionary<,>))) - { - return true; - } - - return type.GetInterfaces() - .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 && - (type.GetGenericTypeDefinition() == typeof(Dictionary<,>) || - type.GetGenericTypeDefinition() == typeof(IDictionary<,>))) - { - return type.GetGenericArguments(); - } - - var dictionaryInterface = type.GetInterfaces() - .FirstOrDefault(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); - - 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 - .GetCustomAttribute(true) - ?.Description - ?? (type.BaseType != null - ? 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 - ?.GetCustomAttribute(true) - ?.Description - ?? (parameterInfo != null - ? 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) - return null; - - var description = memberInfo - .GetCustomAttribute(true) - ?.Description; - - if (description != null) - return description; - - return memberInfo.MemberType switch - { - MemberTypes.Field => GetFieldDescription((FieldInfo)memberInfo), - MemberTypes.Property => GetPropertyDescription((PropertyInfo)memberInfo), - _ => 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) - return null; - - return fieldInfo - .GetCustomAttribute(true) - ?.Description - ?? (fieldInfo.FieldType != null - ? 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) - return null; - - return propertyInfo - .GetCustomAttribute(true) - ?.Description - ?? (propertyInfo.PropertyType != null - ? 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) - return null; - - // First try to find the member by the exact name (in case no naming policy is applied) - var memberInfo = context.PropertyInfo.DeclaringType - .GetMember( - name: context.PropertyInfo.Name, - bindingAttr: BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .FirstOrDefault(); - - // If not found by exact name, try to convert camelCase back to PascalCase - // This handles the case where JSON naming policy transforms the property name (e.g., PascalCase -> camelCase) - if (memberInfo == null) - { - var pascalCaseName = ToPascalCase(context.PropertyInfo.Name); - memberInfo = context.PropertyInfo.DeclaringType - .GetMember( - name: pascalCaseName, - bindingAttr: BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .FirstOrDefault(); - } - - // If still not found, try to find by case-insensitive name match - if (memberInfo == null) - { - var allMembers = context.PropertyInfo.DeclaringType.GetMembers(BindingFlags.Public | BindingFlags.Instance); - memberInfo = allMembers.FirstOrDefault(m => - string.Equals(m.Name, context.PropertyInfo.Name, StringComparison.OrdinalIgnoreCase)); - } - - if (memberInfo == null) - return null; - - return GetDescription(memberInfo); - } -#endif - - private static string ToPascalCase(string camelCase) - { - if (string.IsNullOrEmpty(camelCase)) - return 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); - return fieldInfo != null ? GetFieldDescription(fieldInfo) : null; - } - - /// - /// Checks if an object's runtime type is assignable to the target type. - /// This is a cross-platform alternative to Type.IsAssignableTo which is only available in .NET 5+. - /// - /// The object to check (can be null) - /// The target type to check assignability to - /// True if the object can be assigned to the target type - public static bool IsAssignableTo(object? obj, Type targetType) - { - if (targetType == null) - return false; - - // Null is assignable to any reference type or nullable value type - if (obj == null) - return !targetType.IsValueType || Nullable.GetUnderlyingType(targetType) != null; - - // Check if the object's type is assignable to the target type - 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) - return false; - - // Handle nullable types - type = Nullable.GetUnderlyingType(type) ?? type; - - // Handle nullable types - to = Nullable.GetUnderlyingType(to) ?? to; - - // Check if the type is assignable to the target type - if (to.IsAssignableFrom(type)) - return true; - - // Check for primitive types - if (type.IsPrimitive && to.IsPrimitive) - return true; - - // Check for string conversion - if (type == typeof(string) && to == typeof(object)) - return true; - - 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)) - return -1; - - var distance = 0; - var current = targetType; - while (current != null && current != baseType) - { - current = current.BaseType; - distance++; - } - 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 || - type.IsEnum || - type == typeof(string) || - type == typeof(decimal) || - type == typeof(DateTime) || - type == typeof(DateTimeOffset) || - 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(); - if (visited.Contains(type.GetHashCode())) - yield break; - - if (type.IsGenericType) - { - var genericArguments = type.GetGenericArguments(); - if (genericArguments != null) - { - foreach (var genericArgument in genericArguments) - { - // HashCode.Combine is not available in netstandard2.0, so use a simple combination - var compositeHashCode = type.GetHashCode() ^ (genericArgument.GetHashCode() * 397); - if (visited.Contains(compositeHashCode)) - continue; - - visited.Add(compositeHashCode); - yield return genericArgument; - - foreach (var nestedGenericType in GetGenericTypes(genericArgument, visited)) - yield return nestedGenericType; - } - } - } - - if (type.BaseType == null) - yield break; - if (visited.Contains(type.BaseType.GetHashCode())) - yield break; - - 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) - return true; // Arrays are IEnumerable - - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - return true; - - return type.GetInterfaces() - .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); - } - - /// - /// Gets the element type of an enumerable or array type. - /// Results are cached for performance when processing large collections. - /// Use to clear the cache if needed. - /// - /// The enumerable type to inspect. - /// The element type (T in ), or null if the type is not enumerable. - public static Type? GetEnumerableItemType(Type type) - { - return _enumerableItemTypeCache.GetOrAdd(type, GetEnumerableItemTypeInternal); - } - - /// - /// Internal implementation of GetEnumerableItemType without caching. - /// - private static Type? GetEnumerableItemTypeInternal(Type type) - { - if (type.IsArray) - return type.GetElementType(); // For arrays, return the element type - - // Check if the type itself is IEnumerable - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - return type.GetGenericArguments().FirstOrDefault(); - - // Check if the type directly implements IEnumerable - var enumerableInterface = type.GetInterfaces() - .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); - - if (enumerableInterface != null) - return enumerableInterface.GetGenericArguments().FirstOrDefault(); - - // Check base types recursively - var baseType = type.BaseType; - while (baseType != null && baseType != typeof(object)) - { - if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - return baseType.GetGenericArguments().FirstOrDefault(); - - // Check if base type implements IEnumerable - enumerableInterface = baseType.GetInterfaces() - .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); - - if (enumerableInterface != null) - return enumerableInterface.GetGenericArguments().FirstOrDefault(); - - baseType = baseType.BaseType; - } - - 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; - if (type == null) - { - error = $"Object is null and type is unknown. Provide proper {nameof(SerializedMember.typeName)}."; - return null; - } - - 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) - { - error = $"{nameof(SerializedMember)}.{nameof(SerializedMember.typeName)} is null or empty. Provide proper {nameof(SerializedMember.typeName)}."; - return null; - } - - var type = GetType(member?.typeName); - if (type == null) - { - if (fallbackType == null) - { - error = $"Type '{member?.typeName}' not found."; - return null; - } - error = null; - return fallbackType; - } - - 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) - { - if (fallbackMember == null) - { - error = $"Type is unknown and {nameof(SerializedMember)}.{nameof(SerializedMember.typeName)} is null or empty."; - return null; - } - type = GetType(fallbackMember?.typeName); - if (type == null) - { - error = $"Type '{fallbackMember?.typeName}' not found."; - return null; - } - error = null; - } - - error = null; - return type; - } } -} +} \ No newline at end of file From b7fd7179f600f11e3567ead9b3ef733cd5fb87e9 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 29 Jan 2026 23:00:48 -0800 Subject: [PATCH 05/17] feat: add unit tests for TypeUtils.GetType method with assembly name filtering --- .../GetTypeWithAssemblyTests.cs | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs diff --git a/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs new file mode 100644 index 00000000..a30c0746 --- /dev/null +++ b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs @@ -0,0 +1,461 @@ +/* + * 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.Generic; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; +using com.IvanMurzak.ReflectorNet.Tests.Model; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.TypeUtilsTests +{ + /// + /// Tests for TypeUtils.GetType(string? assemblyName, string? typeName) method. + /// Verifies that types are correctly resolved when filtering by assembly name prefix. + /// + public class GetTypeWithAssemblyTests : BaseTest + { + public GetTypeWithAssemblyTests(ITestOutputHelper output) : base(output) { } + + #region Constants - Assembly Prefixes + + private const string ReflectorNetPrefix = "ReflectorNet"; + private const string ReflectorNetTestsPrefix = "ReflectorNet.Tests"; + private const string ReflectorNetOuterPrefix = "ReflectorNet.Tests.OuterAssembly"; + private const string SystemPrefix = "System"; + private const string NonExistentPrefix = "NonExistent.Assembly.Prefix"; + + #endregion + + #region Edge Case Tests + + [Fact] + public void GetType_NullAssemblyName_DelegatesToStandardGetType() + { + // When assemblyName is null, should delegate to GetType(typeName) + var type = TypeUtils.GetType(null, "System.Int32"); + Assert.Equal(typeof(int), type); + + var reflectorType = TypeUtils.GetType(null, "com.IvanMurzak.ReflectorNet.Model.SerializedMember"); + Assert.Equal(typeof(SerializedMember), reflectorType); + } + + [Fact] + public void GetType_EmptyAssemblyName_DelegatesToStandardGetType() + { + // When assemblyName is empty, should delegate to GetType(typeName) + var type = TypeUtils.GetType("", "System.String"); + Assert.Equal(typeof(string), type); + + var reflectorType = TypeUtils.GetType("", "com.IvanMurzak.ReflectorNet.Model.MethodRef"); + Assert.Equal(typeof(MethodRef), reflectorType); + } + + [Fact] + public void GetType_NullTypeName_ReturnsNull() + { + Assert.Null(TypeUtils.GetType(ReflectorNetPrefix, null)); + Assert.Null(TypeUtils.GetType(SystemPrefix, null)); + } + + [Fact] + public void GetType_EmptyTypeName_ReturnsNull() + { + Assert.Null(TypeUtils.GetType(ReflectorNetPrefix, "")); + Assert.Null(TypeUtils.GetType(ReflectorNetPrefix, " ")); + } + + [Fact] + public void GetType_NonExistentAssemblyPrefix_ReturnsNull() + { + // Types should not be found when assembly prefix doesn't match any loaded assembly + Assert.Null(TypeUtils.GetType(NonExistentPrefix, "System.Int32")); + Assert.Null(TypeUtils.GetType(NonExistentPrefix, "com.IvanMurzak.ReflectorNet.Model.SerializedMember")); + } + + [Fact] + public void GetType_InvalidTypeName_ReturnsNull() + { + Assert.Null(TypeUtils.GetType(ReflectorNetPrefix, "NonExistent.Type.That.Does.Not.Exist")); + Assert.Null(TypeUtils.GetType(SystemPrefix, "System.NonExistentType")); + } + + #endregion + + #region ReflectorNet Assembly Tests + + [Fact] + public void GetType_ReflectorNetTypes_WithMatchingPrefix() + { + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.Reflector"] = typeof(Reflector), + ["com.IvanMurzak.ReflectorNet.Model.SerializedMember"] = typeof(SerializedMember), + ["com.IvanMurzak.ReflectorNet.Model.SerializedMemberList"] = typeof(SerializedMemberList), + ["com.IvanMurzak.ReflectorNet.Model.MethodRef"] = typeof(MethodRef), + ["com.IvanMurzak.ReflectorNet.Model.MethodData"] = typeof(MethodData), + }; + + ValidateGetTypeWithAssembly(ReflectorNetPrefix, testCases, "ReflectorNet Types with matching prefix"); + } + + [Fact] + public void GetType_ReflectorNetTypes_WithNonMatchingPrefix() + { + // ReflectorNet types should NOT be found when using OuterAssembly prefix + var typeName = "com.IvanMurzak.ReflectorNet.Model.SerializedMember"; + var result = TypeUtils.GetType(ReflectorNetOuterPrefix, typeName); + + _output.WriteLine($"GetType(\"{ReflectorNetOuterPrefix}\", \"{typeName}\") = {result?.FullName ?? "null"}"); + Assert.Null(result); + } + + #endregion + + #region OuterAssembly Tests + + [Fact] + public void GetType_OuterAssemblyTypes_WithMatchingPrefix() + { + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass"] = typeof(OuterSimpleClass), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleStruct"] = typeof(OuterSimpleStruct), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSealedClass"] = typeof(OuterSealedClass), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Person"] = typeof(Person), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Address"] = typeof(Address), + }; + + ValidateGetTypeWithAssembly(ReflectorNetOuterPrefix, testCases, "OuterAssembly Types with matching prefix"); + } + + [Fact] + public void GetType_OuterAssemblyTypes_WithBroaderPrefix() + { + // OuterAssembly types should be found with "ReflectorNet" prefix (broader) + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass"] = typeof(OuterSimpleClass), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Person"] = typeof(Person), + }; + + ValidateGetTypeWithAssembly(ReflectorNetPrefix, testCases, "OuterAssembly Types with broader prefix"); + } + + [Fact] + public void GetType_OuterAssemblyNestedTypes_WithMatchingPrefix() + { + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterContainer+NestedClass"] = typeof(OuterContainer.NestedClass), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterContainer+NestedStruct"] = typeof(OuterContainer.NestedStruct), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterContainer+NestedContainer+DoubleNestedClass"] = typeof(OuterContainer.NestedContainer.DoubleNestedClass), + }; + + ValidateGetTypeWithAssembly(ReflectorNetOuterPrefix, testCases, "OuterAssembly Nested Types"); + } + + #endregion + + #region Generic Types Tests + + [Fact] + public void GetType_GenericTypes_WithMatchingPrefix() + { + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass"] = typeof(OuterGenericClass), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass"] = typeof(OuterGenericClass), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass2"] = typeof(OuterGenericClass2), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericStruct"] = typeof(OuterGenericStruct), + }; + + ValidateGetTypeWithAssembly(ReflectorNetOuterPrefix, testCases, "Generic Types with matching prefix"); + } + + [Fact] + public void GetType_NestedGenericTypes_WithMatchingPrefix() + { + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterContainer+NestedGenericClass"] = typeof(OuterContainer.NestedGenericClass), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericContainer+NestedInGeneric"] = typeof(OuterGenericContainer.NestedInGeneric), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericContainer+NestedGenericInGeneric"] = typeof(OuterGenericContainer.NestedGenericInGeneric), + }; + + ValidateGetTypeWithAssembly(ReflectorNetOuterPrefix, testCases, "Nested Generic Types with matching prefix"); + } + + #endregion + + #region Array Types Tests + + [Fact] + public void GetType_ArrayTypes_WithMatchingPrefix() + { + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass[]"] = typeof(OuterSimpleClass[]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleStruct[]"] = typeof(OuterSimpleStruct[]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass[][]"] = typeof(OuterSimpleClass[][]), + }; + + ValidateGetTypeWithAssembly(ReflectorNetOuterPrefix, testCases, "Array Types with matching prefix"); + } + + [Fact] + public void GetType_GenericArrayTypes_WithBroadPrefix() + { + // Generic array types with System type arguments need a broader prefix + // because System.Int32 is not in ReflectorNet assemblies + // Using no prefix (delegates to standard GetType) for these cases + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass[]"] = typeof(OuterGenericClass[]), + }; + + // Use broader prefix that allows System types to resolve + ValidateGetTypeWithAssembly(ReflectorNetPrefix, testCases, "Generic Array Types with broader prefix"); + } + + [Fact] + public void GetType_ArrayTypes_WithNonMatchingPrefix() + { + // Array of OuterAssembly type should NOT be found with Test assembly prefix only + // Note: ReflectorNet.Tests does NOT start with "ReflectorNet.Tests.OuterAssembly" + var typeName = "com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass[]"; + + // This should return null because OuterSimpleClass is in OuterAssembly, not in Tests + var result = TypeUtils.GetType("ReflectorNet.Tests.Model", typeName); + + _output.WriteLine($"GetType(\"ReflectorNet.Tests.Model\", \"{typeName}\") = {result?.FullName ?? "null"}"); + Assert.Null(result); + } + + #endregion + + #region Cross-Assembly Generic Tests + + [Fact] + public void GetType_CrossAssemblyGenericTypes() + { + // Generic type from OuterAssembly with type argument from Tests assembly + // This should work with broader prefix that covers both assemblies + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass"] = typeof(OuterGenericClass), + }; + + ValidateGetTypeWithAssembly(ReflectorNetPrefix, testCases, "Cross-Assembly Generic Types"); + } + + #endregion + + #region This Assembly Tests + + [Fact] + public void GetType_ThisAssemblyTypes_WithMatchingPrefix() + { + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.Tests.Model.Vector3"] = typeof(Vector3), + ["com.IvanMurzak.ReflectorNet.Tests.Model.SolarSystem"] = typeof(SolarSystem), + ["com.IvanMurzak.ReflectorNet.Tests.Model.GameObjectRef"] = typeof(GameObjectRef), + ["com.IvanMurzak.ReflectorNet.Tests.Model.SolarSystem+CelestialBody"] = typeof(SolarSystem.CelestialBody), + }; + + ValidateGetTypeWithAssembly(ReflectorNetTestsPrefix, testCases, "This Assembly Types with matching prefix"); + } + + [Fact] + public void GetType_ThisAssemblyArrayTypes_WithMatchingPrefix() + { + var testCases = new Dictionary + { + ["com.IvanMurzak.ReflectorNet.Tests.Model.Vector3[]"] = typeof(Vector3[]), + ["com.IvanMurzak.ReflectorNet.Tests.Model.GameObjectRef[]"] = typeof(GameObjectRef[]), + }; + + ValidateGetTypeWithAssembly(ReflectorNetTestsPrefix, testCases, "This Assembly Array Types"); + } + + #endregion + + #region Filtering Verification Tests + + [Fact] + public void GetType_VerifyAssemblyFiltering_TypeNotFoundInWrongAssembly() + { + // Verify that types are actually filtered by assembly + // Vector3 is in ReflectorNet.Tests, so it should NOT be found with OuterAssembly prefix + var vectorTypeName = "com.IvanMurzak.ReflectorNet.Tests.Model.Vector3"; + + var withCorrectPrefix = TypeUtils.GetType(ReflectorNetTestsPrefix, vectorTypeName); + var withWrongPrefix = TypeUtils.GetType(ReflectorNetOuterPrefix, vectorTypeName); + + _output.WriteLine($"With correct prefix '{ReflectorNetTestsPrefix}': {withCorrectPrefix?.FullName ?? "null"}"); + _output.WriteLine($"With wrong prefix '{ReflectorNetOuterPrefix}': {withWrongPrefix?.FullName ?? "null"}"); + + Assert.NotNull(withCorrectPrefix); + Assert.Equal(typeof(Vector3), withCorrectPrefix); + Assert.Null(withWrongPrefix); + } + + [Fact] + public void GetType_VerifyAssemblyFiltering_BothDirectionsFiltered() + { + // OuterSimpleClass is in OuterAssembly + // Vector3 is in Tests + // Each should only be found with the correct prefix + + var outerType = "com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass"; + var testType = "com.IvanMurzak.ReflectorNet.Tests.Model.Vector3"; + + // OuterSimpleClass with OuterAssembly prefix - should work + Assert.NotNull(TypeUtils.GetType(ReflectorNetOuterPrefix, outerType)); + // OuterSimpleClass with Tests prefix - should fail + Assert.Null(TypeUtils.GetType(ReflectorNetTestsPrefix, outerType)); + + // Vector3 with Tests prefix - should work + Assert.NotNull(TypeUtils.GetType(ReflectorNetTestsPrefix, testType)); + // Vector3 with OuterAssembly prefix - should fail + Assert.Null(TypeUtils.GetType(ReflectorNetOuterPrefix, testType)); + } + + #endregion + + #region Round-Trip Tests + + [Fact] + public void GetType_RoundTrip_GetTypeId_GetTypeWithAssembly() + { + // Verify round-trip: Type -> GetTypeId -> GetType(assemblyPrefix, typeId) -> same Type + var testCases = new (Type type, string assemblyPrefix)[] + { + (typeof(SerializedMember), ReflectorNetPrefix), + (typeof(MethodRef), ReflectorNetPrefix), + (typeof(OuterSimpleClass), ReflectorNetOuterPrefix), + (typeof(OuterGenericClass), ReflectorNetOuterPrefix), + (typeof(Vector3), ReflectorNetTestsPrefix), + (typeof(SolarSystem), ReflectorNetTestsPrefix), + }; + + foreach (var (originalType, prefix) in testCases) + { + var typeId = TypeUtils.GetTypeId(originalType); + var resolvedType = TypeUtils.GetType(prefix, typeId); + + _output.WriteLine($" {originalType} -> \"{typeId}\" -> {resolvedType} (prefix: {prefix})"); + Assert.Equal(originalType, resolvedType); + } + } + + #endregion + + #region Cache Tests + + [Fact] + public void GetType_CacheWorksCorrectly() + { + // Clear cache first + TypeUtils.ClearAssemblyTypeCache(); + + var typeName = "com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass"; + + // First call - should resolve and cache + var result1 = TypeUtils.GetType(ReflectorNetOuterPrefix, typeName); + Assert.NotNull(result1); + + // Second call - should use cache + var result2 = TypeUtils.GetType(ReflectorNetOuterPrefix, typeName); + Assert.NotNull(result2); + + // Results should be the same + Assert.Equal(result1, result2); + + _output.WriteLine($"First call: {result1?.FullName}"); + _output.WriteLine($"Second call (cached): {result2?.FullName}"); + } + + [Fact] + public void GetType_DifferentPrefixesCachedSeparately() + { + // Clear cache first + TypeUtils.ClearAssemblyTypeCache(); + + var typeName = "com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass"; + + // Call with matching prefix - should find + var withMatching = TypeUtils.GetType(ReflectorNetOuterPrefix, typeName); + Assert.NotNull(withMatching); + + // Call with non-matching prefix - should NOT find (and cache the null result) + var withNonMatching = TypeUtils.GetType(ReflectorNetTestsPrefix, typeName); + Assert.Null(withNonMatching); + + // Call with matching prefix again - should still find (from its own cache entry) + var withMatchingAgain = TypeUtils.GetType(ReflectorNetOuterPrefix, typeName); + Assert.NotNull(withMatchingAgain); + + _output.WriteLine($"With matching prefix: {withMatching?.FullName}"); + _output.WriteLine($"With non-matching prefix: {withNonMatching?.FullName ?? "null"}"); + _output.WriteLine($"With matching prefix again: {withMatchingAgain?.FullName}"); + } + + #endregion + + #region Helper Methods + + /// + /// Validates all entries in a dictionary by resolving type names with assembly prefix. + /// + private void ValidateGetTypeWithAssembly(string assemblyPrefix, Dictionary typeMap, string testName) + { + _output.WriteLine($"### Validating {testName} with prefix '{assemblyPrefix}' ({typeMap.Count} entries)\n"); + + var failedTypes = new List<(string typeName, Type? expected, Type? actual)>(); + var passedCount = 0; + + foreach (var kvp in typeMap) + { + var typeName = kvp.Key; + var expectedType = kvp.Value; + var actualType = TypeUtils.GetType(assemblyPrefix, typeName); + + if (actualType == expectedType) + { + passedCount++; + _output.WriteLine($" [PASS] {typeName} -> {actualType?.GetTypeId().ValueOrNull()}"); + } + else + { + failedTypes.Add((typeName, expectedType, actualType)); + _output.WriteLine($" [FAIL] TypeName: {typeName}"); + _output.WriteLine($" Expected: {expectedType?.GetTypeId().ValueOrNull()}"); + _output.WriteLine($" Actual: {actualType?.GetTypeId().ValueOrNull()}"); + } + } + + _output.WriteLine($"\n### Summary: {passedCount}/{typeMap.Count} passed\n"); + + if (failedTypes.Count > 0) + { + var errorMessage = $"Failed {failedTypes.Count} type(s) in {testName}:\n"; + foreach (var (typeName, expected, actual) in failedTypes) + { + errorMessage += $" - TypeName '{typeName}': expected '{expected?.GetTypeId().ValueOrNull()}' but got '{actual?.GetTypeId().ValueOrNull()}'\n"; + } + Assert.Fail(errorMessage); + } + } + + #endregion + } +} From a9983b5fff890fa7a3f9578e2a73940551f561af Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 29 Jan 2026 23:21:55 -0800 Subject: [PATCH 06/17] feat: simplify type resolution by removing assembly prefix from GetType calls --- ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs index 079e129e..294e9474 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs @@ -257,7 +257,7 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) var typeArgs = new Type[typeArgNames.Length]; for (int i = 0; i < typeArgNames.Length; i++) { - var argType = GetType(assemblyPrefix, typeArgNames[i].Trim()); + var argType = GetType(typeArgNames[i].Trim()); if (argType == null) return null; typeArgs[i] = argType; @@ -299,7 +299,7 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) nestedArgs = new Type[argNames.Length]; for (int i = 0; i < argNames.Length; i++) { - var tempType = GetType(assemblyPrefix, argNames[i]?.Trim()); + var tempType = GetType(argNames[i]?.Trim()); if (tempType == null) return null; nestedArgs[i] = tempType; } @@ -569,7 +569,7 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) var argTypeName = currentArg.ToString().Trim(); if (!string.IsNullOrEmpty(argTypeName)) { - var argType = GetType(assemblyPrefix, argTypeName); + var argType = GetType(argTypeName); if (argType == null) return null; args.Add(argType); From d30a6ec89a81e9b07e5ee71592d7c41907642643 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 29 Jan 2026 23:38:24 -0800 Subject: [PATCH 07/17] feat: enhance type resolution tests by adding assertions for correct type retrieval and failure cases --- .../src/TypeUtilsTests/GetTypeWithAssemblyTests.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs index a30c0746..9a624eb0 100644 --- a/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs +++ b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs @@ -315,16 +315,21 @@ public void GetType_VerifyAssemblyFiltering_BothDirectionsFiltered() // Vector3 is in Tests // Each should only be found with the correct prefix + var outerSimpleClassType = typeof(com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass); + var vector3ClassType = typeof(com.IvanMurzak.ReflectorNet.Tests.Model.Vector3); + var outerType = "com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass"; var testType = "com.IvanMurzak.ReflectorNet.Tests.Model.Vector3"; // OuterSimpleClass with OuterAssembly prefix - should work Assert.NotNull(TypeUtils.GetType(ReflectorNetOuterPrefix, outerType)); - // OuterSimpleClass with Tests prefix - should fail - Assert.Null(TypeUtils.GetType(ReflectorNetTestsPrefix, outerType)); + Assert.Equal(outerSimpleClassType, TypeUtils.GetType(ReflectorNetOuterPrefix, outerType)); + // OuterSimpleClass with System prefix - should fail + Assert.Null(TypeUtils.GetType(SystemPrefix, outerType)); // Vector3 with Tests prefix - should work Assert.NotNull(TypeUtils.GetType(ReflectorNetTestsPrefix, testType)); + Assert.Equal(vector3ClassType, TypeUtils.GetType(ReflectorNetTestsPrefix, testType)); // Vector3 with OuterAssembly prefix - should fail Assert.Null(TypeUtils.GetType(ReflectorNetOuterPrefix, testType)); } @@ -397,7 +402,7 @@ public void GetType_DifferentPrefixesCachedSeparately() Assert.NotNull(withMatching); // Call with non-matching prefix - should NOT find (and cache the null result) - var withNonMatching = TypeUtils.GetType(ReflectorNetTestsPrefix, typeName); + var withNonMatching = TypeUtils.GetType(SystemPrefix, typeName); Assert.Null(withNonMatching); // Call with matching prefix again - should still find (from its own cache entry) From 3a96ab28c2b8007cdc7adf276185b04cf97eaaf2 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 00:25:32 -0800 Subject: [PATCH 08/17] feat: enhance type resolution by adding exact assembly lookup and refactoring related methods --- .../ReflectorTests/IsTypeBlacklistedTests.cs | 3 +- .../GetTypeWithAssemblyTests.cs | 4 +- .../GetTypeWithExactAssemblyTests.cs | 176 ++++++++++++++ .../src/Reflector/Reflector.Registry.cs | 23 +- ReflectorNet/src/Utils/TypeUtils.Cache.cs | 14 ++ .../src/Utils/TypeUtils.GetType.Private.cs | 221 ++++++++++++++++++ ReflectorNet/src/Utils/TypeUtils.GetType.cs | 71 +++++- 7 files changed, 484 insertions(+), 28 deletions(-) create mode 100644 ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithExactAssemblyTests.cs diff --git a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs index eee42744..f7be3fef 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using com.IvanMurzak.ReflectorNet.Utils; using Xunit; using Xunit.Abstractions; @@ -1456,7 +1457,7 @@ public void IsTypeBlacklisted_CacheSizeLimit_ClearsWhenExceeded() reflector.Converters.BlacklistType(typeof(BlacklistedBaseClass)); // Get all types from the current assembly to fill the cache - var types = typeof(IsTypeBlacklistedTests).Assembly.GetTypes() + var types = AssemblyUtils.GetAssemblyTypes(typeof(IsTypeBlacklistedTests).Assembly) .Where(t => t != null) .Take(1100) // More than MaxBlacklistCacheSize (1000) .ToList(); diff --git a/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs index 9a624eb0..1c8eb720 100644 --- a/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs +++ b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs @@ -40,10 +40,10 @@ public GetTypeWithAssemblyTests(ITestOutputHelper output) : base(output) { } public void GetType_NullAssemblyName_DelegatesToStandardGetType() { // When assemblyName is null, should delegate to GetType(typeName) - var type = TypeUtils.GetType(null, "System.Int32"); + var type = TypeUtils.GetType((string?)null, "System.Int32"); Assert.Equal(typeof(int), type); - var reflectorType = TypeUtils.GetType(null, "com.IvanMurzak.ReflectorNet.Model.SerializedMember"); + var reflectorType = TypeUtils.GetType((string?)null, "com.IvanMurzak.ReflectorNet.Model.SerializedMember"); Assert.Equal(typeof(SerializedMember), reflectorType); } diff --git a/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithExactAssemblyTests.cs b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithExactAssemblyTests.cs new file mode 100644 index 00000000..16eaab83 --- /dev/null +++ b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithExactAssemblyTests.cs @@ -0,0 +1,176 @@ +/* + * 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.Generic; +using System.Reflection; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; +using com.IvanMurzak.ReflectorNet.Tests.Model; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.TypeUtilsTests +{ + /// + /// Tests for TypeUtils.GetType(Assembly assembly, string? typeName) method. + /// Verifies that types are correctly resolved when searching within a specific assembly. + /// + public class GetTypeWithExactAssemblyTests : BaseTest + { + public GetTypeWithExactAssemblyTests(ITestOutputHelper output) : base(output) { } + + private readonly Assembly _reflectorNetAssembly = typeof(SerializedMember).Assembly; + private readonly Assembly _testsAssembly = typeof(GetTypeWithExactAssemblyTests).Assembly; + private readonly Assembly _outerAssembly = typeof(ParentClass).Assembly; + private readonly Assembly _systemAssembly = typeof(int).Assembly; + + #region Edge Case Tests + + [Fact] + public void GetType_NullAssembly_ReturnsNull() + { + var type = TypeUtils.GetType((Assembly)null!, "System.Int32"); + Assert.Null(type); + } + + [Fact] + public void GetType_NullTypeName_ReturnsNull() + { + var type = TypeUtils.GetType(_systemAssembly, null); + Assert.Null(type); + } + + [Fact] + public void GetType_EmptyTypeName_ReturnsNull() + { + var type = TypeUtils.GetType(_systemAssembly, ""); + Assert.Null(type); + + type = TypeUtils.GetType(_systemAssembly, " "); + Assert.Null(type); + } + + [Fact] + public void GetType_ExistingType_InWrongAssembly_ReturnsNull() + { + // System.Int32 exists, but is not in ReflectorNet assembly + var type = TypeUtils.GetType(_reflectorNetAssembly, "System.Int32"); + Assert.Null(type); + } + + #endregion + + #region Simple Type Tests + + [Fact] + public void GetType_SimpleType_InCorrectAssembly_Found() + { + // Test existing type in ReflectorNet assembly + var type = TypeUtils.GetType(_reflectorNetAssembly, typeof(SerializedMember).FullName); + Assert.Equal(typeof(SerializedMember), type); + + // Test existing type in System assembly + var intType = TypeUtils.GetType(_systemAssembly, "System.Int32"); + Assert.Equal(typeof(int), intType); + } + + [Fact] + public void GetType_SimpleType_ByAssemblyQualifiedName_Found() + { + var typeName = typeof(SerializedMember).AssemblyQualifiedName; + var type = TypeUtils.GetType(_reflectorNetAssembly, typeName); + Assert.Equal(typeof(SerializedMember), type); + } + + #endregion + + #region Array Type Tests + + [Fact] + public void GetType_ArrayType_Simple_Found() + { + // SerializedMember[] + var expectedType = typeof(SerializedMember[]); + var typeName = expectedType.FullName; + + var type = TypeUtils.GetType(_reflectorNetAssembly, typeName); + Assert.Equal(expectedType, type); + } + + [Fact] + public void GetType_MultiDimensionalArray_Found() + { + // SerializedMember[,] + var expectedType = typeof(SerializedMember[,]); + var typeName = expectedType.FullName; + + var type = TypeUtils.GetType(_reflectorNetAssembly, typeName); + Assert.Equal(expectedType, type); + } + + #endregion + + #region Generic Type Tests + + [Fact] + public void GetType_GenericType_StandardNotation_Found() + { + // Note: C# notation List is usually resolved by special logic if supported, + // but standard reflection expects `1 notations or assembly qualified ones. + // Our implementation has TryResolveCSharpGenericType support. + + // Testing List + // Since List<> is in System.Private.CoreLib (or similar) but T is in ReflectorNet, + // finding this via "Assembly" based lookup is tricky. + // The method signature is GetType(Assembly assembly, ...) + // If we ask for List on SystemAssembly, it might fail because it can't resolve SerializedMember easily + // unless fully qualified. + + // Let's test a generic type DEFINED in the target assembly. + // We need a generic type in one of our assemblies. + // Let's look for one or generic usage. + // Assuming TypeUtils.Cache.cs uses LruCache which is in ReflectorNet assembly? + // No, LruCache is in Utils namespace. + + var expectedType = typeof(com.IvanMurzak.ReflectorNet.Utils.LruCache); + + // Test C# syntax + var csharpName = "com.IvanMurzak.ReflectorNet.Utils.LruCache"; + var type = TypeUtils.GetType(_reflectorNetAssembly, csharpName); + Assert.Equal(expectedType, type); + } + + [Fact] + public void GetType_GenericType_ReflectionNotation_Found() + { + var expectedType = typeof(com.IvanMurzak.ReflectorNet.Utils.LruCache); + var fullName = expectedType.FullName; // Has `2[[...]] formatting + + var type = TypeUtils.GetType(_reflectorNetAssembly, fullName); + Assert.Equal(expectedType, type); + } + + #endregion + + #region Nested Type Tests + + [Fact] + public void GetType_NestedType_Found() + { + var expectedType = typeof(com.IvanMurzak.ReflectorNet.OuterAssembly.Model.ParentClass.NestedClass); + + // Standard Reflection nested notation: Parent+Child + var standardName = "com.IvanMurzak.ReflectorNet.OuterAssembly.Model.ParentClass+NestedClass"; + var type = TypeUtils.GetType(_outerAssembly, standardName); + Assert.Equal(expectedType, type); + } + + #endregion + } +} diff --git a/ReflectorNet/src/Reflector/Reflector.Registry.cs b/ReflectorNet/src/Reflector/Reflector.Registry.cs index f00a1fb5..d37b9b4b 100644 --- a/ReflectorNet/src/Reflector/Reflector.Registry.cs +++ b/ReflectorNet/src/Reflector/Reflector.Registry.cs @@ -168,33 +168,12 @@ public bool BlacklistType(string assemblyNamePrefix, string typeFullName) if (string.IsNullOrEmpty(assemblyNamePrefix) || string.IsNullOrEmpty(typeFullName)) return false; - var type = FindTypeInAssemblies(assemblyNamePrefix, typeFullName); + var type = TypeUtils.GetType(assemblyNamePrefix, typeFullName); if (type != null) return BlacklistType(type); return false; } - /// - /// Finds a type by its full name in assemblies whose name starts with the specified prefix. - /// - /// The prefix that assembly names must start with. - /// The full name of the type to find. - /// The found type, or null if not found. - private static Type? FindTypeInAssemblies(string assemblyNamePrefix, string typeFullName) - { - foreach (var assembly in AssemblyUtils.GetAssembliesStartingWith(assemblyNamePrefix)) - { - var types = AssemblyUtils.GetAssemblyTypes(assembly); - for (int i = 0; i < types.Length; i++) - { - var type = types[i]; - if (type.FullName == typeFullName) - return type; - } - } - return null; - } - /// /// Finds a type by its full name in a pre-collected list of type arrays from assemblies. /// diff --git a/ReflectorNet/src/Utils/TypeUtils.Cache.cs b/ReflectorNet/src/Utils/TypeUtils.Cache.cs index 42b7e23a..1b679d44 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Cache.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Cache.cs @@ -31,6 +31,10 @@ public static partial class TypeUtils // Key format: "assemblyPrefix|typeName" private static readonly LruCache _assemblyTypeCache = new(TypeCacheCapacity); + // LRU cache for exact assembly type lookups (thread-safe) + // Key format: "assemblyFullName|typeName" + private static readonly LruCache _exactAssemblyTypeCache = new(TypeCacheCapacity); + // LRU cache for enumerable item types to avoid repeated interface/inheritance walks (thread-safe) private static readonly LruCache _enumerableItemTypeCache = new(EnumerableItemTypeCacheCapacity); @@ -63,5 +67,15 @@ public static void ClearAssemblyTypeCache(ILogger? logger = null) _assemblyTypeCache.Count, _assemblyTypeCache.Capacity); _assemblyTypeCache.Clear(); } + + /// + /// Clears the exact assembly type resolution cache. + /// + public static void ClearExactAssemblyTypeCache(ILogger? logger = null) + { + logger?.LogDebug("Clearing exact assembly type resolution cache with {count} entries (capacity: {capacity}).", + _exactAssemblyTypeCache.Count, _exactAssemblyTypeCache.Capacity); + _exactAssemblyTypeCache.Clear(); + } } } diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs index 294e9474..702d612b 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs @@ -1,5 +1,8 @@ using System; using System.Linq; +using System.Reflection; +using System.Text; +using System.Collections.Generic; namespace com.IvanMurzak.ReflectorNet.Utils { @@ -596,5 +599,223 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) return args.Count > 0 ? args.ToArray() : null; } + + private static Type? ResolveSimpleType(Assembly assembly, string name) + { + var type = assembly.GetType(name, throwOnError: false); + if (type != null) + return type; + + try + { + return AssemblyUtils.GetAssemblyTypes(assembly).FirstOrDefault(t => + name == t.AssemblyQualifiedName || + name == t.FullName || + name == t.Name); + } + catch + { + return null; + } + } + + private static Type? TryResolveArrayType(Assembly assembly, string typeName) + { + if (!typeName.EndsWith("]")) + return null; + + var lastOpenBracket = typeName.LastIndexOf('['); + if (lastOpenBracket < 0) + return null; + + var suffix = typeName.Substring(lastOpenBracket); + var content = suffix.Substring(1, suffix.Length - 2); + if (content.Length > 0 && content.Any(c => c != ',')) + return null; + + var commas = content.Length; + var elementTypeName = typeName.Substring(0, lastOpenBracket); + var elementType = GetType(assembly, elementTypeName); + + if (elementType == null) return null; + + return commas == 0 + ? elementType.MakeArrayType() + : elementType.MakeArrayType(commas + 1); + } + + private static Type? TryResolveCSharpGenericType(Assembly assembly, string typeName) + { + var openBracketIndex = typeName.IndexOf('<'); + if (openBracketIndex < 0) + return null; + + var closeBracketIndex = FindMatchingCloseBracket(typeName, openBracketIndex); + if (closeBracketIndex < 0) + return null; + + var baseTypeName = typeName.Substring(0, openBracketIndex); + if (string.IsNullOrWhiteSpace(baseTypeName)) + return null; + + var typeArgsString = typeName.Substring(openBracketIndex + 1, closeBracketIndex - openBracketIndex - 1); + var typeArgNames = ParseCSharpGenericArguments(typeArgsString); + if (typeArgNames == null || typeArgNames.Length == 0) + return null; + + var genericDefName = $"{baseTypeName}`{typeArgNames.Length}"; + var genericDef = ResolveSimpleType(assembly, genericDefName); + if (genericDef == null || !genericDef.IsGenericTypeDefinition) + return null; + + var typeArgs = new Type[typeArgNames.Length]; + for (int i = 0; i < typeArgNames.Length; i++) + { + var argType = GetType(typeArgNames[i].Trim()); + if (argType == null) + return null; + typeArgs[i] = argType; + } + + Type? currentType; + try + { + currentType = genericDef.MakeGenericType(typeArgs); + } + catch + { + return null; + } + + var remaining = typeName.Substring(closeBracketIndex + 1); + while (!string.IsNullOrEmpty(remaining)) + { + if (!remaining.StartsWith("+") && !remaining.StartsWith(".")) + return null; + + remaining = remaining.Substring(1); + + var open = remaining.IndexOf('<'); + string nestedName; + Type[]? nestedArgs = null; + int nextRemainingIndex; + + if (open > 0) + { + var close = FindMatchingCloseBracket(remaining, open); + if (close < 0) return null; + + nestedName = remaining.Substring(0, open); + var argsStr = remaining.Substring(open + 1, close - open - 1); + var argNames = ParseCSharpGenericArguments(argsStr); + if (argNames == null) return null; + + nestedArgs = new Type[argNames.Length]; + for (int i = 0; i < argNames.Length; i++) + { + var tempType = GetType(argNames[i]?.Trim()); + if (tempType == null) return null; + nestedArgs[i] = tempType; + } + + nextRemainingIndex = close + 1; + } + else + { + var nextSep = remaining.IndexOfAny(NestedTypeSeparators); + if (nextSep > 0) + { + nestedName = remaining.Substring(0, nextSep); + nextRemainingIndex = nextSep; + } + else + { + nestedName = remaining; + nextRemainingIndex = remaining.Length; + } + } + + Type? nestedType; + Type[] allArgs; + + if (nestedArgs != null) + { + nestedType = currentType.GetNestedType($"{nestedName}`{nestedArgs.Length}"); + if (nestedType == null) return null; + + if (currentType.IsGenericType && !currentType.IsGenericTypeDefinition) + { + var parentArgs = currentType.GetGenericArguments(); + allArgs = new Type[parentArgs.Length + nestedArgs.Length]; + Array.Copy(parentArgs, allArgs, parentArgs.Length); + Array.Copy(nestedArgs, 0, allArgs, parentArgs.Length, nestedArgs.Length); + } + else + { + allArgs = nestedArgs; + } + } + else + { + nestedType = currentType.GetNestedType(nestedName); + if (nestedType == null) return null; + + allArgs = currentType.IsGenericType && !currentType.IsGenericTypeDefinition + ? currentType.GetGenericArguments() + : Type.EmptyTypes; + } + + if (nestedType.IsGenericTypeDefinition) + { + try + { + currentType = nestedType.GetGenericArguments().Length == allArgs.Length + ? nestedType.MakeGenericType(allArgs) + : nestedType; + } + catch + { + return null; + } + } + else + { + currentType = nestedType; + } + + remaining = remaining.Substring(nextRemainingIndex); + } + + return currentType; + } + + private static Type? TryResolveClassicGenericType(Assembly assembly, string typeName) + { + var backtickIndex = typeName.IndexOf('`'); + if (backtickIndex < 0) + return null; + + var argsStart = typeName.IndexOf("[[", backtickIndex); + if (argsStart < 0) + return null; + + var genericDefName = typeName.Substring(0, argsStart); + var genericDef = ResolveSimpleType(assembly, genericDefName); + if (genericDef == null || !genericDef.IsGenericTypeDefinition) + return null; + + var typeArgs = ParseGenericArguments(typeName, argsStart); + if (typeArgs == null || typeArgs.Length != genericDef.GetGenericArguments().Length) + return null; + + try + { + return genericDef.MakeGenericType(typeArgs); + } + catch + { + return null; + } + } } } diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.cs index a40bf49f..bf9515a3 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reflection; namespace com.IvanMurzak.ReflectorNet.Utils { @@ -65,11 +66,14 @@ public static partial class TypeUtils } /// - /// Retrieves a by its name and assembly name. + /// Retrieves a by its name and (optionally) an assembly name prefix. /// - /// The name of the assembly containing the type. + /// + /// The name, or prefix of the name, of the assembly containing the type. The value is used as a prefix, + /// and the method will match any assembly whose name starts with this value. + /// /// The name of the type to retrieve. - /// The corresponding to the specified name and assembly, or if the type cannot be found. + /// The corresponding to the specified name and assembly name prefix, or if the type cannot be found. public static Type? GetType(string? assemblyName, string? typeName) { if (string.IsNullOrWhiteSpace(typeName)) @@ -129,5 +133,66 @@ public static partial class TypeUtils return type; } + + /// + /// Retrieves a by its name within a specific assembly. + /// + /// The assembly to search in. + /// The name of the type to retrieve. + /// The corresponding to the specified name in the specified assembly, or if the type cannot be found. + public static Type? GetType(Assembly assembly, string? typeName) + { + if (string.IsNullOrWhiteSpace(typeName) || assembly == null) + return null; + + var cacheKey = $"{assembly.GetName().Name}|{typeName}"; + if (_exactAssemblyTypeCache.TryGetValue(cacheKey, out var cachedType)) + return cachedType; + + Type? type = null; + try + { + type = assembly.GetType(typeName, throwOnError: false); + } + catch + { + } + + if (type != null) + { + _exactAssemblyTypeCache[cacheKey] = type; + return type; + } + + type = TryResolveArrayType(assembly, typeName); + if (type != null) + { + _exactAssemblyTypeCache[cacheKey] = type; + return type; + } + + type = TryResolveCSharpGenericType(assembly, typeName); + if (type != null) + { + _exactAssemblyTypeCache[cacheKey] = type; + return type; + } + + type = TryResolveClassicGenericType(assembly, typeName); + if (type != null) + { + _exactAssemblyTypeCache[cacheKey] = type; + return type; + } + + type = AssemblyUtils.GetAssemblyTypes(assembly).FirstOrDefault(t => + typeName == t.FullName || + typeName == t.AssemblyQualifiedName || + typeName == t.GetTypeId()); + + _exactAssemblyTypeCache[cacheKey] = type; + + return type; + } } } From f4d6941fd49b2a07b8da48e407cf37f14ed9ace8 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 00:31:12 -0800 Subject: [PATCH 09/17] refactor: rename BlacklistTypesInAssembly to BlacklistType for consistency --- .../ReflectorTests/IsTypeBlacklistedTests.cs | 30 +++++++++---------- .../src/Reflector/Reflector.Registry.cs | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs index f7be3fef..1e66bb02 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs @@ -1643,7 +1643,7 @@ public void BlacklistType_WithAssemblyPrefix_NullTypeName_ReturnsFalse() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType(assemblyName, null!); + var result = reflector.Converters.BlacklistType(assemblyName, (string)null!); // Assert Assert.False(result, "BlacklistType should return false for null type name"); @@ -1736,7 +1736,7 @@ public void BlacklistTypes_WithAssemblyPrefix_MultipleTypes_AllAreBlacklisted() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( assemblyName, typeof(BlacklistedBaseClass).FullName!, typeof(NonBlacklistedClass).FullName!, @@ -1758,7 +1758,7 @@ public void BlacklistTypes_WithAssemblyPrefix_MixedValidAndInvalid_OnlyValidAdde var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - Mix of valid and invalid type names - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( assemblyName, typeof(BlacklistedBaseClass).FullName!, "This.Does.Not.Exist", @@ -1779,7 +1779,7 @@ public void BlacklistTypes_WithAssemblyPrefix_NonMatchingAssembly_ReturnsFalse() var reflector = new Reflector(); // Act - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( "NonExistentAssembly", typeof(BlacklistedBaseClass).FullName!, typeof(NonBlacklistedClass).FullName!); @@ -1798,7 +1798,7 @@ public void BlacklistTypes_WithAssemblyPrefix_EmptyArray_ReturnsFalse() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistTypesInAssembly(assemblyName, Array.Empty()); + var result = reflector.Converters.BlacklistType(assemblyName, Array.Empty()); // Assert Assert.False(result, "BlacklistTypes should return false when no types are provided"); @@ -1813,7 +1813,7 @@ public void BlacklistTypes_WithAssemblyPrefix_EmptyAssemblyPrefix_ReturnsFalse() var reflector = new Reflector(); // Act - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( "", typeof(BlacklistedBaseClass).FullName!); @@ -1830,7 +1830,7 @@ public void BlacklistTypes_WithAssemblyPrefix_NullAssemblyPrefix_ReturnsFalse() var reflector = new Reflector(); // Act - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( null!, typeof(BlacklistedBaseClass).FullName!); @@ -1849,7 +1849,7 @@ public void BlacklistTypes_WithAssemblyPrefix_DuplicateTypes_AddsOnlyOnce() var typeName = typeof(BlacklistedBaseClass).FullName!; // Act - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( assemblyName, typeName, typeName, @@ -1868,10 +1868,10 @@ public void BlacklistTypes_WithAssemblyPrefix_AlreadyBlacklisted_ReturnsFalse() var reflector = new Reflector(); var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; var typeName = typeof(BlacklistedBaseClass).FullName!; - reflector.Converters.BlacklistTypesInAssembly(assemblyName, typeName); + reflector.Converters.BlacklistType(assemblyName, typeName); // Act - Try to add the same type again - var result = reflector.Converters.BlacklistTypesInAssembly(assemblyName, typeName); + var result = reflector.Converters.BlacklistType(assemblyName, typeName); // Assert Assert.False(result, "BlacklistTypes should return false when all types already blacklisted"); @@ -1886,7 +1886,7 @@ public void BlacklistTypes_WithAssemblyPrefix_WithNullTypeNames_IgnoresNulls() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( assemblyName, typeof(BlacklistedBaseClass).FullName!, null!, @@ -1906,7 +1906,7 @@ public void BlacklistTypes_WithAssemblyPrefix_WithEmptyTypeNames_IgnoresEmpty() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( assemblyName, typeof(BlacklistedBaseClass).FullName!, "", @@ -1928,7 +1928,7 @@ public void BlacklistTypes_WithAssemblyPrefix_PartialAssemblyName_MatchesMultipl var typeName = typeof(BlacklistedBaseClass).FullName!; // Act - var result = reflector.Converters.BlacklistTypesInAssembly(assemblyPrefix, typeName); + var result = reflector.Converters.BlacklistType(assemblyPrefix, typeName); // Assert Assert.True(result, "BlacklistTypes should return true when type is found in matching assembly"); @@ -1947,7 +1947,7 @@ public void BlacklistTypes_WithAssemblyPrefix_CacheInvalidation_WorksCorrectly() Assert.False(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); // Act - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( assemblyName, typeof(BlacklistedBaseClass).FullName!); @@ -1965,7 +1965,7 @@ public void BlacklistTypes_WithAssemblyPrefix_AllInvalid_ReturnsFalse() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - All invalid type names - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistType( assemblyName, "This.Does.Not.Exist", "Neither.Does.This"); diff --git a/ReflectorNet/src/Reflector/Reflector.Registry.cs b/ReflectorNet/src/Reflector/Reflector.Registry.cs index d37b9b4b..5a8dc46d 100644 --- a/ReflectorNet/src/Reflector/Reflector.Registry.cs +++ b/ReflectorNet/src/Reflector/Reflector.Registry.cs @@ -224,7 +224,7 @@ public bool BlacklistTypes(params string[] typeFullNames) /// The prefix that assembly names must start with (e.g., "MyCompany.MyProduct"). /// The full names of the types to blacklist. /// True if at least one type was resolved and added; false if all types could not be resolved or were already blacklisted. - public bool BlacklistTypesInAssembly(string assemblyNamePrefix, params string[] typeFullNames) + public bool BlacklistType(string assemblyNamePrefix, params string[] typeFullNames) { if (string.IsNullOrEmpty(assemblyNamePrefix)) return false; From b53e01994595586ac79d87354926c1d4c1e56306 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 01:16:40 -0800 Subject: [PATCH 10/17] refactor: optimize blacklist handling by removing redundant type search method and enhancing type resolution --- .../src/Reflector/Reflector.Registry.cs | 42 ++++--------------- .../src/Utils/TypeUtils.GetType.Private.cs | 28 ++----------- 2 files changed, 12 insertions(+), 58 deletions(-) diff --git a/ReflectorNet/src/Reflector/Reflector.Registry.cs b/ReflectorNet/src/Reflector/Reflector.Registry.cs index 5a8dc46d..96450cf6 100644 --- a/ReflectorNet/src/Reflector/Reflector.Registry.cs +++ b/ReflectorNet/src/Reflector/Reflector.Registry.cs @@ -174,26 +174,6 @@ public bool BlacklistType(string assemblyNamePrefix, string typeFullName) return false; } - /// - /// Finds a type by its full name in a pre-collected list of type arrays from assemblies. - /// - /// The pre-collected list of type arrays from matching assemblies. - /// The full name of the type to find. - /// The found type, or null if not found. - private static Type? FindTypeInCachedAssemblies(List typesFromAssemblies, string typeFullName) - { - for (int i = 0; i < typesFromAssemblies.Count; i++) - { - var types = typesFromAssemblies[i]; - for (int j = 0; j < types.Length; j++) - { - var type = types[j]; - if (type.FullName == typeFullName) - return type; - } - } - return null; - } /// /// Adds multiple types to the blacklist by their full names, preventing them from being processed by any converter. @@ -229,23 +209,17 @@ public bool BlacklistType(string assemblyNamePrefix, params string[] typeFullNam if (string.IsNullOrEmpty(assemblyNamePrefix)) return false; + var changed = false; + // Collect matching assemblies and their types once - var matchingAssemblies = new List(); foreach (var assembly in AssemblyUtils.GetAssembliesStartingWith(assemblyNamePrefix)) - matchingAssemblies.Add(AssemblyUtils.GetAssemblyTypes(assembly)); - - if (matchingAssemblies.Count == 0) - return false; - - var changed = false; - foreach (var typeFullName in typeFullNames) { - if (string.IsNullOrEmpty(typeFullName)) - continue; - - var type = FindTypeInCachedAssemblies(matchingAssemblies, typeFullName); - if (type != null && _blacklistedTypes.TryAdd(type, 0)) - changed = true; + foreach (var typeFullName in typeFullNames) + { + var type = TypeUtils.GetType(assembly, typeFullName); + if (type != null && _blacklistedTypes.TryAdd(type, 0)) + changed = true; + } } if (changed) _blacklistCache = new ConcurrentDictionary(); // Invalidate cache when blacklist changes diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs index 702d612b..3a11c467 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs @@ -40,30 +40,10 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) private static Type? TryResolveArrayType(string typeName) { - if (!typeName.EndsWith("]")) - return null; - - var lastOpenBracket = typeName.LastIndexOf('['); - if (lastOpenBracket < 0) - return null; - - var suffix = typeName.Substring(lastOpenBracket); - var content = suffix.Substring(1, suffix.Length - 2); - if (content.Length > 0 && content.Any(c => c != ',')) - return null; - - var commas = content.Length; - var elementTypeName = typeName.Substring(0, lastOpenBracket); - var elementType = GetType(elementTypeName); - - if (elementType == null) return null; - - return commas == 0 - ? elementType.MakeArrayType() - : elementType.MakeArrayType(commas + 1); + return TryResolveArrayType((string?)null, typeName); } - private static Type? TryResolveArrayType(string assemblyPrefix, string typeName) + private static Type? TryResolveArrayType(string? assemblyPrefix, string typeName) { if (!typeName.EndsWith("]")) return null; @@ -260,7 +240,7 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) var typeArgs = new Type[typeArgNames.Length]; for (int i = 0; i < typeArgNames.Length; i++) { - var argType = GetType(typeArgNames[i].Trim()); + var argType = GetType(typeArgNames[i].Trim()); // looking for generic type in all assemblies if (argType == null) return null; typeArgs[i] = argType; @@ -302,7 +282,7 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) nestedArgs = new Type[argNames.Length]; for (int i = 0; i < argNames.Length; i++) { - var tempType = GetType(argNames[i]?.Trim()); + var tempType = GetType(argNames[i]?.Trim()); // looking for generic type in all assemblies if (tempType == null) return null; nestedArgs[i] = tempType; } From 70dc179dac205668d451f25f60151a5f3a07422a Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 01:30:52 -0800 Subject: [PATCH 11/17] refactor: streamline CSharp generic type resolution by consolidating methods and improving assembly handling --- .../src/Utils/TypeUtils.GetType.Private.cs | 148 +----------------- 1 file changed, 5 insertions(+), 143 deletions(-) diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs index 3a11c467..86de5866 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs @@ -70,150 +70,10 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) private static Type? TryResolveCSharpGenericType(string typeName) { - var openBracketIndex = typeName.IndexOf('<'); - if (openBracketIndex < 0) - return null; - - var closeBracketIndex = FindMatchingCloseBracket(typeName, openBracketIndex); - if (closeBracketIndex < 0) - return null; - - var baseTypeName = typeName.Substring(0, openBracketIndex); - if (string.IsNullOrWhiteSpace(baseTypeName)) - return null; - - var typeArgsString = typeName.Substring(openBracketIndex + 1, closeBracketIndex - openBracketIndex - 1); - var typeArgNames = ParseCSharpGenericArguments(typeArgsString); - if (typeArgNames == null || typeArgNames.Length == 0) - return null; - - var genericDefName = $"{baseTypeName}`{typeArgNames.Length}"; - var genericDef = ResolveSimpleType(genericDefName); - if (genericDef == null || !genericDef.IsGenericTypeDefinition) - return null; - - var typeArgs = new Type[typeArgNames.Length]; - for (int i = 0; i < typeArgNames.Length; i++) - { - var argType = GetType(typeArgNames[i].Trim()); - if (argType == null) - return null; - typeArgs[i] = argType; - } - - Type? currentType; - try - { - currentType = genericDef.MakeGenericType(typeArgs); - } - catch - { - return null; - } - - var remaining = typeName.Substring(closeBracketIndex + 1); - while (!string.IsNullOrEmpty(remaining)) - { - if (!remaining.StartsWith("+") && !remaining.StartsWith(".")) - return null; - - remaining = remaining.Substring(1); - - var open = remaining.IndexOf('<'); - string nestedName; - Type[]? nestedArgs = null; - int nextRemainingIndex; - - if (open > 0) - { - var close = FindMatchingCloseBracket(remaining, open); - if (close < 0) return null; - - nestedName = remaining.Substring(0, open); - var argsStr = remaining.Substring(open + 1, close - open - 1); - var argNames = ParseCSharpGenericArguments(argsStr); - if (argNames == null) return null; - - nestedArgs = new Type[argNames.Length]; - for (int i = 0; i < argNames.Length; i++) - { - var tempType = GetType(argNames[i]?.Trim()); - if (tempType == null) return null; - nestedArgs[i] = tempType; - } - - nextRemainingIndex = close + 1; - } - else - { - var nextSep = remaining.IndexOfAny(NestedTypeSeparators); - if (nextSep > 0) - { - nestedName = remaining.Substring(0, nextSep); - nextRemainingIndex = nextSep; - } - else - { - nestedName = remaining; - nextRemainingIndex = remaining.Length; - } - } - - Type? nestedType; - Type[] allArgs; - - if (nestedArgs != null) - { - nestedType = currentType.GetNestedType($"{nestedName}`{nestedArgs.Length}"); - if (nestedType == null) return null; - - if (currentType.IsGenericType && !currentType.IsGenericTypeDefinition) - { - var parentArgs = currentType.GetGenericArguments(); - allArgs = new Type[parentArgs.Length + nestedArgs.Length]; - Array.Copy(parentArgs, allArgs, parentArgs.Length); - Array.Copy(nestedArgs, 0, allArgs, parentArgs.Length, nestedArgs.Length); - } - else - { - allArgs = nestedArgs; - } - } - else - { - nestedType = currentType.GetNestedType(nestedName); - if (nestedType == null) return null; - - allArgs = currentType.IsGenericType && !currentType.IsGenericTypeDefinition - ? currentType.GetGenericArguments() - : Type.EmptyTypes; - } - - if (nestedType.IsGenericTypeDefinition) - { - try - { - currentType = nestedType.GetGenericArguments().Length == allArgs.Length - ? nestedType.MakeGenericType(allArgs) - : nestedType; - } - catch - { - return null; - } - } - else - { - currentType = nestedType; - } - - remaining = remaining.Substring(nextRemainingIndex); - } - - return currentType; + return TryResolveCSharpGenericType((string?)null, typeName); } - private static Type? TryResolveCSharpGenericType(string assemblyPrefix, string typeName) + private static Type? TryResolveCSharpGenericType(string? assemblyPrefix, string typeName) { var openBracketIndex = typeName.IndexOf('<'); if (openBracketIndex < 0) @@ -233,7 +93,9 @@ private static bool IsTypeInMatchingAssembly(Type type, string assemblyPrefix) return null; var genericDefName = $"{baseTypeName}`{typeArgNames.Length}"; - var genericDef = ResolveSimpleType(assemblyPrefix, genericDefName); + var genericDef = assemblyPrefix == null + ? ResolveSimpleType(genericDefName) + : ResolveSimpleType(assemblyPrefix, genericDefName); if (genericDef == null || !genericDef.IsGenericTypeDefinition) return null; From d073697323b3730ebbc3890b4504a6e24feacbd3 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 01:43:44 -0800 Subject: [PATCH 12/17] refactor: update TryResolveClassicGenericType to accept assembly prefix and streamline type resolution --- .../src/Utils/TypeUtils.GetType.Private.cs | 88 ++----------------- ReflectorNet/src/Utils/TypeUtils.GetType.cs | 6 +- ReflectorNet/src/Utils/TypeUtils.Helpers.cs | 6 +- 3 files changed, 13 insertions(+), 87 deletions(-) diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs index 86de5866..dcfd94e7 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs @@ -280,7 +280,7 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) return args.Count > 0 ? args.ToArray() : null; } - private static Type? TryResolveClassicGenericType(string typeName) + private static Type? TryResolveClassicGenericType(string? assemblyPrefix, string typeName) { var backtickIndex = typeName.IndexOf('`'); if (backtickIndex < 0) @@ -291,7 +291,9 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) return null; var genericDefName = typeName.Substring(0, argsStart); - var genericDef = ResolveSimpleType(genericDefName); + var genericDef = assemblyPrefix == null + ? ResolveSimpleType(genericDefName) + : ResolveSimpleType(assemblyPrefix, genericDefName); if (genericDef == null || !genericDef.IsGenericTypeDefinition) return null; @@ -309,35 +311,6 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) } } - private static Type? TryResolveClassicGenericType(string assemblyPrefix, string typeName) - { - var backtickIndex = typeName.IndexOf('`'); - if (backtickIndex < 0) - return null; - - var argsStart = typeName.IndexOf("[[", backtickIndex); - if (argsStart < 0) - return null; - - var genericDefName = typeName.Substring(0, argsStart); - var genericDef = ResolveSimpleType(assemblyPrefix, genericDefName); - if (genericDef == null || !genericDef.IsGenericTypeDefinition) - return null; - - var typeArgs = ParseGenericArguments(assemblyPrefix, typeName, argsStart); - if (typeArgs == null || typeArgs.Length != genericDef.GetGenericArguments().Length) - return null; - - try - { - return genericDef.MakeGenericType(typeArgs); - } - catch - { - return null; - } - } - private static Type[]? ParseGenericArguments(string typeName, int startIndex) { var args = new System.Collections.Generic.List(); @@ -380,58 +353,7 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) } else if (c == ',' && depth == 1) { - } - else if (depth > 1) - { - currentArg.Append(c); - } - } - - return args.Count > 0 ? args.ToArray() : null; - } - - private static Type[]? ParseGenericArguments(string assemblyPrefix, string typeName, int startIndex) - { - var args = new System.Collections.Generic.List(); - var depth = 0; - var currentArg = new System.Text.StringBuilder(); - - for (int i = startIndex; i < typeName.Length; i++) - { - var c = typeName[i]; - - if (c == '[') - { - depth++; - if (depth > 2) - currentArg.Append(c); - } - else if (c == ']') - { - depth--; - if (depth == 1) - { - var argTypeName = currentArg.ToString().Trim(); - if (!string.IsNullOrEmpty(argTypeName)) - { - var argType = GetType(argTypeName); - if (argType == null) - return null; - args.Add(argType); - } - currentArg.Clear(); - } - else if (depth > 1) - { - currentArg.Append(c); - } - else if (depth == 0) - { - break; - } - } - else if (c == ',' && depth == 1) - { + // Top-level generic argument separator: intentionally ignored. } else if (depth > 1) { diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.cs index bf9515a3..6c4617d2 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.cs @@ -26,6 +26,7 @@ public static partial class TypeUtils } catch { + // Ignore exceptions from Type.GetType } if (type != null) @@ -48,7 +49,9 @@ public static partial class TypeUtils return type; } - type = TryResolveClassicGenericType(typeName); + type = TryResolveClassicGenericType( + assemblyPrefix: null, + typeName: typeName); if (type != null) { _typeCache[typeName] = type; @@ -95,6 +98,7 @@ public static partial class TypeUtils } catch { + // Ignore exceptions from Type.GetType } if (type != null) diff --git a/ReflectorNet/src/Utils/TypeUtils.Helpers.cs b/ReflectorNet/src/Utils/TypeUtils.Helpers.cs index 1ff5cbfd..69468d96 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Helpers.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Helpers.cs @@ -125,7 +125,7 @@ public static IEnumerable GetGenericTypes(Type type, HashSet? visited { if (fallbackType == null) { - error = $"Type '{member?.typeName}' not found."; + error = $"Type '{member!.typeName}' not found."; return null; } error = null; @@ -145,10 +145,10 @@ public static IEnumerable GetGenericTypes(Type type, HashSet? visited error = $"Type is unknown and {nameof(SerializedMember)}.{nameof(SerializedMember.typeName)} is null or empty."; return null; } - type = GetType(fallbackMember?.typeName); + type = GetType(fallbackMember.typeName); if (type == null) { - error = $"Type '{fallbackMember?.typeName}' not found."; + error = $"Type '{fallbackMember.typeName}' not found."; return null; } error = null; From 25df3d9240a6e4965f8251afd8dae0da3a1b6c4d Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 02:15:46 -0800 Subject: [PATCH 13/17] refactor: enhance documentation for TypeUtils methods to improve clarity and usability --- .../src/Utils/TypeUtils.Collections.cs | 20 +++++++++ .../src/Utils/TypeUtils.Description.cs | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/ReflectorNet/src/Utils/TypeUtils.Collections.cs b/ReflectorNet/src/Utils/TypeUtils.Collections.cs index 7aa59276..568c304e 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Collections.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Collections.cs @@ -6,6 +6,11 @@ namespace com.IvanMurzak.ReflectorNet.Utils { public static partial class TypeUtils { + /// + /// Determines whether the specified type is a generic dictionary (e.g. Dictionary<,> or IDictionary<,>). + /// + /// The type to check. + /// if the type is a generic dictionary; otherwise, . public static bool IsDictionary(Type type) { if (type.IsGenericType && @@ -19,6 +24,11 @@ public static bool IsDictionary(Type type) .Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); } + /// + /// Gets the generic arguments of the dictionary type. + /// + /// The type to check. + /// An array of representing the generic arguments, or if the type is not a generic dictionary. public static Type[]? GetDictionaryGenericArguments(Type type) { if (type.IsGenericType && @@ -34,6 +44,11 @@ public static bool IsDictionary(Type type) return dictionaryInterface?.GetGenericArguments(); } + /// + /// Determines whether the specified type is an enumerable type (array or implements ). + /// + /// The type to check. + /// if the type is enumerable; otherwise, . public static bool IsIEnumerable(Type type) { if (type.IsArray) @@ -46,6 +61,11 @@ public static bool IsIEnumerable(Type type) .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); } + /// + /// Gets the element type of an enumerable type. + /// + /// The enumerable type. + /// The of the elements, or if the type is not enumerable or the element type cannot be determined. public static Type? GetEnumerableItemType(Type type) { return _enumerableItemTypeCache.GetOrAdd(type, GetEnumerableItemTypeInternal); diff --git a/ReflectorNet/src/Utils/TypeUtils.Description.cs b/ReflectorNet/src/Utils/TypeUtils.Description.cs index 55ba8765..b4649a9e 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Description.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Description.cs @@ -11,6 +11,11 @@ namespace com.IvanMurzak.ReflectorNet.Utils { public static partial class TypeUtils { + /// + /// Retrieves the description of the specified . + /// + /// The type to retrieve the description for. + /// The description of the type, or the description of its base type if defined; otherwise, . public static string? GetDescription(Type type) { return type @@ -21,6 +26,11 @@ public static partial class TypeUtils : null); } + /// + /// Retrieves the description of the specified . + /// + /// The parameter info to retrieve the description for. + /// The description of the parameter, or the description of its parameter type if defined; otherwise, . public static string? GetDescription(ParameterInfo? parameterInfo) { return parameterInfo @@ -31,6 +41,11 @@ public static partial class TypeUtils : null); } + /// + /// Retrieves the description of the specified . + /// + /// The member info to retrieve the description for. + /// The description of the member, trying to fall back to field or property specific descriptions logic; otherwise . public static string? GetDescription(MemberInfo? memberInfo) { if (memberInfo == null) @@ -51,6 +66,11 @@ public static partial class TypeUtils }; } + /// + /// Retrieves the description of the specified . + /// + /// The field info to retrieve the description for. + /// The description of the field, or the description of its field type; otherwise, . public static string? GetFieldDescription(FieldInfo? fieldInfo) { if (fieldInfo == null) @@ -64,6 +84,11 @@ public static partial class TypeUtils : null); } + /// + /// Retrieves the description of the specified . + /// + /// The property info to retrieve the description for. + /// The description of the property, or the description of its property type; otherwise, . public static string? GetPropertyDescription(PropertyInfo? propertyInfo) { if (propertyInfo == null) @@ -77,6 +102,12 @@ public static partial class TypeUtils : null); } + /// + /// Retrieves the description of a property specified by its name within a . + /// + /// The type containing the property. + /// The name of the property. + /// The description of the property if found; otherwise, . public static string? GetPropertyDescription(Type type, string propertyName) { var propertyInfo = type.GetProperty(propertyName); @@ -84,6 +115,11 @@ public static partial class TypeUtils } #if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER + /// + /// Retrieves the description of a property from a . + /// + /// The JSON schema exporter context. + /// The description of the property associated with the context; otherwise, . public static string? GetPropertyDescription(JsonSchemaExporterContext context) { if (context.PropertyInfo == null || context.PropertyInfo.DeclaringType == null) @@ -127,6 +163,12 @@ private static string ToPascalCase(string camelCase) return char.ToUpperInvariant(camelCase[0]) + camelCase.Substring(1); } + /// + /// Retrieves the description of a field specified by its name within a . + /// + /// The type containing the field. + /// The name of the field. + /// The description of the field if found; otherwise, . public static string? GetFieldDescription(Type type, string fieldName) { var fieldInfo = type.GetField(fieldName); From 36d30e2e1105be22e62423227293ff43fa880cd1 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 02:25:19 -0800 Subject: [PATCH 14/17] refactor: rename BlacklistType methods to BlacklistTypeInAssembly for clarity and consistency --- .../ReflectorTests/IsTypeBlacklistedTests.cs | 60 +++++++++---------- .../src/Reflector/Reflector.Registry.cs | 11 ++-- ReflectorNet/src/Utils/TypeUtils.GetType.cs | 1 + 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs index 1e66bb02..c0815512 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs @@ -1319,7 +1319,7 @@ public void BlacklistTypes_ByStringNames_AllInvalid_ReturnsFalse() var reflector = new Reflector(); // Act - All invalid type names - var result = reflector.Converters.BlacklistTypes( + var result = reflector.Converters.BlacklistTypesInAssembly( "This.Does.Not.Exist", "Neither.Does.This"); @@ -1334,10 +1334,10 @@ public void BlacklistTypes_ByStringNames_AlreadyBlacklisted_ReturnsFalse() { // Arrange var reflector = new Reflector(); - reflector.Converters.BlacklistTypes("System.String", "System.Int32"); + reflector.Converters.BlacklistTypesInAssembly("System.String", "System.Int32"); // Act - Try to add the same types again - var result = reflector.Converters.BlacklistTypes("System.String", "System.Int32"); + var result = reflector.Converters.BlacklistTypesInAssembly("System.String", "System.Int32"); // Assert Assert.False(result, "BlacklistTypes should return false when all types already blacklisted"); @@ -1528,7 +1528,7 @@ public void BlacklistType_WithAssemblyPrefix_TypeInMatchingAssembly_IsBlackliste var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType(assemblyName, typeFullName); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, typeFullName); // Assert Assert.True(result, "BlacklistType should return true when type is added"); @@ -1544,7 +1544,7 @@ public void BlacklistType_WithAssemblyPrefix_TypeInNonMatchingAssembly_ReturnsFa var typeFullName = typeof(BlacklistedBaseClass).FullName!; // Act - Use a prefix that won't match the assembly containing BlacklistedBaseClass - var result = reflector.Converters.BlacklistType("NonExistentAssembly", typeFullName); + var result = reflector.Converters.BlacklistTypeInAssembly("NonExistentAssembly", typeFullName); // Assert Assert.False(result, "BlacklistType should return false when assembly prefix doesn't match"); @@ -1563,7 +1563,7 @@ public void BlacklistType_WithAssemblyPrefix_PartialAssemblyName_MatchesAssembly var assemblyPrefix = assemblyName.Split('.')[0]; // Act - var result = reflector.Converters.BlacklistType(assemblyPrefix, typeFullName); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyPrefix, typeFullName); // Assert Assert.True(result, "BlacklistType should return true when assembly prefix matches"); @@ -1579,7 +1579,7 @@ public void BlacklistType_WithAssemblyPrefix_InvalidTypeName_ReturnsFalse() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType(assemblyName, "This.Type.Does.Not.Exist"); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, "This.Type.Does.Not.Exist"); // Assert Assert.False(result, "BlacklistType should return false for non-existent type"); @@ -1595,7 +1595,7 @@ public void BlacklistType_WithAssemblyPrefix_EmptyAssemblyPrefix_ReturnsFalse() var typeFullName = typeof(BlacklistedBaseClass).FullName!; // Act - var result = reflector.Converters.BlacklistType("", typeFullName); + var result = reflector.Converters.BlacklistTypeInAssembly("", typeFullName); // Assert Assert.False(result, "BlacklistType should return false for empty assembly prefix"); @@ -1611,7 +1611,7 @@ public void BlacklistType_WithAssemblyPrefix_NullAssemblyPrefix_ReturnsFalse() var typeFullName = typeof(BlacklistedBaseClass).FullName!; // Act - var result = reflector.Converters.BlacklistType(null!, typeFullName); + var result = reflector.Converters.BlacklistTypeInAssembly(null!, typeFullName); // Assert Assert.False(result, "BlacklistType should return false for null assembly prefix"); @@ -1627,7 +1627,7 @@ public void BlacklistType_WithAssemblyPrefix_EmptyTypeName_ReturnsFalse() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType(assemblyName, ""); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, ""); // Assert Assert.False(result, "BlacklistType should return false for empty type name"); @@ -1643,7 +1643,7 @@ public void BlacklistType_WithAssemblyPrefix_NullTypeName_ReturnsFalse() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType(assemblyName, (string)null!); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, (string)null!); // Assert Assert.False(result, "BlacklistType should return false for null type name"); @@ -1658,10 +1658,10 @@ public void BlacklistType_WithAssemblyPrefix_AlreadyBlacklisted_ReturnsFalse() var reflector = new Reflector(); var typeFullName = typeof(BlacklistedBaseClass).FullName!; var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; - reflector.Converters.BlacklistType(assemblyName, typeFullName); + reflector.Converters.BlacklistTypeInAssembly(assemblyName, typeFullName); // Act - Try to add the same type again - var result = reflector.Converters.BlacklistType(assemblyName, typeFullName); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, typeFullName); // Assert Assert.False(result, "BlacklistType should return false when type already blacklisted"); @@ -1677,7 +1677,7 @@ public void BlacklistType_WithAssemblyPrefix_DerivedTypesAreBlacklisted() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType(assemblyName, typeFullName); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, typeFullName); // Assert - Derived types should also be blacklisted Assert.True(result, "BlacklistType should return true when type is added"); @@ -1698,7 +1698,7 @@ public void BlacklistType_WithAssemblyPrefix_CacheInvalidation_WorksCorrectly() Assert.False(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); // Act - var result = reflector.Converters.BlacklistType(assemblyName, typeFullName); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, typeFullName); // Assert - Cache should be invalidated Assert.True(result, "BlacklistType should return true when type is added"); @@ -1716,7 +1716,7 @@ public void BlacklistType_WithAssemblyPrefix_SystemAssembly_CanBlacklistSystemTy // Note: In .NET Core/5+, System.String is in System.Private.CoreLib var stringType = typeof(string); var assemblyName = stringType.Assembly.GetName().Name!; - var result = reflector.Converters.BlacklistType(assemblyName, "System.String"); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, "System.String"); // Assert Assert.True(result, "BlacklistType should return true when type is added"); @@ -1736,7 +1736,7 @@ public void BlacklistTypes_WithAssemblyPrefix_MultipleTypes_AllAreBlacklisted() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypesInAssembly( assemblyName, typeof(BlacklistedBaseClass).FullName!, typeof(NonBlacklistedClass).FullName!, @@ -1758,7 +1758,7 @@ public void BlacklistTypes_WithAssemblyPrefix_MixedValidAndInvalid_OnlyValidAdde var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - Mix of valid and invalid type names - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypesInAssembly( assemblyName, typeof(BlacklistedBaseClass).FullName!, "This.Does.Not.Exist", @@ -1779,7 +1779,7 @@ public void BlacklistTypes_WithAssemblyPrefix_NonMatchingAssembly_ReturnsFalse() var reflector = new Reflector(); // Act - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypesInAssembly( "NonExistentAssembly", typeof(BlacklistedBaseClass).FullName!, typeof(NonBlacklistedClass).FullName!); @@ -1798,7 +1798,7 @@ public void BlacklistTypes_WithAssemblyPrefix_EmptyArray_ReturnsFalse() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType(assemblyName, Array.Empty()); + var result = reflector.Converters.BlacklistTypesInAssembly(assemblyName, Array.Empty()); // Assert Assert.False(result, "BlacklistTypes should return false when no types are provided"); @@ -1813,7 +1813,7 @@ public void BlacklistTypes_WithAssemblyPrefix_EmptyAssemblyPrefix_ReturnsFalse() var reflector = new Reflector(); // Act - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypeInAssembly( "", typeof(BlacklistedBaseClass).FullName!); @@ -1830,7 +1830,7 @@ public void BlacklistTypes_WithAssemblyPrefix_NullAssemblyPrefix_ReturnsFalse() var reflector = new Reflector(); // Act - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypeInAssembly( null!, typeof(BlacklistedBaseClass).FullName!); @@ -1849,7 +1849,7 @@ public void BlacklistTypes_WithAssemblyPrefix_DuplicateTypes_AddsOnlyOnce() var typeName = typeof(BlacklistedBaseClass).FullName!; // Act - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypesInAssembly( assemblyName, typeName, typeName, @@ -1868,10 +1868,10 @@ public void BlacklistTypes_WithAssemblyPrefix_AlreadyBlacklisted_ReturnsFalse() var reflector = new Reflector(); var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; var typeName = typeof(BlacklistedBaseClass).FullName!; - reflector.Converters.BlacklistType(assemblyName, typeName); + reflector.Converters.BlacklistTypeInAssembly(assemblyName, typeName); // Act - Try to add the same type again - var result = reflector.Converters.BlacklistType(assemblyName, typeName); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyName, typeName); // Assert Assert.False(result, "BlacklistTypes should return false when all types already blacklisted"); @@ -1886,7 +1886,7 @@ public void BlacklistTypes_WithAssemblyPrefix_WithNullTypeNames_IgnoresNulls() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypesInAssembly( assemblyName, typeof(BlacklistedBaseClass).FullName!, null!, @@ -1906,7 +1906,7 @@ public void BlacklistTypes_WithAssemblyPrefix_WithEmptyTypeNames_IgnoresEmpty() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypesInAssembly( assemblyName, typeof(BlacklistedBaseClass).FullName!, "", @@ -1928,7 +1928,7 @@ public void BlacklistTypes_WithAssemblyPrefix_PartialAssemblyName_MatchesMultipl var typeName = typeof(BlacklistedBaseClass).FullName!; // Act - var result = reflector.Converters.BlacklistType(assemblyPrefix, typeName); + var result = reflector.Converters.BlacklistTypeInAssembly(assemblyPrefix, typeName); // Assert Assert.True(result, "BlacklistTypes should return true when type is found in matching assembly"); @@ -1947,7 +1947,7 @@ public void BlacklistTypes_WithAssemblyPrefix_CacheInvalidation_WorksCorrectly() Assert.False(reflector.Converters.IsTypeBlacklisted(typeof(BlacklistedBaseClass))); // Act - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypeInAssembly( assemblyName, typeof(BlacklistedBaseClass).FullName!); @@ -1965,7 +1965,7 @@ public void BlacklistTypes_WithAssemblyPrefix_AllInvalid_ReturnsFalse() var assemblyName = typeof(BlacklistedBaseClass).Assembly.GetName().Name!; // Act - All invalid type names - var result = reflector.Converters.BlacklistType( + var result = reflector.Converters.BlacklistTypesInAssembly( assemblyName, "This.Does.Not.Exist", "Neither.Does.This"); diff --git a/ReflectorNet/src/Reflector/Reflector.Registry.cs b/ReflectorNet/src/Reflector/Reflector.Registry.cs index 96450cf6..0b02d726 100644 --- a/ReflectorNet/src/Reflector/Reflector.Registry.cs +++ b/ReflectorNet/src/Reflector/Reflector.Registry.cs @@ -163,7 +163,7 @@ public bool BlacklistType(string typeFullName) /// The prefix that assembly names must start with (e.g., "MyCompany.MyProduct"). /// The full name of the type to blacklist (e.g., "MyCompany.MyProduct.SomeClass"). /// True if the type was resolved and added; false if the type could not be resolved or was already blacklisted. - public bool BlacklistType(string assemblyNamePrefix, string typeFullName) + public bool BlacklistTypeInAssembly(string assemblyNamePrefix, string typeFullName) { if (string.IsNullOrEmpty(assemblyNamePrefix) || string.IsNullOrEmpty(typeFullName)) return false; @@ -204,7 +204,7 @@ public bool BlacklistTypes(params string[] typeFullNames) /// The prefix that assembly names must start with (e.g., "MyCompany.MyProduct"). /// The full names of the types to blacklist. /// True if at least one type was resolved and added; false if all types could not be resolved or were already blacklisted. - public bool BlacklistType(string assemblyNamePrefix, params string[] typeFullNames) + public bool BlacklistTypesInAssembly(string assemblyNamePrefix, params string[] typeFullNames) { if (string.IsNullOrEmpty(assemblyNamePrefix)) return false; @@ -217,8 +217,11 @@ public bool BlacklistType(string assemblyNamePrefix, params string[] typeFullNam foreach (var typeFullName in typeFullNames) { var type = TypeUtils.GetType(assembly, typeFullName); - if (type != null && _blacklistedTypes.TryAdd(type, 0)) - changed = true; + if (type != null) + { + if (_blacklistedTypes.TryAdd(type, 0)) + changed = true; + } } } if (changed) diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.cs index 6c4617d2..2dc8769d 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.cs @@ -160,6 +160,7 @@ public static partial class TypeUtils } catch { + // Ignore exceptions from Assembly.GetType } if (type != null) From 8f03615610afd39e147c6967e01c20310b8c0025 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 02:27:02 -0800 Subject: [PATCH 15/17] refactor: enhance TypeUtils documentation with detailed summaries for methods --- ReflectorNet/src/Utils/TypeUtils.Helpers.cs | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/ReflectorNet/src/Utils/TypeUtils.Helpers.cs b/ReflectorNet/src/Utils/TypeUtils.Helpers.cs index 69468d96..beef510e 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Helpers.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Helpers.cs @@ -6,6 +6,12 @@ namespace com.IvanMurzak.ReflectorNet.Utils { public static partial class TypeUtils { + /// + /// Determine if the given object is assignable to the given type. + /// + /// The object to check. + /// The type to check against. + /// true if the object is assignable to the type; otherwise, false. public static bool IsAssignableTo(object? obj, Type targetType) { if (targetType == null) @@ -17,6 +23,12 @@ public static bool IsAssignableTo(object? obj, Type targetType) return targetType.IsAssignableFrom(obj.GetType()); } + /// + /// Checks if a source type can be cast or converted to a target type. + /// + /// The source type. + /// The target type. + /// true if the cast or conversion is possible; otherwise, false. public static bool IsCastable(Type? type, Type to) { if (type == null || to == null) @@ -37,6 +49,12 @@ public static bool IsCastable(Type? type, Type to) return false; } + /// + /// Calculates the inheritance distance between a base type and a target type. + /// + /// The base type. + /// The target type which should inherit from . + /// The number of inheritance steps, or -1 if the types are not related or does not inherit from . public static int GetInheritanceDistance(Type baseType, Type targetType) { if (!baseType.IsAssignableFrom(targetType)) @@ -52,6 +70,11 @@ public static int GetInheritanceDistance(Type baseType, Type targetType) return current == baseType ? distance : -1; } + /// + /// Checks if a type is considered primitive (including enums, strings, decimals, dates, timespans, and GUIDs). + /// + /// The type to check. + /// true if the type is primitive or one of the supported simple types; otherwise, false. public static bool IsPrimitive(Type type) { return type.IsPrimitive || @@ -64,6 +87,12 @@ public static bool IsPrimitive(Type type) type == typeof(Guid); } + /// + /// Recursively retrieves all generic type arguments from a type and its hierarchy. + /// + /// The type to inspect. + /// Top-level call should pass null. Used internally to prevent infinite recursion. + /// An enumeration of all generic types found in the type and its base classes. public static IEnumerable GetGenericTypes(Type type, HashSet? visited = null) { visited ??= new HashSet(); @@ -99,6 +128,13 @@ public static IEnumerable GetGenericTypes(Type type, HashSet? visited yield return baseGenericType; } + /// + /// Resolves a type based on an object instance, falling back to a specified type if the object is null. + /// + /// The object instance to determine the type from. + /// The type to return if is null. + /// On failure, contains an error message describing why the type could not be resolved. + /// The resolved , or null if resolution fails. public static Type? GetTypeWithObjectPriority(object? obj, Type? fallbackType, out string? error) { var type = obj?.GetType() ?? fallbackType; @@ -112,6 +148,13 @@ public static IEnumerable GetGenericTypes(Type type, HashSet? visited return type; } + /// + /// Resolves a type prioritizing the type name defined in , falling back to . + /// + /// The serialized member info usually containing the type name. + /// The type to return if the member type name is missing or invalid. + /// On failure, contains an error message describing why the type could not be resolved. + /// The resolved , or null if resolution fails. public static Type? GetTypeWithNamePriority(SerializedMember? member, Type? fallbackType, out string? error) { if (StringUtils.IsNullOrEmpty(member?.typeName) && fallbackType == null) @@ -136,6 +179,13 @@ public static IEnumerable GetGenericTypes(Type type, HashSet? visited return type; } + /// + /// Resolves a type prioritizing the provided , falling back to the type definition in . + /// + /// The preferred type. + /// The member to resolve the type from if is null. + /// On failure, contains an error message describing why the type could not be resolved. + /// The resolved , or null if resolution fails. public static Type? GetTypeWithValuePriority(Type? type, SerializedMember? fallbackMember, out string? error) { if (type == null) From d7d128da1413b3a5df645e3d9caedd4ae6ee7f05 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 02:40:50 -0800 Subject: [PATCH 16/17] refactor: rename BlacklistTypesInAssembly to BlacklistTypes for consistency and clarity; streamline type blacklisting logic --- .../ReflectorTests/IsTypeBlacklistedTests.cs | 6 +++--- .../src/Reflector/Reflector.Registry.cs | 7 ++----- ReflectorNet/src/Utils/TypeUtils.GetType.cs | 5 +++++ ReflectorNet/src/Utils/TypeUtils.cs | 17 ----------------- 4 files changed, 10 insertions(+), 25 deletions(-) delete mode 100644 ReflectorNet/src/Utils/TypeUtils.cs diff --git a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs index c0815512..f1cc3ca6 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs @@ -1319,7 +1319,7 @@ public void BlacklistTypes_ByStringNames_AllInvalid_ReturnsFalse() var reflector = new Reflector(); // Act - All invalid type names - var result = reflector.Converters.BlacklistTypesInAssembly( + var result = reflector.Converters.BlacklistTypes( "This.Does.Not.Exist", "Neither.Does.This"); @@ -1334,10 +1334,10 @@ public void BlacklistTypes_ByStringNames_AlreadyBlacklisted_ReturnsFalse() { // Arrange var reflector = new Reflector(); - reflector.Converters.BlacklistTypesInAssembly("System.String", "System.Int32"); + reflector.Converters.BlacklistTypes("System.String", "System.Int32"); // Act - Try to add the same types again - var result = reflector.Converters.BlacklistTypesInAssembly("System.String", "System.Int32"); + var result = reflector.Converters.BlacklistTypes("System.String", "System.Int32"); // Assert Assert.False(result, "BlacklistTypes should return false when all types already blacklisted"); diff --git a/ReflectorNet/src/Reflector/Reflector.Registry.cs b/ReflectorNet/src/Reflector/Reflector.Registry.cs index 0b02d726..2d55b44c 100644 --- a/ReflectorNet/src/Reflector/Reflector.Registry.cs +++ b/ReflectorNet/src/Reflector/Reflector.Registry.cs @@ -217,11 +217,8 @@ public bool BlacklistTypesInAssembly(string assemblyNamePrefix, params string[] foreach (var typeFullName in typeFullNames) { var type = TypeUtils.GetType(assembly, typeFullName); - if (type != null) - { - if (_blacklistedTypes.TryAdd(type, 0)) - changed = true; - } + if (type != null && _blacklistedTypes.TryAdd(type, 0)) + changed = true; } } if (changed) diff --git a/ReflectorNet/src/Utils/TypeUtils.GetType.cs b/ReflectorNet/src/Utils/TypeUtils.GetType.cs index 2dc8769d..84602c44 100644 --- a/ReflectorNet/src/Utils/TypeUtils.GetType.cs +++ b/ReflectorNet/src/Utils/TypeUtils.GetType.cs @@ -77,6 +77,11 @@ public static partial class TypeUtils /// /// The name of the type to retrieve. /// The corresponding to the specified name and assembly name prefix, or if the type cannot be found. + /// + /// Note: For generic types, only the generic type definition is required to be in an assembly whose name matches the + /// specified prefix. Generic type arguments are resolved from all currently loaded assemblies, not only from assemblies + /// whose names start with the provided prefix. + /// public static Type? GetType(string? assemblyName, string? typeName) { if (string.IsNullOrWhiteSpace(typeName)) diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs deleted file mode 100644 index 97a6b018..00000000 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -namespace com.IvanMurzak.ReflectorNet.Utils -{ - // This file remains as a minimal placeholder. Functionality has been split - // into smaller partial files: TypeUtils.Cache.cs, TypeUtils.Resolve.cs, - // TypeUtils.Collections.cs, TypeUtils.Description.cs, TypeUtils.Helpers.cs, - // and TypeUtils.Name.cs. - public static partial class TypeUtils - { - } -} \ No newline at end of file From d716d47a6d09fc8d72d300003fbd137140272480 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 30 Jan 2026 18:32:18 -0800 Subject: [PATCH 17/17] test: add unit tests for BlacklistTypesInAssembly method to verify type blacklisting functionality --- .../BlacklistTypesInAssemblyTests.cs | 309 ++++++++++++++++++ ReflectorNet/ReflectorNet.csproj | 2 +- 2 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 ReflectorNet.Tests/src/ReflectorTests/BlacklistTypesInAssemblyTests.cs diff --git a/ReflectorNet.Tests/src/ReflectorTests/BlacklistTypesInAssemblyTests.cs b/ReflectorNet.Tests/src/ReflectorTests/BlacklistTypesInAssemblyTests.cs new file mode 100644 index 00000000..c30f25ed --- /dev/null +++ b/ReflectorNet.Tests/src/ReflectorTests/BlacklistTypesInAssemblyTests.cs @@ -0,0 +1,309 @@ +/* + * 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.Generic; +using System.Linq; +using com.IvanMurzak.ReflectorNet.Tests.TypeUtilsTests; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.ReflectorTests +{ + /// + /// Tests for Reflector.Registry.BlacklistTypesInAssembly method. + /// Focuses on verifying that complex types are correctly blacklisted when batch-registered via assembly scan. + /// + public class BlacklistTypesInAssemblyTests : BaseTest + { + public BlacklistTypesInAssemblyTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void BlacklistTypesInAssembly_ComplexOuterAssemblyTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // We target the loaded assembly "ReflectorNet.Tests.OuterAssembly" + // Note: The assembly name differs from the namespace prefix "com.IvanMurzak..." + var assemblyPrefix = "ReflectorNet.Tests.OuterAssembly"; + + // Force load the assembly to ensure it's available for scanning + var dummy = typeof(com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass); + + // Collect all type names defined in the OuterAssembly test data + var typesToBlacklist = new HashSet(); + + // Add simple and generic types from OuterAssembly + foreach (var kvp in GetTypeIdTests.OuterAssemblyTypes) + { + typesToBlacklist.Add(kvp.Key); + + // For generic types, also blacklist the open generic definition. + // This ensures that any constructed generic of this type (even with non-blacklisted args) + // is considered blacklisted. + if (kvp.Value.IsGenericType && !kvp.Value.IsGenericTypeDefinition) + { + var genericDef = kvp.Value.GetGenericTypeDefinition(); + // FullName might be null for some types, but should be fine for these class definitions + if (genericDef.FullName != null) + { + typesToBlacklist.Add(genericDef.FullName); + } + } + } + + // Add array types from OuterAssembly + foreach (var typeId in GetTypeIdTests.OuterAssemblyArrayTypes.Keys) + { + typesToBlacklist.Add(typeId); + } + + _output.WriteLine($"Targeting assembly prefix: {assemblyPrefix}"); + _output.WriteLine($"Blacklisting {typesToBlacklist.Count} unique type identifiers via BlacklistTypesInAssembly..."); + + // Act + // Attempt to blacklist all these types by finding them in OuterAssembly + var changed = registry.BlacklistTypesInAssembly(assemblyPrefix, typesToBlacklist.ToArray()); + + Assert.True(changed, "BlacklistTypesInAssembly should return true as types should have been added."); + + // Assert + + // 1. Verify the types we explicitly asked to blacklist are indeed blacklisted + VerifySetIsBlacklisted(registry, GetTypeIdTests.OuterAssemblyTypes, "OuterAssemblyTypes (Direct Targets)"); + VerifySetIsBlacklisted(registry, GetTypeIdTests.OuterAssemblyArrayTypes, "OuterAssemblyArrayTypes (Direct Targets)"); + + // 2. Verify derived complex types are also blacklisted. + VerifySetIsBlacklisted(registry, GetTypeIdTests.ComplexCombinedTypes, "ComplexCombinedTypes (Implicitly Blacklisted)"); + } + + [Fact] + public void BlacklistTypesInAssembly_BuiltInTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.BuiltInTypes, "BuiltInTypes"); + } + + [Fact] + public void BlacklistTypesInAssembly_BuiltInArrayTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.BuiltInArrayTypes, "BuiltInArrayTypes"); + } + + [Fact] + public void BlacklistTypesInAssembly_BuiltInGenericTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.BuiltInGenericTypes, "BuiltInGenericTypes"); + } + + [Fact] + public void BlacklistTypesInAssembly_NestedGenericTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.NestedGenericTypes, "NestedGenericTypes"); + } + + [Fact] + public void BlacklistTypesInAssembly_ThisAssemblyTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.ThisAssemblyTypes, "ThisAssemblyTypes"); + } + + [Fact] + public void BlacklistTypesInAssembly_ReflectorNetTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.ReflectorNetTypes, "ReflectorNetTypes"); + } + + [Fact] + public void BlacklistTypesInAssembly_OuterAssemblyTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Force load the assembly to ensure it's available for scanning + var dummy = typeof(com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass); + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.OuterAssemblyTypes, "OuterAssemblyTypes"); + } + + [Fact] + public void BlacklistTypesInAssembly_OuterAssemblyArrayTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Force load the assembly to ensure it's available for scanning + var dummy = typeof(com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass); + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.OuterAssemblyArrayTypes, "OuterAssemblyArrayTypes"); + } + + [Fact] + public void BlacklistTypesInAssembly_ComplexCombinedTypes_AreBlacklisted() + { + // Arrange + var reflector = new Reflector(); + var registry = reflector.Converters; + + // Force load the assembly to ensure it's available for scanning + var dummy = typeof(com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass); + + // Act & Assert + BlacklistAndVerifyByTypeId(registry, GetTypeIdTests.ComplexCombinedTypes, "ComplexCombinedTypes"); + } + + /// + /// Blacklists types using their TypeUtils.GetTypeId() string representation with exact assembly matching, + /// then verifies they are correctly blacklisted. + /// + private void BlacklistAndVerifyByTypeId(Reflector.Registry registry, Dictionary types, string dictionaryName) + { + _output.WriteLine($"### Testing {dictionaryName} ({types.Count} types)\n"); + + // Group types by their root assembly to blacklist efficiently + var typesByAssembly = new Dictionary>(); + + foreach (var kvp in types) + { + var type = kvp.Value; + var typeId = TypeUtils.GetTypeId(type); + + // Get the assembly name from the type itself + // For arrays, get element type's assembly; for generics, get the generic definition's assembly + var rootType = GetRootType(type); + var assemblyName = rootType.Assembly.GetName().Name; + + if (string.IsNullOrEmpty(assemblyName)) + continue; + + if (!typesByAssembly.TryGetValue(assemblyName, out var list)) + { + list = new List<(string, Type)>(); + typesByAssembly[assemblyName] = list; + } + list.Add((typeId, type)); + } + + // Blacklist types grouped by assembly + var totalBlacklisted = 0; + foreach (var kvp in typesByAssembly) + { + var assemblyName = kvp.Key; + var typeList = kvp.Value; + var typeIds = typeList.Select(t => t.typeId).ToArray(); + + _output.WriteLine($" Assembly: {assemblyName}"); + foreach (var (typeId, type) in typeList) + { + _output.WriteLine($" - {typeId}"); + } + + var changed = registry.BlacklistTypesInAssembly(assemblyName, typeIds); + if (changed) + totalBlacklisted += typeIds.Length; + } + + _output.WriteLine($"\n Blacklisted {totalBlacklisted} types across {typesByAssembly.Count} assemblies.\n"); + + // Verify all types are blacklisted + VerifySetIsBlacklisted(registry, types, dictionaryName); + } + + /// + /// Gets the root type for assembly resolution. + /// For arrays, returns the element type. + /// For generics, returns the generic type definition. + /// + private static Type GetRootType(Type type) + { + // Handle arrays - get the element type + if (type.IsArray) + { + var elementType = type.GetElementType(); + return elementType != null ? GetRootType(elementType) : type; + } + + // Handle constructed generics - get the definition + if (type.IsGenericType && !type.IsGenericTypeDefinition) + { + return type.GetGenericTypeDefinition(); + } + + return type; + } + + private void VerifySetIsBlacklisted(Reflector.Registry registry, Dictionary types, string dictionaryName) + { + _output.WriteLine($"\n### Verifying {dictionaryName} ({types.Count} types)"); + + var failedTypes = new List(); + var passedCount = 0; + + foreach (var kvp in types) + { + var typeId = kvp.Key; + var type = kvp.Value; + + // Primary check: IsTypeBlacklisted + var isBlacklisted = registry.IsTypeBlacklisted(type); + + if (isBlacklisted) + { + passedCount++; + } + else + { + failedTypes.Add(typeId); + _output.WriteLine($" [FAIL] {typeId} is NOT blacklisted."); + } + } + + _output.WriteLine($" [SUMMARY] {passedCount}/{types.Count} passed."); + + if (failedTypes.Count > 0) + { + Assert.Fail($"Failed to blacklist {failedTypes.Count} types in {dictionaryName}. First failure: {failedTypes[0]}"); + } + } + } +} diff --git a/ReflectorNet/ReflectorNet.csproj b/ReflectorNet/ReflectorNet.csproj index 9d54debd..1a60dbcf 100644 --- a/ReflectorNet/ReflectorNet.csproj +++ b/ReflectorNet/ReflectorNet.csproj @@ -9,7 +9,7 @@ com.IvanMurzak.ReflectorNet - 3.11.0 + 3.12.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.