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.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs b/ReflectorNet.Tests/src/ReflectorTests/IsTypeBlacklistedTests.cs
index a4871671..f1cc3ca6 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();
@@ -1515,5 +1516,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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly("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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly("", 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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(assemblyName, (string)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.BlacklistTypeInAssembly(assemblyName, typeFullName);
+
+ // Act - Try to add the same type again
+ var result = reflector.Converters.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(
+ "",
+ 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.BlacklistTypeInAssembly(
+ 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.BlacklistTypeInAssembly(assemblyName, typeName);
+
+ // Act - Try to add the same type again
+ var result = reflector.Converters.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(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.BlacklistTypeInAssembly(
+ 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.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs
new file mode 100644
index 00000000..1c8eb720
--- /dev/null
+++ b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeWithAssemblyTests.cs
@@ -0,0 +1,466 @@
+/*
+ * 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((string?)null, "System.Int32");
+ Assert.Equal(typeof(int), type);
+
+ var reflectorType = TypeUtils.GetType((string?)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 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));
+ 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));
+ }
+
+ #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(SystemPrefix, 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
+ }
+}
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.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/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.
diff --git a/ReflectorNet/src/Reflector/Reflector.Registry.cs b/ReflectorNet/src/Reflector/Reflector.Registry.cs
index 5e8ef54b..2d55b44c 100644
--- a/ReflectorNet/src/Reflector/Reflector.Registry.cs
+++ b/ReflectorNet/src/Reflector/Reflector.Registry.cs
@@ -156,6 +156,25 @@ 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 BlacklistTypeInAssembly(string assemblyNamePrefix, string typeFullName)
+ {
+ if (string.IsNullOrEmpty(assemblyNamePrefix) || string.IsNullOrEmpty(typeFullName))
+ return false;
+
+ var type = TypeUtils.GetType(assemblyNamePrefix, typeFullName);
+ if (type != null)
+ return BlacklistType(type);
+ return false;
+ }
+
+
///
/// 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 +196,36 @@ 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;
+
+ // Collect matching assemblies and their types once
+ foreach (var assembly in AssemblyUtils.GetAssembliesStartingWith(assemblyNamePrefix))
+ {
+ 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
+ return changed;
+ }
+
///
/// Checks if a type is blacklisted. This includes:
/// - The type itself being blacklisted
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.Cache.cs b/ReflectorNet/src/Utils/TypeUtils.Cache.cs
new file mode 100644
index 00000000..1b679d44
--- /dev/null
+++ b/ReflectorNet/src/Utils/TypeUtils.Cache.cs
@@ -0,0 +1,81 @@
+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 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);
+
+ ///
+ /// 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();
+ }
+
+ ///
+ /// 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.Collections.cs b/ReflectorNet/src/Utils/TypeUtils.Collections.cs
new file mode 100644
index 00000000..568c304e
--- /dev/null
+++ b/ReflectorNet/src/Utils/TypeUtils.Collections.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+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 &&
+ (type.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
+ type.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
+ {
+ return true;
+ }
+
+ return type.GetInterfaces()
+ .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 &&
+ (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();
+ }
+
+ ///
+ /// 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)
+ return true;
+
+ 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 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);
+ }
+
+ 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..b4649a9e
--- /dev/null
+++ b/ReflectorNet/src/Utils/TypeUtils.Description.cs
@@ -0,0 +1,178 @@
+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
+ {
+ ///
+ /// 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
+ .GetCustomAttribute(true)
+ ?.Description
+ ?? (type.BaseType != null
+ ? GetDescription(type.BaseType!)
+ : 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
+ ?.GetCustomAttribute(true)
+ ?.Description
+ ?? (parameterInfo != null
+ ? GetDescription(parameterInfo.ParameterType)
+ : 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)
+ 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
+ };
+ }
+
+ ///
+ /// 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)
+ return null;
+
+ return fieldInfo
+ .GetCustomAttribute(true)
+ ?.Description
+ ?? (fieldInfo.FieldType != null
+ ? GetDescription(fieldInfo.FieldType)
+ : 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)
+ return null;
+
+ return propertyInfo
+ .GetCustomAttribute(true)
+ ?.Description
+ ?? (propertyInfo.PropertyType != null
+ ? GetDescription(propertyInfo.PropertyType)
+ : 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);
+ return propertyInfo != null ? GetPropertyDescription(propertyInfo) : null;
+ }
+
+#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)
+ 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);
+ }
+
+ ///
+ /// 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);
+ 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..dcfd94e7
--- /dev/null
+++ b/ReflectorNet/src/Utils/TypeUtils.GetType.Private.cs
@@ -0,0 +1,585 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Collections.Generic;
+
+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)
+ {
+ return TryResolveArrayType((string?)null, typeName);
+ }
+
+ 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)
+ {
+ return TryResolveCSharpGenericType((string?)null, typeName);
+ }
+
+ 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 = assemblyPrefix == null
+ ? ResolveSimpleType(genericDefName)
+ : 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(typeArgNames[i].Trim()); // looking for generic type in all assemblies
+ 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()); // looking for generic type in all assemblies
+ 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? 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 = assemblyPrefix == null
+ ? ResolveSimpleType(genericDefName)
+ : ResolveSimpleType(assemblyPrefix, 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[]? 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)
+ {
+ // Top-level generic argument separator: intentionally ignored.
+ }
+ else if (depth > 1)
+ {
+ currentArg.Append(c);
+ }
+ }
+
+ 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
new file mode 100644
index 00000000..84602c44
--- /dev/null
+++ b/ReflectorNet/src/Utils/TypeUtils.GetType.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Linq;
+using System.Reflection;
+
+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
+ {
+ // Ignore exceptions from Type.GetType
+ }
+
+ 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(
+ assemblyPrefix: null,
+ typeName: 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 (optionally) an assembly name prefix.
+ ///
+ ///
+ /// 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 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))
+ 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
+ {
+ // Ignore exceptions from Type.GetType
+ }
+
+ 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;
+ }
+
+ ///
+ /// 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
+ {
+ // Ignore exceptions from Assembly.GetType
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/ReflectorNet/src/Utils/TypeUtils.Helpers.cs b/ReflectorNet/src/Utils/TypeUtils.Helpers.cs
new file mode 100644
index 00000000..beef510e
--- /dev/null
+++ b/ReflectorNet/src/Utils/TypeUtils.Helpers.cs
@@ -0,0 +1,211 @@
+using System;
+using System.Collections.Generic;
+using com.IvanMurzak.ReflectorNet.Model;
+
+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)
+ return false;
+
+ if (obj == null)
+ return !targetType.IsValueType || Nullable.GetUnderlyingType(targetType) != null;
+
+ 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)
+ 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;
+ }
+
+ ///
+ /// 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))
+ return -1;
+
+ var distance = 0;
+ var current = targetType;
+ while (current != null && current != baseType)
+ {
+ current = current.BaseType;
+ distance++;
+ }
+ 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 ||
+ type.IsEnum ||
+ type == typeof(string) ||
+ type == typeof(decimal) ||
+ type == typeof(DateTime) ||
+ type == typeof(DateTimeOffset) ||
+ type == typeof(TimeSpan) ||
+ 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();
+ 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;
+ }
+
+ ///
+ /// 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;
+ 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 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)
+ {
+ 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 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)
+ {
+ 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
deleted file mode 100644
index ccb99d29..00000000
--- a/ReflectorNet/src/Utils/TypeUtils.cs
+++ /dev/null
@@ -1,1042 +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.
- */
-
-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
-{
- 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;
-
- // LRU cache for resolved type names to avoid repeated AllTypes enumeration (thread-safe)
- private static readonly LruCache _typeCache = 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();
- }
-
- ///
- /// 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;
- }
-
- ///
- /// 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 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 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(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.
- ///
- 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;
- }
- }
-
- ///
- /// 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;
- }
-
- ///
- /// 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;
- }
- }
-}