diff --git a/benchmarks/DEEPCLONE_BENCHMARK_RESULTS.md b/benchmarks/DEEPCLONE_BENCHMARK_RESULTS.md deleted file mode 100644 index a920682c4..000000000 --- a/benchmarks/DEEPCLONE_BENCHMARK_RESULTS.md +++ /dev/null @@ -1,136 +0,0 @@ -# DeepClone Performance Benchmarks - -This benchmark measures the performance of `DeepClone()` across different object types and sizes, representing realistic usage patterns in Foundatio: - -- **Cache entries** (InMemoryCacheClient) - Small to medium objects -- **Queue messages** (InMemoryQueue) - Medium nested objects -- **File storage specs** (InMemoryFileStorage) - FileSpec objects -- **Large event documents** - Error/exception events similar to Exceptionless -- **Log batches** - Bulk log processing scenarios -- **Dynamic types** - Objects with `object` properties containing various runtime types - -## Results Summary - -| Benchmark | Mean | Allocated | Use Case | -|-----------|------|-----------|----------| -| **DeepClone_SmallObject** | 52.93 ns | 184 B | Simple cache entries | -| **DeepClone_FileSpec** | 299.87 ns | 920 B | File storage metadata | -| **DeepClone_SmallObjectWithCollections** | 384.61 ns | 1,096 B | Config/metadata caching | -| **DeepClone_StringArray_1000** | 470.19 ns | 8,160 B | String collections | -| **DeepClone_DynamicWithDictionary** | 797.00 ns | 2,712 B | JSON-like dynamic data | -| **DeepClone_MediumNestedObject** | 1,020.19 ns | 3,416 B | Typical queue messages | -| **DeepClone_DynamicWithNestedObject** | 1,130.58 ns | 3,560 B | Nested dynamic objects | -| **DeepClone_DynamicWithArray** | 3,672.72 ns | 8,800 B | Mixed-type arrays | -| **DeepClone_LargeEventDocument_10MB** | 43,718.94 ns | 129,792 B | Error tracking events | -| **DeepClone_ObjectList_100** | 110,525.77 ns | 318,888 B | Batch processing | -| **DeepClone_ObjectDictionary_50** | 555,214.24 ns | 549,704 B | Keyed collections | -| **DeepClone_LargeLogBatch_10MB** | 3,662,330.45 ns | 3,564,760 B | Bulk log ingestion | - -## Key Findings - -### Performance Characteristics - -1. **Small Objects (~50-400 ns)**: Simple objects with primitives and small collections clone extremely fast -2. **Medium Objects (~1-4 µs)**: Nested objects with multiple collections show linear scaling -3. **Large Objects (~44 µs - 3.7 ms)**: Complex nested structures with many strings scale well - -### Memory Efficiency - -- DeepClone creates new instances of all reference types -- Strings are copied (not interned) to ensure true isolation -- Collections are recreated with the same capacity - -### Dynamic Type Handling - -Objects with `object` properties are handled correctly regardless of runtime type: -- Dictionary → cloned as dictionary -- Nested objects → recursively cloned -- Mixed arrays → each element cloned appropriately - -### Scaling Behavior - -| Object Complexity | Time per KB | Notes | -|-------------------|-------------|-------| -| Simple objects | ~0.3 ns/B | Primitives + small strings | -| Nested objects | ~0.3 ns/B | Consistent with simple objects | -| Large collections | ~1.0 ns/B | Dictionary/List overhead | - -## Recommendations - -### When to Use DeepClone - -1. **Cache isolation**: When cached values must not be modified by callers -2. **Queue message safety**: Prevent mutation of in-flight messages -3. **Test data setup**: Create independent copies for parallel tests - -### Performance Considerations - -1. **Avoid cloning large objects frequently**: For 10MB+ objects, consider immutable designs -2. **Use cloning selectively**: Only clone when mutation isolation is required -3. **Consider object pooling**: For high-frequency cloning of similar objects - -### Alternatives to Consider - -| Scenario | Alternative | Trade-off | -|----------|-------------|-----------| -| Read-only access | Immutable types | No cloning needed | -| Serialization | JSON/MessagePack | Cross-process compatible | -| Partial updates | Copy-on-write | Lazy cloning | - -## Test Environment - -- .NET 10.0.2 (10.0.2, 10.0.225.61305) -- Arm64 RyuJIT armv8.0-a -- macOS Tahoe 26.2 (Darwin 25.2.0) -- Apple M1 Max, 10 logical cores -- BenchmarkDotNet v0.15.8 - -## Raw Results - -``` -BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] -Apple M1 Max, 1 CPU, 10 logical and 10 physical cores -.NET SDK 10.0.102 - [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a - DefaultJob : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a - - -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | -|------------------------------------- |----------------:|--------------:|--------------:|-------:|--------:|---------:|---------:|---------:|----------:|------------:| -| DeepClone_StringArray_1000 | 470.19 ns | 6.227 ns | 5.200 ns | 0.011 | 0.00 | 1.3003 | 0.0367 | - | 8160 B | 0.063 | -| DeepClone_ObjectList_100 | 110,525.77 ns | 844.665 ns | 659.459 ns | 2.528 | 0.02 | 50.5371 | 16.8457 | - | 318888 B | 2.457 | -| DeepClone_ObjectDictionary_50 | 555,214.24 ns | 9,038.564 ns | 8,012.452 ns | 12.700 | 0.19 | 87.8906 | 50.7813 | 19.5313 | 549704 B | 4.235 | -| DeepClone_DynamicWithDictionary | 797.00 ns | 3.333 ns | 2.783 ns | 0.018 | 0.00 | 0.4320 | 0.0029 | - | 2712 B | 0.021 | -| DeepClone_DynamicWithNestedObject | 1,130.58 ns | 16.461 ns | 12.852 ns | 0.026 | 0.00 | 0.5665 | 0.0038 | - | 3560 B | 0.027 | -| DeepClone_DynamicWithArray | 3,672.72 ns | 15.754 ns | 13.966 ns | 0.084 | 0.00 | 1.4000 | 0.0229 | - | 8800 B | 0.068 | -| DeepClone_LargeEventDocument_10MB | 43,718.94 ns | 300.379 ns | 234.516 ns | 1.000 | 0.01 | 20.6299 | 4.0894 | - | 129792 B | 1.000 | -| DeepClone_LargeLogBatch_10MB | 3,662,330.45 ns | 18,117.322 ns | 15,128.785 ns | 83.772 | 0.55 | 562.5000 | 328.1250 | 132.8125 | 3564760 B | 27.465 | -| DeepClone_MediumNestedObject | 1,020.19 ns | 2.533 ns | 2.115 ns | 0.023 | 0.00 | 0.5436 | 0.0038 | - | 3416 B | 0.026 | -| DeepClone_FileSpec | 299.87 ns | 1.618 ns | 1.351 ns | 0.007 | 0.00 | 0.1464 | - | - | 920 B | 0.007 | -| DeepClone_SmallObject | 52.93 ns | 0.366 ns | 0.305 ns | 0.001 | 0.00 | 0.0293 | - | - | 184 B | 0.001 | -| DeepClone_SmallObjectWithCollections | 384.61 ns | 2.238 ns | 1.869 ns | 0.009 | 0.00 | 0.1745 | 0.0005 | - | 1096 B | 0.008 | -``` - -## Benchmark Categories - -### Small Objects (KnownTypes) -- `DeepClone_SmallObject`: Simple POCO with primitives -- `DeepClone_SmallObjectWithCollections`: POCO with List and Dictionary - -### Medium Objects (KnownTypes) -- `DeepClone_MediumNestedObject`: Nested object with user info, request info -- `DeepClone_FileSpec`: File storage metadata object - -### Large Objects (KnownTypes) -- `DeepClone_LargeEventDocument_10MB`: Error tracking event with stack traces -- `DeepClone_LargeLogBatch_10MB`: Batch of ~3000 log entries - -### Dynamic Types -- `DeepClone_DynamicWithDictionary`: Object with Dictionary in object property -- `DeepClone_DynamicWithNestedObject`: Object with nested POCO in object property -- `DeepClone_DynamicWithArray`: Object with mixed-type array in object property - -### Collections -- `DeepClone_StringArray_1000`: Array of 1000 strings -- `DeepClone_ObjectList_100`: List of 100 medium nested objects -- `DeepClone_ObjectDictionary_50`: Dictionary of 50 event documents diff --git a/benchmarks/DeepCloneBenchmarks.cs b/benchmarks/DeepCloneBenchmarks.cs deleted file mode 100644 index 17aa9db7f..000000000 --- a/benchmarks/DeepCloneBenchmarks.cs +++ /dev/null @@ -1,719 +0,0 @@ -using System; -using System.Collections.Generic; -using BenchmarkDotNet.Attributes; -using Foundatio.Utility; - -namespace Foundatio.Benchmarks; - -/// -/// Benchmarks for DeepClone() performance across different object types and sizes. -/// Tests realistic scenarios based on actual Foundatio usage patterns: -/// - Cache entries (InMemoryCacheClient) -/// - Queue messages (InMemoryQueue) -/// - File storage specs (InMemoryFileStorage) -/// - Large nested objects (error/event processing systems) -/// -[MemoryDiagnoser] -[SimpleJob] -[BenchmarkCategory("DeepClone")] -public class DeepCloneBenchmarks -{ - // Small objects - typical cache entries - private SmallObject _smallObject; - private SmallObjectWithCollections _smallObjectWithCollections; - - // Medium objects - typical queue messages - private MediumNestedObject _mediumNestedObject; - private FileSpec _fileSpec; - - // Large objects - ~10MB realistic data - private LargeEventDocument _largeEventDocument; - private LargeLogBatch _largeLogBatch; - - // Dynamic type objects - object properties with various runtime types - private ObjectWithDynamicProperties _dynamicWithDictionary; - private ObjectWithDynamicProperties _dynamicWithNestedObject; - private ObjectWithDynamicProperties _dynamicWithArray; - - // Arrays and collections - private string[] _stringArray; - private List _objectList; - private Dictionary _objectDictionary; - - // Seed for deterministic data generation - private const int Seed = 42; - - [GlobalSetup] - public void Setup() - { - var random = new Random(Seed); - - // Small objects - _smallObject = CreateSmallObject(random); - _smallObjectWithCollections = CreateSmallObjectWithCollections(random); - - // Medium objects - _mediumNestedObject = CreateMediumNestedObject(random); - _fileSpec = CreateFileSpec(random); - - // Large objects (~10MB each) - _largeEventDocument = CreateLargeEventDocument(random, targetSizeBytes: 10 * 1024 * 1024); - _largeLogBatch = CreateLargeLogBatch(random, targetSizeBytes: 10 * 1024 * 1024); - - // Dynamic type objects - _dynamicWithDictionary = CreateDynamicWithDictionary(random); - _dynamicWithNestedObject = CreateDynamicWithNestedObject(random); - _dynamicWithArray = CreateDynamicWithArray(random); - - // Arrays and collections - _stringArray = CreateStringArray(random, 1000); - _objectList = CreateObjectList(random, 100); - _objectDictionary = CreateObjectDictionary(random, 50); - } - - [Benchmark] - [BenchmarkCategory("Small", "KnownTypes")] - public SmallObject DeepClone_SmallObject() - { - return _smallObject.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Small", "KnownTypes")] - public SmallObjectWithCollections DeepClone_SmallObjectWithCollections() - { - return _smallObjectWithCollections.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Medium", "KnownTypes")] - public MediumNestedObject DeepClone_MediumNestedObject() - { - return _mediumNestedObject.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Medium", "KnownTypes")] - public FileSpec DeepClone_FileSpec() - { - return _fileSpec.DeepClone(); - } - - [Benchmark(Baseline = true)] - [BenchmarkCategory("Large", "KnownTypes")] - public LargeEventDocument DeepClone_LargeEventDocument_10MB() - { - return _largeEventDocument.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Large", "KnownTypes")] - public LargeLogBatch DeepClone_LargeLogBatch_10MB() - { - return _largeLogBatch.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Dynamic")] - public ObjectWithDynamicProperties DeepClone_DynamicWithDictionary() - { - return _dynamicWithDictionary.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Dynamic")] - public ObjectWithDynamicProperties DeepClone_DynamicWithNestedObject() - { - return _dynamicWithNestedObject.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Dynamic")] - public ObjectWithDynamicProperties DeepClone_DynamicWithArray() - { - return _dynamicWithArray.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Collections")] - public string[] DeepClone_StringArray_1000() - { - return _stringArray.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Collections")] - public List DeepClone_ObjectList_100() - { - return _objectList.DeepClone(); - } - - [Benchmark] - [BenchmarkCategory("Collections")] - public Dictionary DeepClone_ObjectDictionary_50() - { - return _objectDictionary.DeepClone(); - } - - private static SmallObject CreateSmallObject(Random random) - { - return new SmallObject - { - Id = random.Next(), - Name = GenerateString(random, 50), - CreatedAt = DateTime.UtcNow.AddDays(-random.Next(365)), - IsActive = random.Next(2) == 1, - Score = random.NextDouble() * 100 - }; - } - - private static SmallObjectWithCollections CreateSmallObjectWithCollections(Random random) - { - return new SmallObjectWithCollections - { - Id = random.Next(), - Name = GenerateString(random, 50), - Tags = GenerateStringList(random, 10, 20), - Metadata = GenerateStringDictionary(random, 5, 30) - }; - } - - private static MediumNestedObject CreateMediumNestedObject(Random random) - { - return new MediumNestedObject - { - Id = Guid.NewGuid(), - Type = GenerateString(random, 30), - Source = GenerateString(random, 100), - Message = GenerateString(random, 500), - Timestamp = DateTime.UtcNow.AddMinutes(-random.Next(10000)), - Level = random.Next(1, 6), - Tags = GenerateStringList(random, 5, 15), - Properties = GenerateStringDictionary(random, 10, 50), - User = new UserInfo - { - Id = Guid.NewGuid().ToString(), - Name = GenerateString(random, 30), - Email = $"{GenerateString(random, 10)}@example.com", - Roles = GenerateStringList(random, 3, 20) - }, - Request = new RequestInfo - { - Method = random.Next(4) switch { 0 => "GET", 1 => "POST", 2 => "PUT", _ => "DELETE" }, - Path = $"/api/{GenerateString(random, 20)}/{random.Next(1000)}", - QueryString = GenerateStringDictionary(random, 3, 20), - Headers = GenerateStringDictionary(random, 8, 100), - ClientIp = $"{random.Next(256)}.{random.Next(256)}.{random.Next(256)}.{random.Next(256)}", - UserAgent = GenerateString(random, 150) - } - }; - } - - private static FileSpec CreateFileSpec(Random random) - { - return new FileSpec - { - Path = $"/storage/{GenerateString(random, 20)}/{GenerateString(random, 30)}.dat", - Created = DateTime.UtcNow.AddDays(-random.Next(365)), - Modified = DateTime.UtcNow.AddHours(-random.Next(24)), - Size = random.Next(1000, 10000000), - Data = GenerateStringDictionary(random, 5, 50) - }; - } - - private static LargeEventDocument CreateLargeEventDocument(Random random, int targetSizeBytes) - { - // For 10MB target: Create extended data entries with large strings - // Each char in .NET is 2 bytes, plus string object overhead (~26 bytes) - // We want the cloned object to be ~10MB - - var stackFrameCount = 100; - var extendedDataCount = 200; - // Calculate string length to achieve target size - // Each string entry: ~50KB of chars = 25K chars - var extendedDataStringLength = Math.Max(100, targetSizeBytes / extendedDataCount / 2); - - var stackFrames = new List(stackFrameCount); - for (int i = 0; i < stackFrameCount; i++) - { - stackFrames.Add(new StackFrameInfo - { - FileName = $"/src/{GenerateString(random, 30)}/{GenerateString(random, 40)}.cs", - LineNumber = random.Next(1, 5000), - ColumnNumber = random.Next(1, 200), - MethodName = GenerateString(random, 50), - TypeName = $"{GenerateString(random, 30)}.{GenerateString(random, 40)}", - Namespace = $"Company.{GenerateString(random, 20)}.{GenerateString(random, 20)}", - Parameters = GenerateStringList(random, 5, 30), - LocalVariables = GenerateStringDictionary(random, 3, 50) - }); - } - - var extendedData = new Dictionary(extendedDataCount); - for (int i = 0; i < extendedDataCount; i++) - { - extendedData[$"data_{i}"] = GenerateString(random, extendedDataStringLength); - } - - return new LargeEventDocument - { - Id = Guid.NewGuid().ToString(), - OrganizationId = Guid.NewGuid().ToString(), - ProjectId = Guid.NewGuid().ToString(), - StackId = Guid.NewGuid().ToString(), - Type = "error", - Source = GenerateString(random, 200), - Message = GenerateString(random, 2000), - Date = DateTime.UtcNow.AddMinutes(-random.Next(10000)), - Count = random.Next(1, 1000), - IsFirstOccurrence = random.Next(2) == 1, - IsFixed = random.Next(2) == 1, - IsHidden = random.Next(2) == 1, - Tags = GenerateStringList(random, 20, 30), - Geo = $"{random.NextDouble() * 180 - 90},{random.NextDouble() * 360 - 180}", - Value = random.NextDouble() * 10000, - StackTrace = stackFrames, - ExtendedData = extendedData, - ReferenceIds = GenerateStringList(random, 10, 36), - User = new UserInfo - { - Id = Guid.NewGuid().ToString(), - Name = GenerateString(random, 50), - Email = $"{GenerateString(random, 15)}@example.com", - Roles = GenerateStringList(random, 5, 20) - }, - Request = new RequestInfo - { - Method = "POST", - Path = $"/api/{GenerateString(random, 30)}/{random.Next(10000)}", - QueryString = GenerateStringDictionary(random, 10, 50), - Headers = GenerateStringDictionary(random, 20, 200), - ClientIp = $"{random.Next(256)}.{random.Next(256)}.{random.Next(256)}.{random.Next(256)}", - UserAgent = GenerateString(random, 300) - }, - Environment = new EnvironmentInfo - { - MachineName = GenerateString(random, 30), - ProcessorCount = random.Next(1, 128), - TotalPhysicalMemory = random.NextInt64(1024L * 1024 * 1024, 256L * 1024 * 1024 * 1024), - AvailablePhysicalMemory = random.NextInt64(1024L * 1024 * 1024, 64L * 1024 * 1024 * 1024), - OsName = "Windows 11", - OsVersion = "10.0.22631", - Architecture = "x64", - RuntimeVersion = ".NET 8.0.0", - ProcessName = GenerateString(random, 30), - ProcessId = random.Next(1, 65535), - CommandLine = GenerateString(random, 500), - EnvironmentVariables = GenerateStringDictionary(random, 30, 100) - } - }; - } - - private static LargeLogBatch CreateLargeLogBatch(Random random, int targetSizeBytes) - { - // Each log entry is roughly 2000-5000 bytes - // Target ~10MB total - var entryCount = targetSizeBytes / 3000; - - var entries = new List(entryCount); - for (int i = 0; i < entryCount; i++) - { - entries.Add(new LogEntry - { - Id = Guid.NewGuid(), - Timestamp = DateTime.UtcNow.AddMilliseconds(-random.Next(1000000)), - Level = random.Next(6) switch { 0 => "Trace", 1 => "Debug", 2 => "Info", 3 => "Warn", 4 => "Error", _ => "Fatal" }, - Category = $"{GenerateString(random, 20)}.{GenerateString(random, 30)}", - Message = GenerateString(random, 500), - Exception = random.Next(10) == 0 ? GenerateString(random, 2000) : null, - Properties = GenerateStringDictionary(random, 5, 100), - Scopes = GenerateStringList(random, 3, 50), - TraceId = Guid.NewGuid().ToString("N"), - SpanId = random.NextInt64().ToString("x16"), - ParentSpanId = random.Next(2) == 1 ? random.NextInt64().ToString("x16") : null - }); - } - - return new LargeLogBatch - { - BatchId = Guid.NewGuid(), - Source = GenerateString(random, 100), - CreatedAt = DateTime.UtcNow, - Entries = entries, - Metadata = GenerateStringDictionary(random, 10, 50) - }; - } - - private static ObjectWithDynamicProperties CreateDynamicWithDictionary(Random random) - { - return new ObjectWithDynamicProperties - { - Id = random.Next(), - Name = GenerateString(random, 50), - DynamicData = GenerateStringDictionary(random, 20, 100), - NestedDynamic = new Dictionary - { - ["nested1"] = GenerateStringDictionary(random, 5, 50), - ["nested2"] = GenerateStringList(random, 10, 30), - ["nested3"] = random.NextDouble() - } - }; - } - - private static ObjectWithDynamicProperties CreateDynamicWithNestedObject(Random random) - { - return new ObjectWithDynamicProperties - { - Id = random.Next(), - Name = GenerateString(random, 50), - DynamicData = CreateSmallObject(random), - NestedDynamic = CreateMediumNestedObject(random) - }; - } - - private static ObjectWithDynamicProperties CreateDynamicWithArray(Random random) - { - var array = new object[100]; - for (int i = 0; i < array.Length; i++) - { - int mod = i % 3; - array[i] = mod switch - { - 0 => (object)GenerateString(random, 100), - 1 => (object)random.NextDouble(), - _ => (object)CreateSmallObject(random) - }; - } - - return new ObjectWithDynamicProperties - { - Id = random.Next(), - Name = GenerateString(random, 50), - DynamicData = array, - NestedDynamic = new object[] { CreateSmallObject(random), GenerateStringList(random, 5, 20), random.Next() } - }; - } - - private static string[] CreateStringArray(Random random, int count) - { - var array = new string[count]; - for (int i = 0; i < count; i++) - { - array[i] = GenerateString(random, 50 + random.Next(100)); - } - return array; - } - - private static List CreateObjectList(Random random, int count) - { - var list = new List(count); - for (int i = 0; i < count; i++) - { - list.Add(CreateMediumNestedObject(random)); - } - return list; - } - - private static Dictionary CreateObjectDictionary(Random random, int count) - { - var dict = new Dictionary(count); - for (int i = 0; i < count; i++) - { - // Create medium-sized event documents for the dictionary (~10KB each) - dict[$"event_{i}"] = CreateMediumEventDocument(random); - } - return dict; - } - - private static LargeEventDocument CreateMediumEventDocument(Random random) - { - // Create a medium-sized event document (~10KB) for collection benchmarks - var stackFrames = new List(5); - for (int i = 0; i < 5; i++) - { - stackFrames.Add(new StackFrameInfo - { - FileName = $"/src/{GenerateString(random, 20)}/{GenerateString(random, 20)}.cs", - LineNumber = random.Next(1, 1000), - ColumnNumber = random.Next(1, 100), - MethodName = GenerateString(random, 30), - TypeName = GenerateString(random, 40), - Namespace = GenerateString(random, 30), - Parameters = GenerateStringList(random, 3, 20), - LocalVariables = GenerateStringDictionary(random, 2, 30) - }); - } - - var extendedData = new Dictionary(10); - for (int i = 0; i < 10; i++) - { - extendedData[$"data_{i}"] = GenerateString(random, 200); - } - - return new LargeEventDocument - { - Id = Guid.NewGuid().ToString(), - OrganizationId = Guid.NewGuid().ToString(), - ProjectId = Guid.NewGuid().ToString(), - StackId = Guid.NewGuid().ToString(), - Type = "error", - Source = GenerateString(random, 50), - Message = GenerateString(random, 200), - Date = DateTime.UtcNow.AddMinutes(-random.Next(10000)), - Count = random.Next(1, 100), - IsFirstOccurrence = random.Next(2) == 1, - IsFixed = random.Next(2) == 1, - IsHidden = random.Next(2) == 1, - Tags = GenerateStringList(random, 5, 20), - Geo = $"{random.NextDouble() * 180 - 90},{random.NextDouble() * 360 - 180}", - Value = random.NextDouble() * 1000, - StackTrace = stackFrames, - ExtendedData = extendedData, - ReferenceIds = GenerateStringList(random, 3, 36), - User = new UserInfo - { - Id = Guid.NewGuid().ToString(), - Name = GenerateString(random, 30), - Email = $"{GenerateString(random, 10)}@example.com", - Roles = GenerateStringList(random, 2, 15) - }, - Request = new RequestInfo - { - Method = "POST", - Path = $"/api/{GenerateString(random, 20)}", - QueryString = GenerateStringDictionary(random, 3, 30), - Headers = GenerateStringDictionary(random, 5, 50), - ClientIp = $"{random.Next(256)}.{random.Next(256)}.{random.Next(256)}.{random.Next(256)}", - UserAgent = GenerateString(random, 100) - }, - Environment = new EnvironmentInfo - { - MachineName = GenerateString(random, 20), - ProcessorCount = random.Next(1, 32), - TotalPhysicalMemory = random.NextInt64(1024L * 1024 * 1024, 64L * 1024 * 1024 * 1024), - AvailablePhysicalMemory = random.NextInt64(1024L * 1024 * 1024, 32L * 1024 * 1024 * 1024), - OsName = "Windows 11", - OsVersion = "10.0.22631", - Architecture = "x64", - RuntimeVersion = ".NET 8.0.0", - ProcessName = GenerateString(random, 20), - ProcessId = random.Next(1, 65535), - CommandLine = GenerateString(random, 100), - EnvironmentVariables = GenerateStringDictionary(random, 10, 50) - } - }; - } - - private static string GenerateString(Random random, int length) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 _-"; - var result = new char[length]; - for (int i = 0; i < length; i++) - { - result[i] = chars[random.Next(chars.Length)]; - } - return new string(result); - } - - private static List GenerateStringList(Random random, int count, int stringLength) - { - var list = new List(count); - for (int i = 0; i < count; i++) - { - list.Add(GenerateString(random, stringLength)); - } - return list; - } - - private static Dictionary GenerateStringDictionary(Random random, int count, int valueLength) - { - var dict = new Dictionary(count); - for (int i = 0; i < count; i++) - { - dict[$"key_{i}"] = GenerateString(random, valueLength); - } - return dict; - } -} - -/// -/// Small object representing a typical cache entry. -/// -public class SmallObject -{ - public int Id { get; set; } - public string Name { get; set; } - public DateTime CreatedAt { get; set; } - public bool IsActive { get; set; } - public double Score { get; set; } -} - -/// -/// Small object with collections - typical for configuration or metadata caching. -/// -public class SmallObjectWithCollections -{ - public int Id { get; set; } - public string Name { get; set; } - public List Tags { get; set; } - public Dictionary Metadata { get; set; } -} - -/// -/// Medium-sized nested object representing a typical queue message or event. -/// -public class MediumNestedObject -{ - public Guid Id { get; set; } - public string Type { get; set; } - public string Source { get; set; } - public string Message { get; set; } - public DateTime Timestamp { get; set; } - public int Level { get; set; } - public List Tags { get; set; } - public Dictionary Properties { get; set; } - public UserInfo User { get; set; } - public RequestInfo Request { get; set; } -} - -/// -/// File specification - used in InMemoryFileStorage. -/// -public class FileSpec -{ - public string Path { get; set; } - public DateTime Created { get; set; } - public DateTime Modified { get; set; } - public long Size { get; set; } - public Dictionary Data { get; set; } -} - -/// -/// User information - common nested object in events. -/// -public class UserInfo -{ - public string Id { get; set; } - public string Name { get; set; } - public string Email { get; set; } - public List Roles { get; set; } -} - -/// -/// HTTP request information - common in error tracking systems. -/// -public class RequestInfo -{ - public string Method { get; set; } - public string Path { get; set; } - public Dictionary QueryString { get; set; } - public Dictionary Headers { get; set; } - public string ClientIp { get; set; } - public string UserAgent { get; set; } -} - -/// -/// Stack frame information for error tracking. -/// -public class StackFrameInfo -{ - public string FileName { get; set; } - public int LineNumber { get; set; } - public int ColumnNumber { get; set; } - public string MethodName { get; set; } - public string TypeName { get; set; } - public string Namespace { get; set; } - public List Parameters { get; set; } - public Dictionary LocalVariables { get; set; } -} - -/// -/// Environment information for error tracking. -/// -public class EnvironmentInfo -{ - public string MachineName { get; set; } - public int ProcessorCount { get; set; } - public long TotalPhysicalMemory { get; set; } - public long AvailablePhysicalMemory { get; set; } - public string OsName { get; set; } - public string OsVersion { get; set; } - public string Architecture { get; set; } - public string RuntimeVersion { get; set; } - public string ProcessName { get; set; } - public int ProcessId { get; set; } - public string CommandLine { get; set; } - public Dictionary EnvironmentVariables { get; set; } -} - -/// -/// Large event document - represents error/exception events in systems like Exceptionless. -/// Designed to be ~10MB when fully populated. -/// -public class LargeEventDocument -{ - public string Id { get; set; } - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string StackId { get; set; } - public string Type { get; set; } - public string Source { get; set; } - public string Message { get; set; } - public DateTime Date { get; set; } - public int Count { get; set; } - public bool IsFirstOccurrence { get; set; } - public bool IsFixed { get; set; } - public bool IsHidden { get; set; } - public List Tags { get; set; } - public string Geo { get; set; } - public double Value { get; set; } - public List StackTrace { get; set; } - public Dictionary ExtendedData { get; set; } - public List ReferenceIds { get; set; } - public UserInfo User { get; set; } - public RequestInfo Request { get; set; } - public EnvironmentInfo Environment { get; set; } -} - -/// -/// Log entry for batch logging scenarios. -/// -public class LogEntry -{ - public Guid Id { get; set; } - public DateTime Timestamp { get; set; } - public string Level { get; set; } - public string Category { get; set; } - public string Message { get; set; } - public string Exception { get; set; } - public Dictionary Properties { get; set; } - public List Scopes { get; set; } - public string TraceId { get; set; } - public string SpanId { get; set; } - public string ParentSpanId { get; set; } -} - -/// -/// Large log batch - represents a batch of log entries for bulk processing. -/// Designed to be ~10MB when fully populated. -/// -public class LargeLogBatch -{ - public Guid BatchId { get; set; } - public string Source { get; set; } - public DateTime CreatedAt { get; set; } - public List Entries { get; set; } - public Dictionary Metadata { get; set; } -} - -/// -/// Object with dynamic (object) properties to test cloning of unknown types at compile time. -/// This simulates scenarios where JSON deserialization produces Dictionary<string, object> or JToken. -/// -public class ObjectWithDynamicProperties -{ - public int Id { get; set; } - public string Name { get; set; } - public object DynamicData { get; set; } - public object NestedDynamic { get; set; } -} diff --git a/benchmarks/Foundatio.Benchmarks.csproj b/benchmarks/Foundatio.Benchmarks.csproj index 1413368f4..3f9f8a1f3 100644 --- a/benchmarks/Foundatio.Benchmarks.csproj +++ b/benchmarks/Foundatio.Benchmarks.csproj @@ -11,6 +11,8 @@ true Release false + true + ..\build\Foundatio.snk diff --git a/build/Update-DeepCloner.ps1 b/build/Update-DeepCloner.ps1 deleted file mode 100644 index eddbc71a9..000000000 --- a/build/Update-DeepCloner.ps1 +++ /dev/null @@ -1,103 +0,0 @@ -$work_dir = Resolve-Path "$PSScriptRoot" -$src_dir = Resolve-Path "$PSScriptRoot/../src/Foundatio" - -Function UpdateDeepCloner { - param([string]$version) - - $sourceUrl = "https://github.com/force-net/DeepCloner/archive/refs/tags/v$version.zip" - $name = "DeepCloner" - - $zipPath = Join-Path $work_dir "$name.zip" - $extractPath = Join-Path $work_dir $name - $destPath = Join-Path $src_dir $name - - # Download and extract - If (Test-Path $zipPath) { Remove-Item $zipPath } - Write-Host "Downloading DeepCloner v$version..." - Invoke-WebRequest $sourceUrl -OutFile $zipPath - - If (Test-Path $extractPath) { Remove-Item $extractPath -Recurse -Force } - Expand-Archive -Path $zipPath -DestinationPath $extractPath - Remove-Item $zipPath - - # Clean destination - If (Test-Path $destPath) { Remove-Item $destPath -Recurse -Force } - - $dir = (Get-ChildItem $extractPath | Select-Object -First 1).FullName - - # Create directory structure - New-Item $destPath -Type Directory | Out-Null - New-Item (Join-Path $destPath "Helpers") -Type Directory | Out-Null - - # Copy LICENSE - Copy-Item (Join-Path $dir "LICENSE") -Destination $destPath -Force - - # Files to skip (we don't need MSIL generator for .NET Core, and TypeCreationHelper is only used by MSIL) - $skipFiles = @( - "DeepClonerMsilGenerator.cs", - "DeepClonerMsilHelper.cs", - "TypeCreationHelper.cs" - ) - - # Copy and transform DeepClonerExtensions.cs - $srcDeepClonerPath = Join-Path $dir "DeepCloner" - Get-ChildItem -Path $srcDeepClonerPath -Filter "DeepClonerExtensions.cs" | - Foreach-Object { - $c = ($_ | Get-Content -Raw) - # Add NETCORE define at the top - $c = "#define NETCORE`r`n" + $c - # Transform namespaces - $c = $c -replace 'namespace Force\.DeepCloner;','namespace Foundatio.Force.DeepCloner;' - $c = $c -replace 'namespace Force\.DeepCloner\b','namespace Foundatio.Force.DeepCloner' - $c = $c -replace 'using Force\.DeepCloner\.Helpers;','using Foundatio.Force.DeepCloner.Helpers;' - $c = $c -replace 'using Force\.DeepCloner;','using Foundatio.Force.DeepCloner;' - # Make all public types internal (we only expose via ObjectExtensions.DeepClone) - $c = $c -replace 'public static class','internal static class' - $c = $c -replace 'public class','internal class' - $c = $c -replace 'public enum','internal enum' - $c = $c -replace 'public struct','internal struct' - $c = $c -replace 'public abstract class','internal abstract class' - $c | Set-Content (Join-Path $destPath $_.Name) - Write-Host " Processed: $($_.Name)" - } - - # Copy and transform Helpers/*.cs files - $srcHelpersPath = Join-Path $dir "DeepCloner/Helpers" - $destHelpersPath = Join-Path $destPath "Helpers" - Get-ChildItem -Path $srcHelpersPath -Filter *.cs | - Where-Object { $skipFiles -notcontains $_.Name } | - Foreach-Object { - $c = ($_ | Get-Content -Raw) - # Add NETCORE define at the top - $c = "#define NETCORE`r`n" + $c - # Transform namespaces - $c = $c -replace 'namespace Force\.DeepCloner\.Helpers;','namespace Foundatio.Force.DeepCloner.Helpers;' - $c = $c -replace 'namespace Force\.DeepCloner\.Helpers\b','namespace Foundatio.Force.DeepCloner.Helpers' - $c = $c -replace 'namespace Force\.DeepCloner;','namespace Foundatio.Force.DeepCloner;' - $c = $c -replace 'namespace Force\.DeepCloner\b','namespace Foundatio.Force.DeepCloner' - $c = $c -replace 'using Force\.DeepCloner\.Helpers;','using Foundatio.Force.DeepCloner.Helpers;' - $c = $c -replace 'using Force\.DeepCloner;','using Foundatio.Force.DeepCloner;' - # Make all public types internal (we only expose via ObjectExtensions.DeepClone) - $c = $c -replace 'public static class','internal static class' - $c = $c -replace 'public class','internal class' - $c = $c -replace 'public enum','internal enum' - $c = $c -replace 'public struct','internal struct' - $c = $c -replace 'public abstract class','internal abstract class' - $c | Set-Content (Join-Path $destHelpersPath $_.Name) - Write-Host " Processed: Helpers/$($_.Name)" - } - - # Cleanup - Remove-Item $extractPath -Recurse -Force - - Write-Host "" - Write-Host "DeepCloner v$version updated successfully!" - Write-Host "" - Write-Host "Files skipped (not needed for .NET Core):" - foreach ($skip in $skipFiles) { - Write-Host " - $skip" - } -} - -# Update to latest version (0.10.4 as of 2022-04-29) -UpdateDeepCloner "0.10.4" diff --git a/build/Update-FastCloner.ps1 b/build/Update-FastCloner.ps1 new file mode 100644 index 000000000..8203dde6c --- /dev/null +++ b/build/Update-FastCloner.ps1 @@ -0,0 +1,77 @@ +$work_dir = Resolve-Path "$PSScriptRoot" +$src_dir = Resolve-Path "$PSScriptRoot/../src/Foundatio" + +# Change this to update the FastCloner tag being imported. +$version = "3.5.1" + +Function Invoke-CheckedCommand { + param( + [Parameter(Mandatory = $true)][string]$FilePath, + [Parameter(Mandatory = $true)][string[]]$Arguments, + [Parameter(Mandatory = $false)][string]$WorkingDirectory = $work_dir + ) + + Push-Location $WorkingDirectory + try { + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($Arguments -join ' ')" + } + } + finally { + Pop-Location + } +} + +Function UpdateFastCloner { + param([string]$version = $script:version) + + $name = "FastCloner" + $repoUrl = "https://github.com/lofcz/FastCloner.git" + $tag = "v$version" + $tempRoot = Join-Path $work_dir ".update-fastcloner" + $clonePath = Join-Path $tempRoot "repo" + $stagingPath = Join-Path $tempRoot "output" + $destPath = Join-Path $src_dir $name + $cloneSrcPath = Join-Path $clonePath "src" + $inputRoot = Join-Path $cloneSrcPath $name + $builderProject = "FastCloner.Internalization.Builder/FastCloner.Internalization.Builder.csproj" + + if (Test-Path $tempRoot) { + Remove-Item $tempRoot -Recurse -Force + } + + New-Item $tempRoot -ItemType Directory | Out-Null + + try { + Invoke-CheckedCommand git @("clone", "--branch", $tag, "--depth", "1", $repoUrl, $clonePath) + + Invoke-CheckedCommand dotnet @("build", $builderProject) $cloneSrcPath + Invoke-CheckedCommand dotnet @( + "run", + "--project", $builderProject, + "--", + "--input-root", $inputRoot, + "--root-namespace", "Foundatio.FastCloner", + "--output", $stagingPath, + "--preprocessor", "MODERN=true;NET5_0_OR_GREATER=true;NET6_0_OR_GREATER=true;NET8_0_OR_GREATER=true", + "--visibility", "internal", + "--public-api", "none", + "--runtime-only", "true", + "--self-check" + ) $cloneSrcPath + + if (Test-Path $destPath) { + Remove-Item $destPath -Recurse -Force + } + + Move-Item $stagingPath $destPath + } + finally { + if (Test-Path $tempRoot) { + Remove-Item $tempRoot -Recurse -Force + } + } +} + +UpdateFastCloner $version diff --git a/src/Foundatio/DeepCloner/DeepClonerExtensions.cs b/src/Foundatio/DeepCloner/DeepClonerExtensions.cs deleted file mode 100644 index 3ca0d9877..000000000 --- a/src/Foundatio/DeepCloner/DeepClonerExtensions.cs +++ /dev/null @@ -1,79 +0,0 @@ -#define NETCORE -using System; -using System.Security; - -using Foundatio.Force.DeepCloner.Helpers; - -namespace Foundatio.Force.DeepCloner -{ - /// - /// Extensions for object cloning - /// - internal static class DeepClonerExtensions - { - /// - /// Performs deep (full) copy of object and related graph - /// - public static T DeepClone(this T obj) - { - return DeepClonerGenerator.CloneObject(obj); - } - - /// - /// Performs deep (full) copy of object and related graph to existing object - /// - /// existing filled object - /// Method is valid only for classes, classes should be descendants in reality, not in declaration - public static TTo DeepCloneTo(this TFrom objFrom, TTo objTo) where TTo : class, TFrom - { - return (TTo)DeepClonerGenerator.CloneObjectTo(objFrom, objTo, true); - } - - /// - /// Performs shallow copy of object to existing object - /// - /// existing filled object - /// Method is valid only for classes, classes should be descendants in reality, not in declaration - public static TTo ShallowCloneTo(this TFrom objFrom, TTo objTo) where TTo : class, TFrom - { - return (TTo)DeepClonerGenerator.CloneObjectTo(objFrom, objTo, false); - } - - /// - /// Performs shallow (only new object returned, without cloning of dependencies) copy of object - /// - public static T ShallowClone(this T obj) - { - return ShallowClonerGenerator.CloneObject(obj); - } - - static DeepClonerExtensions() - { - if (!PermissionCheck()) - { - throw new SecurityException("DeepCloner should have enough permissions to run. Grant FullTrust or Reflection permission."); - } - } - - private static bool PermissionCheck() - { - // best way to check required permission: execute something and receive exception - // .net security policy is weird for normal usage - try - { - new object().ShallowClone(); - } - catch (VerificationException) - { - return false; - } - catch (MemberAccessException) - { - return false; - } - - return true; - } - } -} - diff --git a/src/Foundatio/DeepCloner/Helpers/ClonerToExprGenerator.cs b/src/Foundatio/DeepCloner/Helpers/ClonerToExprGenerator.cs deleted file mode 100644 index 9acdbfeea..000000000 --- a/src/Foundatio/DeepCloner/Helpers/ClonerToExprGenerator.cs +++ /dev/null @@ -1,293 +0,0 @@ -#define NETCORE -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - internal static class ClonerToExprGenerator - { - internal static object GenerateClonerInternal(Type realType, bool isDeepClone) - { - if (realType.IsValueType()) - throw new InvalidOperationException("Operation is valid only for reference types"); - return GenerateProcessMethod(realType, isDeepClone); - } - - private static object GenerateProcessMethod(Type type, bool isDeepClone) - { - if (type.IsArray) - { - return GenerateProcessArrayMethod(type, isDeepClone); - } - - var methodType = typeof(object); - - var expressionList = new List(); - - ParameterExpression from = Expression.Parameter(methodType); - var fromLocal = from; - var to = Expression.Parameter(methodType); - var toLocal = to; - var state = Expression.Parameter(typeof(DeepCloneState)); - - // if (!type.IsValueType()) - { - fromLocal = Expression.Variable(type); - toLocal = Expression.Variable(type); - // fromLocal = (T)from - expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); - expressionList.Add(Expression.Assign(toLocal, Expression.Convert(to, type))); - - if (isDeepClone) - { - // added from -> to binding to ensure reference loop handling - // structs cannot loop here - // state.AddKnownRef(from, to) - expressionList.Add(Expression.Call(state, typeof(DeepCloneState).GetMethod("AddKnownRef"), from, to)); - } - } - - List fi = new List(); - var tp = type; - do - { -#if !NETCORE -// don't do anything with this dark magic! - if (tp == typeof(ContextBoundObject)) break; -#else - if (tp.Name == "ContextBoundObject") break; -#endif - - fi.AddRange(tp.GetDeclaredFields()); - tp = tp.BaseType(); - } - while (tp != null); - - foreach (var fieldInfo in fi) - { - if (isDeepClone && !DeepClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType)) - { - var methodInfo = fieldInfo.FieldType.IsValueType() - ? typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneStructInternal") - .MakeGenericMethod(fieldInfo.FieldType) - : typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneClassInternal"); - - var get = Expression.Field(fromLocal, fieldInfo); - - // toLocal.Field = Clone...Internal(fromLocal.Field) - var call = (Expression) Expression.Call(methodInfo, get, state); - if (!fieldInfo.FieldType.IsValueType()) - call = Expression.Convert(call, fieldInfo.FieldType); - - // should handle specially - // todo: think about optimization, but it rare case - if (fieldInfo.IsInitOnly) - { - // var setMethod = fieldInfo.GetType().GetMethod("SetValue", new[] { typeof(object), typeof(object) }); - // expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), setMethod, toLocal, call)); - var setMethod = typeof(DeepClonerExprGenerator).GetPrivateStaticMethod("ForceSetField"); - expressionList.Add(Expression.Call(setMethod, Expression.Constant(fieldInfo), - Expression.Convert(toLocal, typeof(object)), Expression.Convert(call, typeof(object)))); - } - else - { - expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), call)); - } - } - else - { - expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), Expression.Field(fromLocal, fieldInfo))); - } - } - - expressionList.Add(Expression.Convert(toLocal, methodType)); - - var funcType = typeof(Func<,,,>).MakeGenericType(methodType, methodType, typeof(DeepCloneState), methodType); - - var blockParams = new List(); - if (from != fromLocal) blockParams.Add(fromLocal); - if (to != toLocal) blockParams.Add(toLocal); - - return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, to, state).Compile(); - } - - private static object GenerateProcessArrayMethod(Type type, bool isDeep) - { - var elementType = type.GetElementType(); - var rank = type.GetArrayRank(); - - ParameterExpression from = Expression.Parameter(typeof(object)); - ParameterExpression to = Expression.Parameter(typeof(object)); - var state = Expression.Parameter(typeof(DeepCloneState)); - - var funcType = typeof(Func<,,,>).MakeGenericType(typeof(object), typeof(object), typeof(DeepCloneState), typeof(object)); - - if (rank == 1 && type == elementType.MakeArrayType()) - { - if (!isDeep) - { - var callS = Expression.Call( - typeof(ClonerToExprGenerator).GetPrivateStaticMethod("ShallowClone1DimArraySafeInternal") - .MakeGenericMethod(elementType), Expression.Convert(from, type), Expression.Convert(to, type)); - return Expression.Lambda(funcType, callS, from, to, state).Compile(); - } - else - { - var methodName = "Clone1DimArrayClassInternal"; - if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) methodName = "Clone1DimArraySafeInternal"; - else if (elementType.IsValueType()) methodName = "Clone1DimArrayStructInternal"; - var methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod(methodName).MakeGenericMethod(elementType); - var callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state); - return Expression.Lambda(funcType, callS, from, to, state).Compile(); - } - } - else - { - // multidim or not zero-based arrays - MethodInfo methodInfo; - if (rank == 2 && type == elementType.MakeArrayType(2)) - methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod("Clone2DimArrayInternal").MakeGenericMethod(elementType); - else - methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod("CloneAbstractArrayInternal"); - - var callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state, Expression.Constant(isDeep)); - return Expression.Lambda(funcType, callS, from, to, state).Compile(); - } - } - - // when we can't use code generation, we can use these methods - internal static T[] ShallowClone1DimArraySafeInternal(T[] objFrom, T[] objTo) - { - var l = Math.Min(objFrom.Length, objTo.Length); - Array.Copy(objFrom, objTo, l); - return objTo; - } - - // when we can't use code generation, we can use these methods - internal static T[] Clone1DimArraySafeInternal(T[] objFrom, T[] objTo, DeepCloneState state) - { - var l = Math.Min(objFrom.Length, objTo.Length); - state.AddKnownRef(objFrom, objTo); - Array.Copy(objFrom, objTo, l); - return objTo; - } - - internal static T[] Clone1DimArrayStructInternal(T[] objFrom, T[] objTo, DeepCloneState state) - { - // not null from called method, but will check it anyway - if (objFrom == null || objTo == null) return null; - var l = Math.Min(objFrom.Length, objTo.Length); - state.AddKnownRef(objFrom, objTo); - var cloner = DeepClonerGenerator.GetClonerForValueType(); - for (var i = 0; i < l; i++) - objTo[i] = cloner(objTo[i], state); - - return objTo; - } - - internal static T[] Clone1DimArrayClassInternal(T[] objFrom, T[] objTo, DeepCloneState state) - { - // not null from called method, but will check it anyway - if (objFrom == null || objTo == null) return null; - var l = Math.Min(objFrom.Length, objTo.Length); - state.AddKnownRef(objFrom, objTo); - for (var i = 0; i < l; i++) - objTo[i] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i], state); - - return objTo; - } - - internal static T[,] Clone2DimArrayInternal(T[,] objFrom, T[,] objTo, DeepCloneState state, bool isDeep) - { - // not null from called method, but will check it anyway - if (objFrom == null || objTo == null) return null; - if (objFrom.GetLowerBound(0) != 0 || objFrom.GetLowerBound(1) != 0 - || objTo.GetLowerBound(0) != 0 || objTo.GetLowerBound(1) != 0) - return (T[,]) CloneAbstractArrayInternal(objFrom, objTo, state, isDeep); - - var l1 = Math.Min(objFrom.GetLength(0), objTo.GetLength(0)); - var l2 = Math.Min(objFrom.GetLength(1), objTo.GetLength(1)); - state.AddKnownRef(objFrom, objTo); - if ((!isDeep || DeepClonerSafeTypes.CanReturnSameObject(typeof(T))) - && objFrom.GetLength(0) == objTo.GetLength(0) - && objFrom.GetLength(1) == objTo.GetLength(1)) - { - Array.Copy(objFrom, objTo, objFrom.Length); - return objTo; - } - - if (!isDeep) - { - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - objTo[i, k] = objFrom[i, k]; - return objTo; - } - - if (typeof(T).IsValueType()) - { - var cloner = DeepClonerGenerator.GetClonerForValueType(); - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - objTo[i, k] = cloner(objFrom[i, k], state); - } - else - { - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - objTo[i, k] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i, k], state); - } - - return objTo; - } - - // rare cases, very slow cloning. currently it's ok - internal static Array CloneAbstractArrayInternal(Array objFrom, Array objTo, DeepCloneState state, bool isDeep) - { - // not null from called method, but will check it anyway - if (objFrom == null || objTo == null) return null; - var rank = objFrom.Rank; - - if (objTo.Rank != rank) - throw new InvalidOperationException("Invalid rank of target array"); - var lowerBoundsFrom = Enumerable.Range(0, rank).Select(objFrom.GetLowerBound).ToArray(); - var lowerBoundsTo = Enumerable.Range(0, rank).Select(objTo.GetLowerBound).ToArray(); - var lengths = Enumerable.Range(0, rank).Select(x => Math.Min(objFrom.GetLength(x), objTo.GetLength(x))).ToArray(); - var idxesFrom = Enumerable.Range(0, rank).Select(objFrom.GetLowerBound).ToArray(); - var idxesTo = Enumerable.Range(0, rank).Select(objTo.GetLowerBound).ToArray(); - - state.AddKnownRef(objFrom, objTo); - - // unable to copy any element - if (lengths.Any(x => x == 0)) - return objTo; - - while (true) - { - if (isDeep) - objTo.SetValue(DeepClonerGenerator.CloneClassInternal(objFrom.GetValue(idxesFrom), state), idxesTo); - else - objTo.SetValue(objFrom.GetValue(idxesFrom), idxesTo); - var ofs = rank - 1; - while (true) - { - idxesFrom[ofs]++; - idxesTo[ofs]++; - if (idxesFrom[ofs] >= lowerBoundsFrom[ofs] + lengths[ofs]) - { - idxesFrom[ofs] = lowerBoundsFrom[ofs]; - idxesTo[ofs] = lowerBoundsTo[ofs]; - ofs--; - if (ofs < 0) return objTo; - } - else - break; - } - } - } - - } -} diff --git a/src/Foundatio/DeepCloner/Helpers/DeepCloneState.cs b/src/Foundatio/DeepCloner/Helpers/DeepCloneState.cs deleted file mode 100644 index e5689c062..000000000 --- a/src/Foundatio/DeepCloner/Helpers/DeepCloneState.cs +++ /dev/null @@ -1,216 +0,0 @@ -#define NETCORE -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - internal class DeepCloneState - { - private MiniDictionary _loops; - - private readonly object[] _baseFromTo = new object[6]; - - private int _idx; - - public object GetKnownRef(object from) - { - // this is faster than call Dictionary from begin - // also, small poco objects does not have a lot of references - var baseFromTo = _baseFromTo; - if (ReferenceEquals(from, baseFromTo[0])) return baseFromTo[3]; - if (ReferenceEquals(from, baseFromTo[1])) return baseFromTo[4]; - if (ReferenceEquals(from, baseFromTo[2])) return baseFromTo[5]; - if (_loops == null) - return null; - - return _loops.FindEntry(from); - } - - public void AddKnownRef(object from, object to) - { - if (_idx < 3) - { - _baseFromTo[_idx] = from; - _baseFromTo[_idx + 3] = to; - _idx++; - return; - } - - if (_loops == null) - _loops = new MiniDictionary(); - _loops.Insert(from, to); - } - - private class MiniDictionary - { - private struct Entry - { - public int HashCode; - public int Next; - public object Key; - public object Value; - } - - private int[] _buckets; - private Entry[] _entries; - private int _count; - - - public MiniDictionary() : this(5) - { - } - - public MiniDictionary(int capacity) - { - if (capacity > 0) - Initialize(capacity); - } - - public object FindEntry(object key) - { - if (_buckets != null) - { - var hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; - var entries1 = _entries; - for (var i = _buckets[hashCode % _buckets.Length]; i >= 0; i = entries1[i].Next) - { - if (entries1[i].HashCode == hashCode && ReferenceEquals(entries1[i].Key, key)) - return entries1[i].Value; - } - } - - return null; - } - - private static readonly int[] _primes = - { - 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, - 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, - 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, - 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, - 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 - }; - - private static int GetPrime(int min) - { - for (var i = 0; i < _primes.Length; i++) - { - var prime = _primes[i]; - if (prime >= min) return prime; - } - - //outside of our predefined table. - //compute the hard way. - for (var i = min | 1; i < int.MaxValue; i += 2) - { - if (IsPrime(i) && (i - 1) % 101 != 0) - return i; - } - - return min; - } - - private static bool IsPrime(int candidate) - { - if ((candidate & 1) != 0) - { - var limit = (int)Math.Sqrt(candidate); - for (var divisor = 3; divisor <= limit; divisor += 2) - { - if ((candidate % divisor) == 0) - return false; - } - - return true; - } - - return candidate == 2; - } - - private static int ExpandPrime(int oldSize) - { - var newSize = 2 * oldSize; - - if ((uint)newSize > 0x7FEFFFFD && 0x7FEFFFFD > oldSize) - { - return 0x7FEFFFFD; - } - - return GetPrime(newSize); - } - - private void Initialize(int size) - { - _buckets = new int[size]; - for (int i = 0; i < _buckets.Length; i++) - _buckets[i] = -1; - _entries = new Entry[size]; - } - - public void Insert(object key, object value) - { - if (_buckets == null) Initialize(0); - var hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; - var targetBucket = hashCode % _buckets.Length; - - var entries1 = _entries; - - // we're always checking for entry before adding new - // so this loop is useless - /*for (var i = _buckets[targetBucket]; i >= 0; i = entries1[i].Next) - { - if (entries1[i].HashCode == hashCode && ReferenceEquals(entries1[i].Key, key)) - { - entries1[i].Value = value; - return; - } - }*/ - - if (_count == entries1.Length) - { - Resize(); - entries1 = _entries; - targetBucket = hashCode % _buckets.Length; - } - - var index = _count; - _count++; - - entries1[index].HashCode = hashCode; - entries1[index].Next = _buckets[targetBucket]; - entries1[index].Key = key; - entries1[index].Value = value; - _buckets[targetBucket] = index; - } - - private void Resize() - { - Resize(ExpandPrime(_count)); - } - - private void Resize(int newSize) - { - var newBuckets = new int[newSize]; - for (int i = 0; i < newBuckets.Length; i++) - newBuckets[i] = -1; - var newEntries = new Entry[newSize]; - Array.Copy(_entries, 0, newEntries, 0, _count); - - for (var i = 0; i < _count; i++) - { - if (newEntries[i].HashCode >= 0) - { - var bucket = newEntries[i].HashCode % newSize; - newEntries[i].Next = newBuckets[bucket]; - newBuckets[bucket] = i; - } - } - - _buckets = newBuckets; - _entries = newEntries; - } - } - } -} diff --git a/src/Foundatio/DeepCloner/Helpers/DeepClonerCache.cs b/src/Foundatio/DeepCloner/Helpers/DeepClonerCache.cs deleted file mode 100644 index eea00e9d2..000000000 --- a/src/Foundatio/DeepCloner/Helpers/DeepClonerCache.cs +++ /dev/null @@ -1,99 +0,0 @@ -#define NETCORE -using System; -using System.Collections.Concurrent; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - internal static class DeepClonerCache - { - private static readonly ConcurrentDictionary _typeCache = new ConcurrentDictionary(); - - private static readonly ConcurrentDictionary _typeCacheDeepTo = new ConcurrentDictionary(); - - private static readonly ConcurrentDictionary _typeCacheShallowTo = new ConcurrentDictionary(); - - private static readonly ConcurrentDictionary _structAsObjectCache = new ConcurrentDictionary(); - - private static readonly ConcurrentDictionary, object> _typeConvertCache = new ConcurrentDictionary, object>(); - - public static object GetOrAddClass(Type type, Func adder) - { - // return _typeCache.GetOrAdd(type, x => adder(x)); - - // this implementation is slightly faster than getoradd - object value; - if (_typeCache.TryGetValue(type, out value)) return value; - - // will lock by type object to ensure only one type generator is generated simultaneously - lock (type) - { - value = _typeCache.GetOrAdd(type, t => adder(t)); - } - - return value; - } - - public static object GetOrAddDeepClassTo(Type type, Func adder) - { - object value; - if (_typeCacheDeepTo.TryGetValue(type, out value)) return value; - - // will lock by type object to ensure only one type generator is generated simultaneously - lock (type) - { - value = _typeCacheDeepTo.GetOrAdd(type, t => adder(t)); - } - - return value; - } - - public static object GetOrAddShallowClassTo(Type type, Func adder) - { - object value; - if (_typeCacheShallowTo.TryGetValue(type, out value)) return value; - - // will lock by type object to ensure only one type generator is generated simultaneously - lock (type) - { - value = _typeCacheShallowTo.GetOrAdd(type, t => adder(t)); - } - - return value; - } - - public static object GetOrAddStructAsObject(Type type, Func adder) - { - // return _typeCache.GetOrAdd(type, x => adder(x)); - - // this implementation is slightly faster than getoradd - object value; - if (_structAsObjectCache.TryGetValue(type, out value)) return value; - - // will lock by type object to ensure only one type generator is generated simultaneously - lock (type) - { - value = _structAsObjectCache.GetOrAdd(type, t => adder(t)); - } - - return value; - } - - public static T GetOrAddConvertor(Type from, Type to, Func adder) - { - return (T)_typeConvertCache.GetOrAdd(new Tuple(from, to), (tuple) => adder(tuple.Item1, tuple.Item2)); - } - - /// - /// This method can be used when we switch between safe / unsafe variants (for testing) - /// - public static void ClearCache() - { - _typeCache.Clear(); - _typeCacheDeepTo.Clear(); - _typeCacheShallowTo.Clear(); - _structAsObjectCache.Clear(); - _typeConvertCache.Clear(); - } - } -} - diff --git a/src/Foundatio/DeepCloner/Helpers/DeepClonerExprGenerator.cs b/src/Foundatio/DeepCloner/Helpers/DeepClonerExprGenerator.cs deleted file mode 100644 index 78f85b4df..000000000 --- a/src/Foundatio/DeepCloner/Helpers/DeepClonerExprGenerator.cs +++ /dev/null @@ -1,265 +0,0 @@ -#define NETCORE -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - internal static class DeepClonerExprGenerator - { - private static readonly ConcurrentDictionary _readonlyFields = new ConcurrentDictionary(); - - private static readonly bool _canFastCopyReadonlyFields = false; - - private static readonly MethodInfo _fieldSetMethod; - static DeepClonerExprGenerator() - { - try - { - typeof(DeepClonerExprGenerator).GetPrivateStaticField(nameof(_canFastCopyReadonlyFields)).SetValue(null, true); -#if NETCORE13 - _fieldSetMethod = typeof(FieldInfo).GetRuntimeMethod("SetValue", new[] { typeof(object), typeof(object) }); -#else - _fieldSetMethod = typeof(FieldInfo).GetMethod("SetValue", new[] {typeof(object), typeof(object)}); -#endif - - if (_fieldSetMethod == null) - throw new ArgumentNullException(); - } - catch (Exception) - { - // cannot - } - } - - internal static object GenerateClonerInternal(Type realType, bool asObject) - { - return GenerateProcessMethod(realType, asObject && realType.IsValueType()); - } - - private static FieldInfo _attributesFieldInfo = typeof(FieldInfo).GetPrivateField("m_fieldAttributes"); - - // today, I found that it not required to do such complex things. Just SetValue is enough - // is it new runtime changes, or I made incorrect assumptions eariler - // slow, but hardcore method to set readonly field - internal static void ForceSetField(FieldInfo field, object obj, object value) - { - var fieldInfo = field.GetType().GetPrivateField("m_fieldAttributes"); - - // TODO: think about it - // nothing to do :( we should a throw an exception, but it is no good for user - if (fieldInfo == null) - return; - var ov = fieldInfo.GetValue(field); - if (!(ov is FieldAttributes)) - return; - var v = (FieldAttributes)ov; - - // protect from parallel execution, when first thread set field readonly back, and second set it to write value - lock (fieldInfo) - { - fieldInfo.SetValue(field, v & ~FieldAttributes.InitOnly); - field.SetValue(obj, value); - fieldInfo.SetValue(field, v | FieldAttributes.InitOnly); - } - } - - private static object GenerateProcessMethod(Type type, bool unboxStruct) - { - if (type.IsArray) - { - return GenerateProcessArrayMethod(type); - } - - if (type.FullName != null && type.FullName.StartsWith("System.Tuple`")) - { - // if not safe type it is no guarantee that some type will contain reference to - // this tuple. In usual way, we're creating new object, setting reference for it - // and filling data. For tuple, we will fill data before creating object - // (in constructor arguments) - var genericArguments = type.GenericArguments(); - // current tuples contain only 8 arguments, but may be in future... - // we'll write code that works with it - if (genericArguments.Length < 10 && genericArguments.All(DeepClonerSafeTypes.CanReturnSameObject)) - { - return GenerateProcessTupleMethod(type); - } - } - - var methodType = unboxStruct || type.IsClass() ? typeof(object) : type; - - var expressionList = new List(); - - ParameterExpression from = Expression.Parameter(methodType); - var fromLocal = from; - var toLocal = Expression.Variable(type); - var state = Expression.Parameter(typeof(DeepCloneState)); - - if (!type.IsValueType()) - { - var methodInfo = typeof(object).GetPrivateMethod("MemberwiseClone"); - - // to = (T)from.MemberwiseClone() - expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(from, methodInfo), type))); - - fromLocal = Expression.Variable(type); - // fromLocal = (T)from - expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); - - // added from -> to binding to ensure reference loop handling - // structs cannot loop here - // state.AddKnownRef(from, to) - expressionList.Add(Expression.Call(state, typeof(DeepCloneState).GetMethod("AddKnownRef"), from, toLocal)); - } - else - { - if (unboxStruct) - { - // toLocal = (T)from; - expressionList.Add(Expression.Assign(toLocal, Expression.Unbox(from, type))); - fromLocal = Expression.Variable(type); - // fromLocal = toLocal; // structs, it is ok to copy - expressionList.Add(Expression.Assign(fromLocal, toLocal)); - } - else - { - // toLocal = from - expressionList.Add(Expression.Assign(toLocal, from)); - } - } - - List fi = new List(); - var tp = type; - do - { -#if !NETCORE - // don't do anything with this dark magic! - if (tp == typeof(ContextBoundObject)) break; -#else - if (tp.Name == "ContextBoundObject") break; -#endif - - fi.AddRange(tp.GetDeclaredFields()); - tp = tp.BaseType(); - } - while (tp != null); - - foreach (var fieldInfo in fi) - { - if (!DeepClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType)) - { - var methodInfo = fieldInfo.FieldType.IsValueType() - ? typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneStructInternal") - .MakeGenericMethod(fieldInfo.FieldType) - : typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneClassInternal"); - - var get = Expression.Field(fromLocal, fieldInfo); - - // toLocal.Field = Clone...Internal(fromLocal.Field) - var call = (Expression)Expression.Call(methodInfo, get, state); - if (!fieldInfo.FieldType.IsValueType()) - call = Expression.Convert(call, fieldInfo.FieldType); - - // should handle specially - // todo: think about optimization, but it rare case - var isReadonly = _readonlyFields.GetOrAdd(fieldInfo, f => f.IsInitOnly); - if (isReadonly) - { - if (_canFastCopyReadonlyFields) - { - expressionList.Add(Expression.Call( - Expression.Constant(fieldInfo), - _fieldSetMethod, - Expression.Convert(toLocal, typeof(object)), - Expression.Convert(call, typeof(object)))); - } - else - { - var setMethod = typeof(DeepClonerExprGenerator).GetPrivateStaticMethod("ForceSetField"); - expressionList.Add(Expression.Call(setMethod, Expression.Constant(fieldInfo), Expression.Convert(toLocal, typeof(object)), Expression.Convert(call, typeof(object)))); - } - } - else - { - expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), call)); - } - } - } - - expressionList.Add(Expression.Convert(toLocal, methodType)); - - var funcType = typeof(Func<,,>).MakeGenericType(methodType, typeof(DeepCloneState), methodType); - - var blockParams = new List(); - if (from != fromLocal) blockParams.Add(fromLocal); - blockParams.Add(toLocal); - - return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, state).Compile(); - } - - private static object GenerateProcessArrayMethod(Type type) - { - var elementType = type.GetElementType(); - var rank = type.GetArrayRank(); - - MethodInfo methodInfo; - - // multidim or not zero-based arrays - if (rank != 1 || type != elementType.MakeArrayType()) - { - if (rank == 2 && type == elementType.MakeArrayType(2)) - { - // small optimization for 2 dim arrays - methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod("Clone2DimArrayInternal").MakeGenericMethod(elementType); - } - else - { - methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneAbstractArrayInternal"); - } - } - else - { - var methodName = "Clone1DimArrayClassInternal"; - if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) methodName = "Clone1DimArraySafeInternal"; - else if (elementType.IsValueType()) methodName = "Clone1DimArrayStructInternal"; - methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod(methodName).MakeGenericMethod(elementType); - } - - ParameterExpression from = Expression.Parameter(typeof(object)); - var state = Expression.Parameter(typeof(DeepCloneState)); - var call = Expression.Call(methodInfo, Expression.Convert(from, type), state); - - var funcType = typeof(Func<,,>).MakeGenericType(typeof(object), typeof(DeepCloneState), typeof(object)); - - return Expression.Lambda(funcType, call, from, state).Compile(); - } - - private static object GenerateProcessTupleMethod(Type type) - { - ParameterExpression from = Expression.Parameter(typeof(object)); - var state = Expression.Parameter(typeof(DeepCloneState)); - - var local = Expression.Variable(type); - var assign = Expression.Assign(local, Expression.Convert(from, type)); - - var funcType = typeof(Func); - - var tupleLength = type.GenericArguments().Length; - - var constructor = Expression.Assign(local, Expression.New(type.GetPublicConstructors().First(x => x.GetParameters().Length == tupleLength), - type.GetPublicProperties().OrderBy(x => x.Name) - .Where(x => x.CanRead && x.Name.StartsWith("Item") && char.IsDigit(x.Name[4])) - .Select(x => Expression.Property(local, x.Name)))); - - return Expression.Lambda(funcType, Expression.Block(new[] { local }, - assign, constructor, Expression.Call(state, typeof(DeepCloneState).GetMethod("AddKnownRef"), from, local), - from), - from, state).Compile(); - } - - } -} - diff --git a/src/Foundatio/DeepCloner/Helpers/DeepClonerGenerator.cs b/src/Foundatio/DeepCloner/Helpers/DeepClonerGenerator.cs deleted file mode 100644 index cfeecbc41..000000000 --- a/src/Foundatio/DeepCloner/Helpers/DeepClonerGenerator.cs +++ /dev/null @@ -1,233 +0,0 @@ -#define NETCORE -using System; -using System.Linq; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - internal static class DeepClonerGenerator - { - public static T CloneObject(T obj) - { - if (obj is ValueType) - { - var type = obj.GetType(); - if (typeof(T) == type) - { - if (DeepClonerSafeTypes.CanReturnSameObject(type)) - return obj; - - return CloneStructInternal(obj, new DeepCloneState()); - } - } - - return (T)CloneClassRoot(obj); - } - - private static object CloneClassRoot(object obj) - { - if (obj == null) - return null; - - var cloner = (Func)DeepClonerCache.GetOrAddClass(obj.GetType(), t => GenerateCloner(t, true)); - - // null -> should return same type - if (cloner == null) - return obj; - - return cloner(obj, new DeepCloneState()); - } - - internal static object CloneClassInternal(object obj, DeepCloneState state) - { - if (obj == null) - return null; - - var cloner = (Func)DeepClonerCache.GetOrAddClass(obj.GetType(), t => GenerateCloner(t, true)); - - // safe object - if (cloner == null) - return obj; - - // loop - var knownRef = state.GetKnownRef(obj); - if (knownRef != null) - return knownRef; - - return cloner(obj, state); - } - - private static T CloneStructInternal(T obj, DeepCloneState state) // where T : struct - { - // no loops, no nulls, no inheritance - var cloner = GetClonerForValueType(); - - // safe ojbect - if (cloner == null) - return obj; - - return cloner(obj, state); - } - - // when we can't use code generation, we can use these methods - internal static T[] Clone1DimArraySafeInternal(T[] obj, DeepCloneState state) - { - var l = obj.Length; - var outArray = new T[l]; - state.AddKnownRef(obj, outArray); - Array.Copy(obj, outArray, obj.Length); - return outArray; - } - - internal static T[] Clone1DimArrayStructInternal(T[] obj, DeepCloneState state) - { - // not null from called method, but will check it anyway - if (obj == null) return null; - var l = obj.Length; - var outArray = new T[l]; - state.AddKnownRef(obj, outArray); - var cloner = GetClonerForValueType(); - for (var i = 0; i < l; i++) - outArray[i] = cloner(obj[i], state); - - return outArray; - } - - internal static T[] Clone1DimArrayClassInternal(T[] obj, DeepCloneState state) - { - // not null from called method, but will check it anyway - if (obj == null) return null; - var l = obj.Length; - var outArray = new T[l]; - state.AddKnownRef(obj, outArray); - for (var i = 0; i < l; i++) - outArray[i] = (T)CloneClassInternal(obj[i], state); - - return outArray; - } - - // relatively frequent case. specially handled - internal static T[,] Clone2DimArrayInternal(T[,] obj, DeepCloneState state) - { - // not null from called method, but will check it anyway - if (obj == null) return null; - - // we cannot determine by type multidim arrays (one dimension is possible) - // so, will check for index here - var lb1 = obj.GetLowerBound(0); - var lb2 = obj.GetLowerBound(1); - if (lb1 != 0 || lb2 != 0) - return (T[,]) CloneAbstractArrayInternal(obj, state); - - var l1 = obj.GetLength(0); - var l2 = obj.GetLength(1); - var outArray = new T[l1, l2]; - state.AddKnownRef(obj, outArray); - if (DeepClonerSafeTypes.CanReturnSameObject(typeof(T))) - { - Array.Copy(obj, outArray, obj.Length); - return outArray; - } - - if (typeof(T).IsValueType()) - { - var cloner = GetClonerForValueType(); - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - outArray[i, k] = cloner(obj[i, k], state); - } - else - { - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - outArray[i, k] = (T)CloneClassInternal(obj[i, k], state); - } - - return outArray; - } - - // rare cases, very slow cloning. currently it's ok - internal static Array CloneAbstractArrayInternal(Array obj, DeepCloneState state) - { - // not null from called method, but will check it anyway - if (obj == null) return null; - var rank = obj.Rank; - - var lengths = Enumerable.Range(0, rank).Select(obj.GetLength).ToArray(); - - var lowerBounds = Enumerable.Range(0, rank).Select(obj.GetLowerBound).ToArray(); - var idxes = Enumerable.Range(0, rank).Select(obj.GetLowerBound).ToArray(); - - var elementType = obj.GetType().GetElementType(); - var outArray = Array.CreateInstance(elementType, lengths, lowerBounds); - - state.AddKnownRef(obj, outArray); - - // we're unable to set any value to this array, so, just return it - if (lengths.Any(x => x == 0)) - return outArray; - - if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) - { - Array.Copy(obj, outArray, obj.Length); - return outArray; - } - - var ofs = rank - 1; - while (true) - { - outArray.SetValue(CloneClassInternal(obj.GetValue(idxes), state), idxes); - idxes[ofs]++; - - if (idxes[ofs] >= lowerBounds[ofs] + lengths[ofs]) - { - do - { - if (ofs == 0) return outArray; - idxes[ofs] = lowerBounds[ofs]; - ofs--; - idxes[ofs]++; - } while (idxes[ofs] >= lowerBounds[ofs] + lengths[ofs]); - - ofs = rank - 1; - } - } - } - - internal static Func GetClonerForValueType() - { - return (Func)DeepClonerCache.GetOrAddStructAsObject(typeof(T), t => GenerateCloner(t, false)); - } - - private static object GenerateCloner(Type t, bool asObject) - { - if (DeepClonerSafeTypes.CanReturnSameObject(t) && (asObject && !t.IsValueType())) - return null; - -#if !NETCORE - if (ShallowObjectCloner.IsSafeVariant()) return DeepClonerExprGenerator.GenerateClonerInternal(t, asObject); - else return DeepClonerMsilGenerator.GenerateClonerInternal(t, asObject); -#else - return DeepClonerExprGenerator.GenerateClonerInternal(t, asObject); -#endif - } - - public static object CloneObjectTo(object objFrom, object objTo, bool isDeep) - { - if (objTo == null) return null; - - if (objFrom == null) - throw new ArgumentNullException("objFrom", "Cannot copy null object to another"); - var type = objFrom.GetType(); - if (!type.IsInstanceOfType(objTo)) - throw new InvalidOperationException("From object should be derived from From object, but From object has type " + objFrom.GetType().FullName + " and to " + objTo.GetType().FullName); - if (objFrom is string) - throw new InvalidOperationException("It is forbidden to clone strings"); - var cloner = (Func)(isDeep - ? DeepClonerCache.GetOrAddDeepClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, true)) - : DeepClonerCache.GetOrAddShallowClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, false))); - if (cloner == null) return objTo; - return cloner(objFrom, objTo, new DeepCloneState()); - } - } -} - diff --git a/src/Foundatio/DeepCloner/Helpers/DeepClonerSafeTypes.cs b/src/Foundatio/DeepCloner/Helpers/DeepClonerSafeTypes.cs deleted file mode 100644 index aaf4fb007..000000000 --- a/src/Foundatio/DeepCloner/Helpers/DeepClonerSafeTypes.cs +++ /dev/null @@ -1,215 +0,0 @@ -#define NETCORE -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - /// - /// Safe types are types, which can be copied without real cloning. e.g. simple structs or strings (it is immutable) - /// - internal static class DeepClonerSafeTypes - { - internal static readonly ConcurrentDictionary KnownTypes = new ConcurrentDictionary(); - - static DeepClonerSafeTypes() - { - foreach ( - var x in - new[] - { - typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), - typeof(float), typeof(double), typeof(decimal), typeof(char), typeof(string), typeof(bool), typeof(DateTime), - typeof(IntPtr), typeof(UIntPtr), typeof(Guid), - // do not clone such native type - Type.GetType("System.RuntimeType"), - Type.GetType("System.RuntimeTypeHandle"), - StringComparer.Ordinal.GetType(), - StringComparer.CurrentCulture.GetType(), // CultureAwareComparer - can be same -#if !NETCORE - typeof(DBNull) -#endif - }) KnownTypes.TryAdd(x, true); - } - - private static bool CanReturnSameType(Type type, HashSet processingTypes) - { - bool isSafe; - if (KnownTypes.TryGetValue(type, out isSafe)) - return isSafe; - - // enums are safe - // pointers (e.g. int*) are unsafe, but we cannot do anything with it except blind copy - if (type.IsEnum() || type.IsPointer) - { - KnownTypes.TryAdd(type, true); - return true; - } - -#if !NETCORE - // do not do anything with remoting. it is very dangerous to clone, bcs it relate to deep core of framework - if (type.FullName.StartsWith("System.Runtime.Remoting.") - && type.Assembly == typeof(System.Runtime.Remoting.CustomErrorsModes).Assembly) - { - KnownTypes.TryAdd(type, true); - return true; - } - - if (type.FullName.StartsWith("System.Reflection.") && type.Assembly == typeof(PropertyInfo).Assembly) - { - KnownTypes.TryAdd(type, true); - return true; - } - - // catched by previous condition - /*if (type.FullName.StartsWith("System.Reflection.Emit") && type.Assembly == typeof(System.Reflection.Emit.OpCode).Assembly) - { - KnownTypes.TryAdd(type, true); - return true; - }*/ - - // this types are serious native resources, it is better not to clone it - if (type.IsSubclassOf(typeof(System.Runtime.ConstrainedExecution.CriticalFinalizerObject))) - { - KnownTypes.TryAdd(type, true); - return true; - } - - // Better not to do anything with COM - if (type.IsCOMObject) - { - KnownTypes.TryAdd(type, true); - return true; - } -#else - // do not copy db null - if (type.FullName.StartsWith("System.DBNull")) - { - KnownTypes.TryAdd(type, true); - return true; - } - - if (type.FullName.StartsWith("System.RuntimeType")) - { - KnownTypes.TryAdd(type, true); - return true; - } - - if (type.FullName.StartsWith("System.Reflection.") && Equals(type.GetTypeInfo().Assembly, typeof(PropertyInfo).GetTypeInfo().Assembly)) - { - KnownTypes.TryAdd(type, true); - return true; - } - - if (type.IsSubclassOfTypeByName("CriticalFinalizerObject")) - { - KnownTypes.TryAdd(type, true); - return true; - } - - // better not to touch ms dependency injection - if (type.FullName.StartsWith("Microsoft.Extensions.DependencyInjection.")) - { - KnownTypes.TryAdd(type, true); - return true; - } - - if (type.FullName == "Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector") - { - KnownTypes.TryAdd(type, true); - return true; - } -#endif - // default comparers should not be cloned due possible comparison EqualityComparer.Default == comparer - if (type.FullName.Contains("EqualityComparer")) - { - if (type.FullName.StartsWith("System.Collections.Generic.GenericEqualityComparer`") - || type.FullName.StartsWith("System.Collections.Generic.ObjectEqualityComparer`") - || type.FullName.StartsWith("System.Collections.Generic.EnumEqualityComparer`") - || type.FullName.StartsWith("System.Collections.Generic.NullableEqualityComparer`") - || type.FullName == "System.Collections.Generic.ByteEqualityComparer") - { - KnownTypes.TryAdd(type, true); - return true; - } - } - - // classes are always unsafe (we should copy it fully to count references) - if (!type.IsValueType()) - { - KnownTypes.TryAdd(type, false); - return false; - } - - if (processingTypes == null) - processingTypes = new HashSet(); - - // structs cannot have a loops, but check it anyway - processingTypes.Add(type); - - List fi = new List(); - var tp = type; - do - { - fi.AddRange(tp.GetAllFields()); - tp = tp.BaseType(); - } - while (tp != null); - - foreach (var fieldInfo in fi) - { - // type loop - var fieldType = fieldInfo.FieldType; - if (processingTypes.Contains(fieldType)) - continue; - - // not safe and not not safe. we need to go deeper - if (!CanReturnSameType(fieldType, processingTypes)) - { - KnownTypes.TryAdd(type, false); - return false; - } - } - - KnownTypes.TryAdd(type, true); - return true; - } - - // not used anymore - /*/// - /// Classes with only safe fields are safe for ShallowClone (if they root objects for copying) - /// - private static bool CanCopyClassInShallow(Type type) - { - // do not do this anything for struct and arrays - if (!type.IsClass() || type.IsArray) - { - return false; - } - - List fi = new List(); - var tp = type; - do - { - fi.AddRange(tp.GetAllFields()); - tp = tp.BaseType(); - } - while (tp != null); - - if (fi.Any(fieldInfo => !CanReturnSameType(fieldInfo.FieldType, null))) - { - return false; - } - - return true; - }*/ - - public static bool CanReturnSameObject(Type type) - { - return CanReturnSameType(type, null); - } - } -} - diff --git a/src/Foundatio/DeepCloner/Helpers/ReflectionHelper.cs b/src/Foundatio/DeepCloner/Helpers/ReflectionHelper.cs deleted file mode 100644 index 41ff7beeb..000000000 --- a/src/Foundatio/DeepCloner/Helpers/ReflectionHelper.cs +++ /dev/null @@ -1,171 +0,0 @@ -#define NETCORE -using System; -using System.Linq; -using System.Reflection; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - internal static class ReflectionHelper - { - public static bool IsEnum(this Type t) - { -#if NETCORE - return t.GetTypeInfo().IsEnum; -#else - return t.IsEnum; -#endif - } - - public static bool IsValueType(this Type t) - { -#if NETCORE - return t.GetTypeInfo().IsValueType; -#else - return t.IsValueType; -#endif - } - - public static bool IsClass(this Type t) - { -#if NETCORE - return t.GetTypeInfo().IsClass; -#else - return t.IsClass; -#endif - } - - public static Type BaseType(this Type t) - { -#if NETCORE - return t.GetTypeInfo().BaseType; -#else - return t.BaseType; -#endif - } - - public static FieldInfo[] GetAllFields(this Type t) - { -#if NETCORE - return t.GetTypeInfo().DeclaredFields.Where(x => !x.IsStatic).ToArray(); -#else - return t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); -#endif - } - - public static PropertyInfo[] GetPublicProperties(this Type t) - { -#if NETCORE - return t.GetTypeInfo().DeclaredProperties.ToArray(); -#else - return t.GetProperties(BindingFlags.Instance | BindingFlags.Public); -#endif - } - - public static FieldInfo[] GetDeclaredFields(this Type t) - { -#if NETCORE - return t.GetTypeInfo().DeclaredFields.Where(x => !x.IsStatic).ToArray(); -#else - return t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); -#endif - } - - public static ConstructorInfo[] GetPrivateConstructors(this Type t) - { -#if NETCORE - return t.GetTypeInfo().DeclaredConstructors.ToArray(); -#else - return t.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); -#endif - } - - public static ConstructorInfo[] GetPublicConstructors(this Type t) - { -#if NETCORE - return t.GetTypeInfo().DeclaredConstructors.ToArray(); -#else - return t.GetConstructors(BindingFlags.Public | BindingFlags.Instance); -#endif - } - - public static MethodInfo GetPrivateMethod(this Type t, string methodName) - { -#if NETCORE - return t.GetTypeInfo().GetDeclaredMethod(methodName); -#else - return t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); -#endif - } - - public static MethodInfo GetMethod(this Type t, string methodName) - { -#if NETCORE - return t.GetTypeInfo().GetDeclaredMethod(methodName); -#else - return t.GetMethod(methodName); -#endif - } - - public static MethodInfo GetPrivateStaticMethod(this Type t, string methodName) - { -#if NETCORE - return t.GetTypeInfo().GetDeclaredMethod(methodName); -#else - return t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static); -#endif - } - - public static FieldInfo GetPrivateField(this Type t, string fieldName) - { -#if NETCORE - return t.GetTypeInfo().GetDeclaredField(fieldName); -#else - return t.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); -#endif - } - - public static FieldInfo GetPrivateStaticField(this Type t, string fieldName) - { -#if NETCORE - return t.GetTypeInfo().GetDeclaredField(fieldName); -#else - return t.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static); -#endif - } - -#if NETCORE - public static bool IsSubclassOfTypeByName(this Type t, string typeName) - { - while (t != null) - { - if (t.Name == typeName) - return true; - t = t.BaseType(); - } - - return false; - } -#endif - -#if NETCORE - public static bool IsAssignableFrom(this Type from, Type to) - { - return from.GetTypeInfo().IsAssignableFrom(to.GetTypeInfo()); - } - - public static bool IsInstanceOfType(this Type from, object to) - { - return from.IsAssignableFrom(to.GetType()); - } -#endif - - public static Type[] GenericArguments(this Type t) - { -#if NETCORE - return t.GetTypeInfo().GenericTypeArguments; -#else - return t.GetGenericArguments(); -#endif - } - } -} diff --git a/src/Foundatio/DeepCloner/Helpers/ShallowClonerGenerator.cs b/src/Foundatio/DeepCloner/Helpers/ShallowClonerGenerator.cs deleted file mode 100644 index af6e8c7a2..000000000 --- a/src/Foundatio/DeepCloner/Helpers/ShallowClonerGenerator.cs +++ /dev/null @@ -1,31 +0,0 @@ -#define NETCORE -using System; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - internal static class ShallowClonerGenerator - { - public static T CloneObject(T obj) - { - // this is faster than typeof(T).IsValueType - if (obj is ValueType) - { - if (typeof(T) == obj.GetType()) - return obj; - - // we're here so, we clone value type obj as object type T - // so, we need to copy it, bcs we have a reference, not real object. - return (T)ShallowObjectCloner.CloneObject(obj); - } - - if (ReferenceEquals(obj, null)) - return (T)(object)null; - - if (DeepClonerSafeTypes.CanReturnSameObject(obj.GetType())) - return obj; - - return (T)ShallowObjectCloner.CloneObject(obj); - } - } -} - diff --git a/src/Foundatio/DeepCloner/Helpers/ShallowObjectCloner.cs b/src/Foundatio/DeepCloner/Helpers/ShallowObjectCloner.cs deleted file mode 100644 index cefbe71e2..000000000 --- a/src/Foundatio/DeepCloner/Helpers/ShallowObjectCloner.cs +++ /dev/null @@ -1,117 +0,0 @@ -#define NETCORE -using System; -using System.Linq.Expressions; -using System.Reflection; -using System.Reflection.Emit; - -namespace Foundatio.Force.DeepCloner.Helpers -{ - /// - /// Internal class but due implementation restriction should be public - /// - internal abstract class ShallowObjectCloner - { - /// - /// Abstract method for real object cloning - /// - protected abstract object DoCloneObject(object obj); - - private static readonly ShallowObjectCloner _unsafeInstance; - - private static ShallowObjectCloner _instance; - - /// - /// Performs real shallow object clone - /// - public static object CloneObject(object obj) - { - return _instance.DoCloneObject(obj); - } - - internal static bool IsSafeVariant() - { - return _instance is ShallowSafeObjectCloner; - } - - static ShallowObjectCloner() - { -#if !NETCORE - _unsafeInstance = GenerateUnsafeCloner(); - _instance = _unsafeInstance; - try - { - _instance.DoCloneObject(new object()); - } - catch (Exception) - { - // switching to safe - _instance = new ShallowSafeObjectCloner(); - } -#else - _instance = new ShallowSafeObjectCloner(); - // no unsafe variant for core - _unsafeInstance = _instance; -#endif - } - - /// - /// Purpose of this method is testing variants - /// - internal static void SwitchTo(bool isSafe) - { - DeepClonerCache.ClearCache(); - if (isSafe) _instance = new ShallowSafeObjectCloner(); - else _instance = _unsafeInstance; - } - -#if !NETCORE - private static ShallowObjectCloner GenerateUnsafeCloner() - { - var mb = TypeCreationHelper.GetModuleBuilder(); - - var builder = mb.DefineType("ShallowSafeObjectClonerImpl", TypeAttributes.Public, typeof(ShallowObjectCloner)); - var ctorBuilder = builder.DefineConstructor(MethodAttributes.Public, CallingConventions.HasThis | CallingConventions.HasThis, Type.EmptyTypes); - - var cil = ctorBuilder.GetILGenerator(); - cil.Emit(OpCodes.Ldarg_0); - // ReSharper disable AssignNullToNotNullAttribute - cil.Emit(OpCodes.Call, typeof(ShallowObjectCloner).GetPrivateConstructors()[0]); - // ReSharper restore AssignNullToNotNullAttribute - cil.Emit(OpCodes.Ret); - - var methodBuilder = builder.DefineMethod( - "DoCloneObject", - MethodAttributes.Public | MethodAttributes.Virtual, - CallingConventions.HasThis, - typeof(object), - new[] { typeof(object) }); - - var il = methodBuilder.GetILGenerator(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, typeof(object).GetPrivateMethod("MemberwiseClone")); - il.Emit(OpCodes.Ret); - var type = builder.CreateType(); - return (ShallowObjectCloner)Activator.CreateInstance(type); - } -#endif - - private class ShallowSafeObjectCloner : ShallowObjectCloner - { - private static readonly Func _cloneFunc; - - static ShallowSafeObjectCloner() - { - var methodInfo = typeof(object).GetPrivateMethod("MemberwiseClone"); - var p = Expression.Parameter(typeof(object)); - var mce = Expression.Call(p, methodInfo); - _cloneFunc = Expression.Lambda>(mce, p).Compile(); - } - - protected override object DoCloneObject(object obj) - { - return _cloneFunc(obj); - } - } - } -} - diff --git a/src/Foundatio/Extensions/ObjectExtensions.cs b/src/Foundatio/Extensions/ObjectExtensions.cs index 8a8d5a5da..959e9c54c 100644 --- a/src/Foundatio/Extensions/ObjectExtensions.cs +++ b/src/Foundatio/Extensions/ObjectExtensions.cs @@ -1,11 +1,12 @@ -using Foundatio.Force.DeepCloner.Helpers; +using System.Diagnostics.CodeAnalysis; namespace Foundatio.Utility; public static class ObjectExtensions { - public static T DeepClone(this T original) + [return: NotNullIfNotNull(nameof(original))] + public static T? DeepClone(this T? original) { - return DeepClonerGenerator.CloneObject(original); + return Foundatio.FastCloner.Code.FastClonerGenerator.CloneObject(original); } } diff --git a/src/Foundatio/FastCloner/Code/AhoCorasick.cs b/src/Foundatio/FastCloner/Code/AhoCorasick.cs new file mode 100644 index 000000000..dea758c37 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/AhoCorasick.cs @@ -0,0 +1,91 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Collections.Generic; + +namespace Foundatio.FastCloner.Code; +internal class AhoCorasick +{ + private class Node + { + public readonly Dictionary Children = new Dictionary(); + public Node? Failure; + public bool IsEndOfPattern; + } + + private readonly Node root = new Node(); + private readonly string[] patterns; + public AhoCorasick(string[] patterns) + { + this.patterns = patterns; + BuildTrie(); + BuildFailureLinks(); + } + + private void BuildTrie() + { + foreach (string pattern in patterns) + { + Node current = root; + foreach (char c in pattern) + { + if (!current.Children.TryGetValue(c, out Node value)) + { + value = new Node(); + current.Children[c] = value; + } + + current = value; + } + + current.IsEndOfPattern = true; + } + } + + private void BuildFailureLinks() + { + Queue queue = new Queue(); + foreach (Node node in root.Children.Values) + { + node.Failure = root; + queue.Enqueue(node); + } + + while (queue.Count > 0) + { + Node current = queue.Dequeue(); + foreach (KeyValuePair kvp in current.Children) + { + queue.Enqueue(kvp.Value); + Node? failure = current.Failure; + while (failure != null && !failure.Children.ContainsKey(kvp.Key)) + { + failure = failure.Failure; + } + + kvp.Value.Failure = failure?.Children.GetValueOrDefault(kvp.Key) ?? root; + } + } + } + + public bool ContainsAnyPattern(string text) + { + Node? current = root; + foreach (char c in text) + { + while (current != null && !current.Children.ContainsKey(c)) + { + current = current.Failure; + } + + current = current?.Children.GetValueOrDefault(c) ?? root; + if (current.IsEndOfPattern) + { + return true; + } + } + + return false; + } +} diff --git a/src/Foundatio/FastCloner/Code/ClonerToExprGenerator.cs b/src/Foundatio/FastCloner/Code/ClonerToExprGenerator.cs new file mode 100644 index 000000000..4f981180f --- /dev/null +++ b/src/Foundatio/FastCloner/Code/ClonerToExprGenerator.cs @@ -0,0 +1,396 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Linq.Expressions; +using System.Reflection; +using System; +using System.Collections.Generic; + +namespace Foundatio.FastCloner.Code; +internal static class ClonerToExprGenerator +{ + internal static object GenerateClonerInternal(Type realType, bool isDeepClone) + { + return realType.IsValueType() ? throw new InvalidOperationException("Operation is valid only for reference types") : GenerateProcessMethod(realType, isDeepClone); + } + + private static object GenerateProcessMethod(Type type, bool isDeepClone) + { + if (type.IsArray) + { + return GenerateProcessArrayMethod(type, isDeepClone); + } + + Type methodType = typeof(object); + List expressionList = []; + ParameterExpression from = Expression.Parameter(methodType); + ParameterExpression fromLocal = from; + ParameterExpression to = Expression.Parameter(methodType); + ParameterExpression toLocal = to; + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + // if (!type.IsValueType()) + { + fromLocal = Expression.Variable(type); + toLocal = Expression.Variable(type); + // fromLocal = (T)from + expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); + expressionList.Add(Expression.Assign(toLocal, Expression.Convert(to, type))); + if (isDeepClone) + { + // added from -> to binding to ensure reference loop handling + // structs cannot loop here + // state.AddKnownRef(from, to) + expressionList.Add(Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, to)); + } + } + + foreach (FieldInfo fieldInfo in EnumerateFields(type)) + { + // Check if member should be shallow cloned (copy reference directly) + if (FastClonerExprGenerator.MemberIsShallow(fieldInfo)) + { + Expression sourceValue = Expression.Field(fromLocal, fieldInfo); + if (fieldInfo.IsInitOnly) + { + ConstantExpression setter = Expression.Constant(FieldAccessorGenerator.GetFieldSetter(fieldInfo)); + expressionList.Add(Expression.Invoke(setter, Expression.Convert(toLocal, typeof(object)), Expression.Convert(sourceValue, typeof(object)))); + } + else + { + expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), sourceValue)); + } + } + else if (isDeepClone && !FastClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType)) + { + MethodInfo methodInfo = StaticMethodInfos.DeepClonerGeneratorMethods.MakeFieldCloneMethodInfo(fieldInfo.FieldType); + MemberExpression get = Expression.Field(fromLocal, fieldInfo); + // toLocal.Field = Clone...Internal(fromLocal.Field) + Expression call = Expression.Call(methodInfo, get, state); + if (!fieldInfo.FieldType.IsValueType()) + call = Expression.Convert(call, fieldInfo.FieldType); + if (fieldInfo.IsInitOnly) + { + ConstantExpression setter = Expression.Constant(FieldAccessorGenerator.GetFieldSetter(fieldInfo)); + expressionList.Add(Expression.Invoke(setter, Expression.Convert(toLocal, typeof(object)), Expression.Convert(call, typeof(object)))); + } + else + { + expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), call)); + } + } + else + { + Expression sourceValue = Expression.Field(fromLocal, fieldInfo); + if (fieldInfo.IsInitOnly) + { + ConstantExpression setter = Expression.Constant(FieldAccessorGenerator.GetFieldSetter(fieldInfo)); + expressionList.Add(Expression.Invoke(setter, Expression.Convert(toLocal, typeof(object)), Expression.Convert(sourceValue, typeof(object)))); + } + else + { + expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), sourceValue)); + } + } + } + + expressionList.Add(Expression.Convert(toLocal, methodType)); + Type funcType = typeof(Func<,,, >).MakeGenericType(methodType, methodType, typeof(FastCloneState), methodType); + List blockParams = []; + if (from != fromLocal) + blockParams.Add(fromLocal); + if (to != toLocal) + blockParams.Add(toLocal); + return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, to, state).Compile(); + static IEnumerable EnumerateFields(Type sourceType) + { + Type? tp = sourceType; + while (tp != null && tp != typeof(ContextBoundObject)) + { + foreach (FieldInfo field in tp.GetDeclaredFields()) + { + yield return field; + } + + tp = tp.BaseType(); + } + } + } + + private static object GenerateProcessArrayMethod(Type type, bool isDeep) + { + Type elementType = type.GetElementType()!; + int rank = type.GetArrayRank(); + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression to = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + Type funcType = typeof(Func<,,, >).MakeGenericType(typeof(object), typeof(object), typeof(FastCloneState), typeof(object)); + if (rank == 1 && type == elementType.MakeArrayType()) + { + if (!isDeep) + { + MethodCallExpression callS = Expression.Call(typeof(ClonerToExprGenerator).GetPrivateStaticMethod(nameof(ShallowClone1DimArraySafeInternal))!.MakeGenericMethod(elementType), Expression.Convert(from, type), Expression.Convert(to, type)); + return Expression.Lambda(funcType, callS, from, to, state).Compile(); + } + else + { + string methodName = nameof(Clone1DimArrayClassInternal); + if (FastClonerSafeTypes.CanReturnSameObject(elementType)) + methodName = nameof(Clone1DimArraySafeInternal); + else if (elementType.IsValueType()) + methodName = nameof(Clone1DimArrayStructInternal); + MethodInfo methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod(methodName)!.MakeGenericMethod(elementType); + MethodCallExpression callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state); + return Expression.Lambda(funcType, callS, from, to, state).Compile(); + } + } + + { + // multidim or not zero-based arrays + MethodInfo methodInfo; + if (rank == 2 && type == elementType.MakeArrayType(2)) + methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod(nameof(Clone2DimArrayInternal))!.MakeGenericMethod(elementType); + else + methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod(nameof(CloneAbstractArrayInternal))!; + MethodCallExpression callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state, Expression.Constant(isDeep)); + return Expression.Lambda(funcType, callS, from, to, state).Compile(); + } + } + + // when we can't use code generation, we can use these methods + internal static T[] ShallowClone1DimArraySafeInternal(T[] objFrom, T[] objTo) + { + int l = Math.Min(objFrom.Length, objTo.Length); + Array.Copy(objFrom, objTo, l); + return objTo; + } + + // when we can't use code generation, we can use these methods + internal static T[] Clone1DimArraySafeInternal(T[] objFrom, T[] objTo, FastCloneState state) + { + int l = Math.Min(objFrom.Length, objTo.Length); + state.AddKnownRef(objFrom, objTo); + Array.Copy(objFrom, objTo, l); + return objTo; + } + + internal static T[]? Clone1DimArrayStructInternal(T[]? objFrom, T[]? objTo, FastCloneState state) + { + // not null from called method, but will check it anyway + if (objFrom == null || objTo == null) + return null; + int l = Math.Min(objFrom.Length, objTo.Length); + state.AddKnownRef(objFrom, objTo); + Func? cloner = FastClonerGenerator.GetClonerForValueType(); + if (cloner is not null) + { + for (int i = 0; i < l; i++) + { + objTo[i] = cloner(objFrom[i], state); + } + } + + return objTo; + } + + internal static T[]? Clone1DimArrayClassInternal(T[]? objFrom, T[]? objTo, FastCloneState state) + { + // not null from called method, but will check it anyway + if (objFrom == null || objTo == null) + return null; + int l = Math.Min(objFrom.Length, objTo.Length); + state.AddKnownRef(objFrom, objTo); + for (int i = 0; i < l; i++) + objTo[i] = (T)FastClonerGenerator.CloneClassInternal(objFrom[i], state)!; + return objTo; + } + + internal static T[, ]? Clone2DimArrayInternal(T[, ]? objFrom, T[, ]? objTo, FastCloneState state, bool isDeep) + { + // not null from called method, but will check it anyway + if (objFrom == null || objTo == null) + return null; + int fromLower0 = objFrom.GetLowerBound(0); + int fromLower1 = objFrom.GetLowerBound(1); + int toLower0 = objTo.GetLowerBound(0); + int toLower1 = objTo.GetLowerBound(1); + if (fromLower0 != 0 || fromLower1 != 0 || toLower0 != 0 || toLower1 != 0) + return (T[, ]? )CloneAbstractArrayInternal(objFrom, objTo, state, isDeep); + int fromLength0 = objFrom.GetLength(0); + int fromLength1 = objFrom.GetLength(1); + int toLength0 = objTo.GetLength(0); + int toLength1 = objTo.GetLength(1); + int l1 = Math.Min(fromLength0, toLength0); + int l2 = Math.Min(fromLength1, toLength1); + state.AddKnownRef(objFrom, objTo); + bool isValueType = typeof(T).IsValueType(); + bool canReturnSame = FastClonerSafeTypes.CanReturnSameObject(typeof(T)); + bool canShallowCopyValues = !isDeep || canReturnSame; + // 2D arrays are row-major - copy row prefix as one contiguous block + if (canShallowCopyValues && fromLength1 == toLength1) + { + Array.Copy(objFrom, objTo, l1 * fromLength1); + return objTo; + } + + if (!isDeep) + { + for (int i = 0; i < l1; i++) + for (int k = 0; k < l2; k++) + objTo[i, k] = objFrom[i, k]; + return objTo; + } + + if (isValueType) + { + Func? cloner = FastClonerGenerator.GetClonerForValueType(); + if (cloner is not null) + { + for (int i = 0; i < l1; i++) + for (int k = 0; k < l2; k++) + objTo[i, k] = cloner(objFrom[i, k], state); + } + } + else + { + for (int i = 0; i < l1; i++) + for (int k = 0; k < l2; k++) + objTo[i, k] = (T)FastClonerGenerator.CloneClassInternal(objFrom[i, k], state)!; + } + + return objTo; + } + + // rare cases, very slow cloning. currently it's ok + internal static Array? CloneAbstractArrayInternal(Array? objFrom, Array? objTo, FastCloneState state, bool isDeep) + { + // not null from called method, but will check it anyway + if (objFrom == null || objTo == null) + return null; + int rank = objFrom.Rank; + int[] lowerBoundsFrom = new int[rank]; + int[] lowerBoundsTo = new int[rank]; + int[] lengths = new int[rank]; + bool hasZeroLength = false; + for (int i = 0; i < rank; i++) + { + int lowerBoundFrom = objFrom.GetLowerBound(i); + int lowerBoundTo = objTo.GetLowerBound(i); + int length = Math.Min(objFrom.GetLength(i), objTo.GetLength(i)); + lowerBoundsFrom[i] = lowerBoundFrom; + lowerBoundsTo[i] = lowerBoundTo; + lengths[i] = length; + hasZeroLength |= length == 0; + } + + state.AddKnownRef(objFrom, objTo); + // unable to copy any element + if (hasZeroLength) + return objTo; + if (rank == 1) + { + int fromLower = lowerBoundsFrom[0]; + int toLower = lowerBoundsTo[0]; + int len = lengths[0]; + if (isDeep) + { + for (int i = 0; i < len; i++) + objTo.SetValue(FastClonerGenerator.CloneClassInternal(objFrom.GetValue(fromLower + i), state), toLower + i); + } + else + { + for (int i = 0; i < len; i++) + objTo.SetValue(objFrom.GetValue(fromLower + i), toLower + i); + } + + return objTo; + } + + if (rank == 2) + { + int fromLower0 = lowerBoundsFrom[0]; + int fromLower1 = lowerBoundsFrom[1]; + int toLower0 = lowerBoundsTo[0]; + int toLower1 = lowerBoundsTo[1]; + int len0 = lengths[0]; + int len1 = lengths[1]; + if (isDeep) + { + for (int i = 0; i < len0; i++) + { + int fromI = fromLower0 + i; + int toI = toLower0 + i; + for (int k = 0; k < len1; k++) + { + objTo.SetValue(FastClonerGenerator.CloneClassInternal(objFrom.GetValue(fromI, fromLower1 + k), state), toI, toLower1 + k); + } + } + } + else + { + for (int i = 0; i < len0; i++) + { + int fromI = fromLower0 + i; + int toI = toLower0 + i; + for (int k = 0; k < len1; k++) + { + objTo.SetValue(objFrom.GetValue(fromI, fromLower1 + k), toI, toLower1 + k); + } + } + } + + return objTo; + } + + int[] idxesFrom = new int[rank]; + int[] idxesTo = new int[rank]; + Array.Copy(lowerBoundsFrom, idxesFrom, rank); + Array.Copy(lowerBoundsTo, idxesTo, rank); + if (isDeep) + { + while (true) + { + objTo.SetValue(FastClonerGenerator.CloneClassInternal(objFrom.GetValue(idxesFrom), state), idxesTo); + int ofs = rank - 1; + while (true) + { + idxesFrom[ofs]++; + idxesTo[ofs]++; + if (idxesFrom[ofs] >= lowerBoundsFrom[ofs] + lengths[ofs]) + { + idxesFrom[ofs] = lowerBoundsFrom[ofs]; + idxesTo[ofs] = lowerBoundsTo[ofs]; + ofs--; + if (ofs < 0) + return objTo; + } + else + break; + } + } + } + else + { + while (true) + { + objTo.SetValue(objFrom.GetValue(idxesFrom), idxesTo); + int ofs = rank - 1; + while (true) + { + idxesFrom[ofs]++; + idxesTo[ofs]++; + if (idxesFrom[ofs] >= lowerBoundsFrom[ofs] + lengths[ofs]) + { + idxesFrom[ofs] = lowerBoundsFrom[ofs]; + idxesTo[ofs] = lowerBoundsTo[ofs]; + ofs--; + if (ofs < 0) + return objTo; + } + else + break; + } + } + } + } +} diff --git a/src/Foundatio/FastCloner/Code/Extensions.cs b/src/Foundatio/FastCloner/Code/Extensions.cs new file mode 100644 index 000000000..6558e3452 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/Extensions.cs @@ -0,0 +1,8 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +namespace Foundatio.FastCloner.Code; +internal static class Extensions +{ +} diff --git a/src/Foundatio/FastCloner/Code/FastCloneState.cs b/src/Foundatio/FastCloner/Code/FastCloneState.cs new file mode 100644 index 000000000..059b1f52e --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastCloneState.cs @@ -0,0 +1,405 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Runtime.CompilerServices; +using System; + +namespace Foundatio.FastCloner.Code; +internal sealed class FastCloneState +{ + private const int InlineKnownRefCapacity = 4; + private const int MetadataCacheSize = 4; + private const int MaxPoolSize = 16; + [ThreadStatic] + private static FastCloneState? []? _pool; + [ThreadStatic] + private static int _poolCount; + [ThreadStatic] + private static FastCloneState? _simpleState; + public bool TrackReferences { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FastCloneState GetSimpleState() + { + return _simpleState ?? CreateSimpleState(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static FastCloneState CreateSimpleState() + { + FastCloneState state = new FastCloneState(trackReferences: false); + _simpleState = state; + return state; + } + + public static FastCloneState Rent() + { + FastCloneState? []? pool = _pool; + if (pool != null && _poolCount > 0) + { + int index = --_poolCount; + FastCloneState? state = pool[index]; + pool[index] = null; + if (state != null) + return state; + } + + return new FastCloneState(); + } + + public static void Return(FastCloneState state) + { + state.Reset(); + FastCloneState? [] pool = _pool ??= new FastCloneState[MaxPoolSize]; + if (_poolCount < MaxPoolSize) + { + pool[_poolCount++] = state; + } + } + + private void Reset() + { + for (int i = 0; i < idx; i++) + { + baseFromTo[i] = null !; + baseFromTo[i + InlineKnownRefCapacity] = null !; + } + + idx = 0; + if (loops is { } localLoops) + { + if (localLoops.Capacity > 4096) + loops = null; + else + localLoops.Clear(); + } + + workCount = 0; + UseWorkList = false; + callDepth = 0; + metadataTypes[0] = null; + metadataTypes[1] = null; + metadataTypes[2] = null; + metadataTypes[3] = null; + metadataWriteIndex = 0; + } + + private FastCloneState(bool trackReferences = true) + { + TrackReferences = trackReferences; + } + + private MiniDictionary? loops; + private readonly object[] baseFromTo = new object[InlineKnownRefCapacity * 2]; + private int idx; + private WorkItem[]? workItems; + private int workCount; + public bool UseWorkList { get; set; } + + private int callDepth; + internal int CurrentDepth => callDepth; + + private readonly Type? [] metadataTypes = new Type? [MetadataCacheSize]; + private readonly FastClonerCache.TypeCloneMetadata[] metadataValues = new FastClonerCache.TypeCloneMetadata[MetadataCacheSize]; + private int metadataWriteIndex; + private readonly struct WorkItem(object from, object to, Type type) + { + public readonly object From = from; + public readonly object To = to; + public readonly Type Type = type; + } + + public object? GetKnownRef(object from) + { + if (!TrackReferences) + return null; + if (idx > 0 && ReferenceEquals(from, baseFromTo[0])) + return baseFromTo[InlineKnownRefCapacity]; + if (idx > 1 && ReferenceEquals(from, baseFromTo[1])) + return baseFromTo[InlineKnownRefCapacity + 1]; + if (idx > 2 && ReferenceEquals(from, baseFromTo[2])) + return baseFromTo[InlineKnownRefCapacity + 2]; + if (idx > 3 && ReferenceEquals(from, baseFromTo[3])) + return baseFromTo[InlineKnownRefCapacity + 3]; + return loops?.FindEntry(from); + } + + public void AddKnownRef(object from, object to) + { + if (!TrackReferences) + return; + if (idx < InlineKnownRefCapacity) + { + baseFromTo[idx] = from; + baseFromTo[idx + InlineKnownRefCapacity] = to; + idx++; + return; + } + + loops ??= new MiniDictionary(); + loops.Insert(from, to); + } + + public void EnsureKnownRefCapacity(int totalExpectedReferences) + { + if (!TrackReferences || totalExpectedReferences <= InlineKnownRefCapacity) + return; + int expectedInDictionary = totalExpectedReferences - InlineKnownRefCapacity; + if (expectedInDictionary <= 0) + return; + if (loops is null) + { + loops = new MiniDictionary(expectedInDictionary); + return; + } + + loops.EnsureCapacity(expectedInDictionary); + } + + public void EnqueueProcess(object from, object to, Type type) + { + if (!TrackReferences) + return; + WorkItem[] local = workItems ??= new WorkItem[16]; + if (workCount == local.Length) + { + int newSize = local.Length * 2; + WorkItem[] resized = new WorkItem[newSize]; + Array.Copy(local, resized, workCount); + workItems = local = resized; + } + + local[workCount++] = new WorkItem(from, to, type); + } + + public void EnsureWorkQueueCapacity(int expectedAdditionalItems) + { + if (!TrackReferences || expectedAdditionalItems <= 0) + return; + int needed = workCount + expectedAdditionalItems; + WorkItem[] local = workItems ??= new WorkItem[Math.Max(16, expectedAdditionalItems)]; + if (needed <= local.Length) + return; + int newSize = local.Length; + while (newSize < needed) + newSize *= 2; + WorkItem[] resized = new WorkItem[newSize]; + Array.Copy(local, resized, workCount); + workItems = resized; + } + + public bool TryPop(out object from, out object to, out Type type) + { + if (!TrackReferences) + { + from = null !; + to = null !; + type = null !; + return false; + } + + if (workCount == 0) + { + from = null !; + to = null !; + type = null !; + return false; + } + + WorkItem wi = workItems![--workCount]; + from = wi.From; + to = wi.To; + type = wi.Type; + return true; + } + + public int IncrementDepth() => ++callDepth; + public void DecrementDepth() + { + if (callDepth > 0) + callDepth--; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetCachedTypeMetadata(Type type, out FastClonerCache.TypeCloneMetadata metadata) + { + if (ReferenceEquals(type, metadataTypes[0])) + { + metadata = metadataValues[0]; + return true; + } + + if (ReferenceEquals(type, metadataTypes[1])) + { + metadata = metadataValues[1]; + return true; + } + + if (ReferenceEquals(type, metadataTypes[2])) + { + metadata = metadataValues[2]; + return true; + } + + if (ReferenceEquals(type, metadataTypes[3])) + { + metadata = metadataValues[3]; + return true; + } + + metadata = null !; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CacheTypeMetadata(Type type, FastClonerCache.TypeCloneMetadata metadata) + { + if (ReferenceEquals(type, metadataTypes[0])) + { + metadataValues[0] = metadata; + return; + } + + if (ReferenceEquals(type, metadataTypes[1])) + { + metadataValues[1] = metadata; + return; + } + + if (ReferenceEquals(type, metadataTypes[2])) + { + metadataValues[2] = metadata; + return; + } + + if (ReferenceEquals(type, metadataTypes[3])) + { + metadataValues[3] = metadata; + return; + } + + int index = metadataWriteIndex; + metadataTypes[index] = type; + metadataValues[index] = metadata; + metadataWriteIndex = (index + 1) & (MetadataCacheSize - 1); + } + + private sealed class MiniDictionary + { + private struct Entry(int hashCode, int next, object key, object value) + { + public readonly int HashCode = hashCode; + public int Next = next; + public readonly object Key = key; + public readonly object Value = value; + } + + private const int DefaultCapacity = 8; + private int[] buckets; + private Entry[] entries; + private int count; + private int bucketMask; + public int Capacity => entries.Length; + + public MiniDictionary() : this(DefaultCapacity) + { + } + + public MiniDictionary(int capacity) + { + int size = RoundUpToPowerOf2(capacity < DefaultCapacity ? DefaultCapacity : capacity); + Initialize(size); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundUpToPowerOf2(int value) + { + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; + } + + public object? FindEntry(object key) + { + int hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; + int bucketIndex = hashCode & bucketMask; + Entry[] entriesLocal = entries; + for (int i = buckets[bucketIndex]; i >= 0; i = entriesLocal[i].Next) + { + ref readonly Entry entry = ref entriesLocal[i]; + if (entry.HashCode == hashCode && ReferenceEquals(entry.Key, key)) + return entry.Value; + } + + return null; + } + + private void Initialize(int size) + { + buckets = new int[size]; + Array.Fill(buckets, -1); + entries = new Entry[size]; + bucketMask = size - 1; + } + + public void Clear() + { + if (count == 0) + return; + Array.Clear(entries, 0, count); + Array.Fill(buckets, -1); + count = 0; + } + + public void EnsureCapacity(int capacity) + { + if (capacity <= entries.Length) + return; + Resize(RoundUpToPowerOf2(capacity)); + } + + public void Insert(object key, object value) + { + int hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; + int[] localBuckets = buckets; + Entry[] localEntries = entries; + if (count == localEntries.Length) + { + Resize(); + localBuckets = buckets; + localEntries = entries; + } + + int targetBucket = hashCode & bucketMask; + int index = count++; + localEntries[index] = new Entry(hashCode, localBuckets[targetBucket], key, value); + localBuckets[targetBucket] = index; + } + + private void Resize() => Resize(entries.Length * 2); + private void Resize(int newSize) + { + int[] newBuckets = new int[newSize]; + Array.Fill(newBuckets, -1); + Entry[] newEntries = new Entry[newSize]; + Array.Copy(entries, newEntries, count); + int newMask = newSize - 1; + for (int i = 0; i < count; i++) + { + ref Entry entry = ref newEntries[i]; + int bucket = entry.HashCode & newMask; + entry.Next = newBuckets[bucket]; + newBuckets[bucket] = i; + } + + buckets = newBuckets; + entries = newEntries; + bucketMask = newMask; + } + } +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerBehaviorAttribute.cs b/src/Foundatio/FastCloner/Code/FastClonerBehaviorAttribute.cs new file mode 100644 index 000000000..6c5eb3672 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerBehaviorAttribute.cs @@ -0,0 +1,34 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System; + +namespace Foundatio.FastCloner.Code; +/// +/// Specifies the cloning behavior for a member (field, property, event) or an entire type (class, struct, record). +/// When applied to a type, all members of that type will use this behavior unless overridden at the member level. +/// This is the base attribute for configuring clone behavior. +/// +/// +/// +/// // Type-level: all usages of SharedService will preserve reference +/// [FastClonerBehavior(CloneBehavior.Reference)] +/// public class SharedService { } +/// +/// // Member-level: override behavior for specific members +/// public class MyClass +/// { +/// [FastClonerBehavior(CloneBehavior.Ignore)] +/// public CancellationToken Token { get; set; } // Set to default +/// } +/// +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] +internal class FastClonerBehaviorAttribute(CloneBehavior behavior) : Attribute +{ + /// + /// Gets the cloning behavior. + /// + public CloneBehavior Behavior { get; } = behavior; +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerCache.cs b/src/Foundatio/FastCloner/Code/FastClonerCache.cs new file mode 100644 index 000000000..1c9be4116 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerCache.cs @@ -0,0 +1,338 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System; + +namespace Foundatio.FastCloner.Code; +/// +/// Specifies how a type should be handled during cloning. +/// +internal enum CloneBehavior +{ + /// + /// Perform deep cloning (default behavior). + /// + Clone, + /// + /// Return the same instance without cloning (for immutable/safe types). + /// + Reference, + /// + /// Perform shallow cloning (MemberwiseClone). + /// + Shallow, + /// + /// Skip cloning, return default. + /// + Ignore +} + +/// +/// Generic static cache that leverages JIT specialization to avoid dictionary lookups +/// when the compile-time type T matches the runtime type. +/// +internal static class ClonerCache +{ + internal readonly struct CacheEntry(Func? cloner, bool isSafe, bool canUseNoTrackingState, FastClonerCache.TypeCloneMetadata? metadata, long version) + { + public Func? Cloner { get; } = cloner; + public bool IsSafe { get; } = isSafe; + public bool CanUseNoTrackingState { get; } = canUseNoTrackingState; + public FastClonerCache.TypeCloneMetadata? Metadata { get; } = metadata; + public long Version { get; } = version; + } + +#if NET10_0_OR_GREATER + private static readonly System.Threading.Lock sync = new System.Threading.Lock(); +#else + private static readonly object sync = new object (); +#endif + private static Func? cloner; + private static bool isSafe; + private static bool canUseNoTrackingState; + private static FastClonerCache.TypeCloneMetadata? metadata; + private static long version = -1; + public static CacheEntry GetCurrent() + { + long currentVersion = FastClonerCache.GetCacheVersion(); + if (Volatile.Read(ref version) != currentVersion) + { + lock (sync) + { + if (version != currentVersion) + { + Refresh(currentVersion); + } + } + } + + return new CacheEntry(cloner, isSafe, canUseNoTrackingState, metadata, currentVersion); + } + + private static void Refresh(long currentVersion) + { + Type type = typeof(T); + FastClonerCache.TypeCloneMetadata typeMetadata = FastClonerGenerator.GetTypeMetadata(type); + bool computedIsSafe = FastClonerSafeTypes.CanReturnSameObject(type); + bool computedCanUseNoTrackingState = !type.IsValueType && typeMetadata is { CyclePolicy: FastClonerCache.CyclePolicy.None, HasBehaviorSensitiveMembers: false } && !FastClonerCache.HasActiveTypeBehaviorOverrides; + Func? computedCloner = null; + if (!computedIsSafe) + { + object? clonerObj = FastClonerExprGenerator.GenerateClonerInternal(type, type.IsValueType); + if (type.IsValueType) + { + computedCloner = clonerObj as Func; + } + else + { + if (clonerObj is Func objectCloner) + { + computedCloner = (obj, state) => (T)objectCloner(obj!, state); + } + } + } + + cloner = computedCloner; + isSafe = computedIsSafe; + canUseNoTrackingState = computedCanUseNoTrackingState; + metadata = typeMetadata; + Volatile.Write(ref version, currentVersion); + } +} + +internal static class FastClonerCache +{ + private static long cacheVersion = 1; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static long GetCacheVersion() => Interlocked.Read(ref cacheVersion); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static long BumpCacheVersion() => Interlocked.Increment(ref cacheVersion); + internal enum CollectionCloneStrategy + { + None = 0, + MemberwiseFast = 1, + SpecializedRebuild = 2, + Hybrid = 3 + } + + internal enum CloneExecutionMode + { + SafeReturn = 0, + MemberwiseThenPatch = 1, + RebuildCollection = 2 + } + + internal enum CyclePolicy + { + None = 0, + TrackReferences = 1, + Worklist = 2 + } + + internal sealed class TypeCloneMetadata + { + public Type Type { get; set; } = typeof(object); + public bool IsSafe { get; set; } + public bool CanHaveCycles { get; set; } + public bool CanSkipReferenceTracking { get; set; } + public bool HasDirectSelfReference { get; set; } + public bool HasBehaviorSensitiveMembers { get; set; } + public bool RequiresSpecializedCloner { get; set; } + public CollectionCloneStrategy CollectionStrategy { get; set; } + public CloneExecutionMode ExecutionMode { get; set; } + public CyclePolicy CyclePolicy { get; set; } + public Func? RecursiveCloner { get; set; } + } + + internal sealed class TypeShape + { + public MemberInfo[] Members { get; init; } = []; + public Dictionary? IgnoredEventDetails { get; init; } + public Type[] CycleFieldTypes { get; init; } = []; + public bool HasReadonlyFields { get; init; } + public bool ContainsIgnoredMembers { get; init; } + public bool HasDirectSelfReference { get; init; } + } + + internal static readonly ConcurrentDictionary TypeBehaviors = []; + internal static volatile bool HasTypeBehaviorOverrides; + internal static volatile bool HasActiveTypeBehaviorOverrides; + internal static bool IsTypeIgnored(Type type) + { + return HasActiveTypeBehaviorOverrides && TypeBehaviors.TryGetValue(type, out CloneBehavior behavior) && behavior == CloneBehavior.Ignore; + } + + internal static volatile bool HasSafeTypeOverrides; + internal static bool IsTypeReference(Type type) + { + return HasActiveTypeBehaviorOverrides && TypeBehaviors.TryGetValue(type, out CloneBehavior behavior) && behavior == CloneBehavior.Reference; + } + + internal static CloneBehavior? GetTypeBehavior(Type type) + { + return TypeBehaviors.TryGetValue(type, out CloneBehavior behavior) ? behavior : null; + } + + internal static void RecalculateTypeBehaviorState() + { + HasTypeBehaviorOverrides = !TypeBehaviors.IsEmpty; + HasActiveTypeBehaviorOverrides = HasTypeBehaviorOverrides && !FastCloner.DisableOptionalFeatures; + HasSafeTypeOverrides = CalculateHasSafeTypeOverrides(); + } + + private static bool CalculateHasSafeTypeOverrides() + { + if (!HasTypeBehaviorOverrides) + return false; + foreach (KeyValuePair kvp in TypeBehaviors) + { + if (kvp.Value == CloneBehavior.Ignore && FastClonerSafeTypes.DefaultKnownTypes.ContainsKey(kvp.Key)) + return true; + } + + return false; + } + + private static readonly ClrCache classCache = new ClrCache(); + private static readonly ClrCache typeMetadataCache = new ClrCache(); + private static readonly ClrCache typeShapeCache = new ClrCache(); + private static readonly ClrCache structCache = new ClrCache(); + private static readonly ClrCache deepClassToCache = new ClrCache(); + private static readonly ClrCache shallowClassToCache = new ClrCache(); + private static readonly ConcurrentLazyCache typeConvertCache = new ConcurrentLazyCache(); + private static readonly GenericClrCache fieldCache = new GenericClrCache(); + private static readonly GenericClrCache memberBehaviorCache = new GenericClrCache(); + private static readonly ClrCache attributedTypeBehaviorCache = new ClrCache(); + private static readonly ClrCache immutableCollectionStatusCache = new ClrCache(); + private static readonly ClrCache specialTypesCache = new ClrCache(); + private static readonly ClrCache isTypeSafeHandleCache = new ClrCache(); + private static readonly ClrCache anonymousTypeStatusCache = new ClrCache(); + private static readonly ClrCache stableHashSemanticsCache = new ClrCache(); + private static readonly ClrCache canHaveCyclesCache = new ClrCache(); + private static readonly ClrCache valueTypeContainsReferencesCache = new ClrCache(); + private static readonly ClrCache collectionPayloadTypeCache = new ClrCache(); + private static readonly ClrCache compilerGeneratedTypeCache = new ClrCache(); + public static object? GetOrAddField(Type type, string name, Func valueFactory) => fieldCache.GetOrAdd(new TypeNameKey(type, name), k => valueFactory(k.Type)); + public static object? GetOrAddClass(Type type, Func valueFactory) => classCache.GetOrAdd(type, valueFactory); + public static TypeCloneMetadata GetOrAddTypeMetadata(Type type, Func valueFactory) => typeMetadataCache.GetOrAdd(type, valueFactory); + public static TypeShape GetOrAddTypeShape(Type type, Func valueFactory) => typeShapeCache.GetOrAdd(type, valueFactory); + public static object? GetOrAddStructAsObject(Type type, Func valueFactory) => structCache.GetOrAdd(type, valueFactory); + public static object GetOrAddDeepClassTo(Type type, Func valueFactory) => deepClassToCache.GetOrAdd(type, valueFactory); + public static object GetOrAddShallowClassTo(Type type, Func valueFactory) => shallowClassToCache.GetOrAdd(type, valueFactory); + public static T GetOrAddConvertor(Type from, Type to, Func valueFactory) => (T)typeConvertCache.GetOrAdd(from, to, (f, t) => valueFactory(f, t)); + public static CloneBehavior? GetOrAddMemberBehavior(MemberInfo memberInfo, Func valueFactory) => memberBehaviorCache.GetOrAdd(memberInfo, valueFactory); + public static CloneBehavior? GetOrAddAttributedTypeBehavior(Type type, Func valueFactory) => attributedTypeBehaviorCache.GetOrAdd(type, valueFactory); + public static bool GetOrAddImmutableCollectionStatus(Type type, Func valueFactory) => immutableCollectionStatusCache.GetOrAdd(type, valueFactory); + public static object GetOrAddSpecialType(Type type, Func valueFactory) => specialTypesCache.GetOrAdd(type, valueFactory); + public static bool GetOrAddIsTypeSafeHandle(Type type, Func valueFactory) => isTypeSafeHandleCache.GetOrAdd(type, valueFactory); + public static bool GetOrAddAnonymousTypeStatus(Type type, Func valueFactory) => anonymousTypeStatusCache.GetOrAdd(type, valueFactory); + public static bool GetOrAddStableHashSemantics(Type type, Func valueFactory) => stableHashSemanticsCache.GetOrAdd(type, valueFactory); + public static bool GetOrAddCanHaveCycles(Type type, Func valueFactory) => canHaveCyclesCache.GetOrAdd(type, valueFactory); + public static bool GetOrAddValueTypeContainsReferences(Type type, Func valueFactory) => valueTypeContainsReferencesCache.GetOrAdd(type, valueFactory); + public static Type? GetOrAddCollectionPayloadType(Type type, Func valueFactory) => collectionPayloadTypeCache.GetOrAdd(type, valueFactory); + public static bool GetOrAddCompilerGeneratedType(Type type, Func valueFactory) => compilerGeneratedTypeCache.GetOrAdd(type, valueFactory); + /// + /// Clears the FastCloner cached reflection metadata. + /// + public static void ClearCache() + { + classCache.Clear(); + typeMetadataCache.Clear(); + typeShapeCache.Clear(); + structCache.Clear(); + deepClassToCache.Clear(); + shallowClassToCache.Clear(); + typeConvertCache.Clear(); + fieldCache.Clear(); + memberBehaviorCache.Clear(); + attributedTypeBehaviorCache.Clear(); + immutableCollectionStatusCache.Clear(); + specialTypesCache.Clear(); + isTypeSafeHandleCache.Clear(); + anonymousTypeStatusCache.Clear(); + stableHashSemanticsCache.Clear(); + canHaveCyclesCache.Clear(); + valueTypeContainsReferencesCache.Clear(); + collectionPayloadTypeCache.Clear(); + compilerGeneratedTypeCache.Clear(); + BumpCacheVersion(); + } + + private sealed class ClrCache + { + private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); + public TValue GetOrAdd(Type type, Func valueFactory) + { + IntPtr handle = type.TypeHandle.Value; + return cache.TryGetValue(handle, out TValue? existing) ? existing : cache.GetOrAdd(handle, _ => valueFactory(type)); + } + + public void Clear() => cache.Clear(); + } + + private sealed class GenericClrCache + where TKey : notnull + { + private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); + public TValue GetOrAdd(TKey key, Func valueFactory) + { + return cache.GetOrAdd(key, valueFactory); + } + + public void Clear() => cache.Clear(); + } + + private readonly struct TypeNameKey(Type type, string name) : IEquatable + { + public Type Type { get; } = type; + public string Name { get; } = name; + + public bool Equals(TypeNameKey other) + { + return ReferenceEquals(Type, other.Type) && Name == other.Name; + } + + public override bool Equals(object? obj) + { + return obj is TypeNameKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (RuntimeHelpers.GetHashCode(Type) * 397) ^ Name.GetHashCode(); + } + } + } + + private sealed class ConcurrentLazyCache + { +#if true + private readonly ConcurrentDictionary<(IntPtr, IntPtr), TValue> cache = new ConcurrentDictionary<(IntPtr, IntPtr), TValue>(); + public TValue GetOrAdd(Type from, Type to, Func valueFactory) + { + (IntPtr, IntPtr) key = (from.TypeHandle.Value, to.TypeHandle.Value); + return cache.GetOrAdd(key, _ => valueFactory(from, to)); + } + + public void Clear() => cache.Clear(); +#else + private readonly ConcurrentDictionary, TValue> cache = new ConcurrentDictionary, TValue>(); + + public TValue GetOrAdd(Type from, Type to, Func valueFactory) + { + Tuple key = Tuple.Create(from.TypeHandle.Value, to.TypeHandle.Value); + return cache.TryGetValue(key, out TValue? cached) ? cached : cache.GetOrAdd(key, _ => valueFactory(from, to)); + } + + public void Clear() => cache.Clear(); +#endif + } +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerExprGenerator.cs b/src/Foundatio/FastCloner/Code/FastClonerExprGenerator.cs new file mode 100644 index 000000000..3ec3bfbfc --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerExprGenerator.cs @@ -0,0 +1,1621 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.ObjectModel; +using System.Dynamic; +using System.Linq.Expressions; +using System.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; + +namespace Foundatio.FastCloner.Code; +internal static class FastClonerExprGenerator +{ + private const int AdaptiveDictionaryRebuildThreshold = 32; + internal static readonly ConcurrentDictionary> CustomTypeHandlers = []; + private static readonly ConcurrentDictionary readonlyFields = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary> adaptiveDictionaryFactoryCache = new ConcurrentDictionary>(); + private static readonly MethodInfo fieldSetMethod; + private static readonly Lazy isTypeIgnoredMethodInfo = new Lazy(() => typeof(FastClonerCache).GetMethod(nameof(FastClonerCache.IsTypeIgnored), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, [typeof(Type)], null)!, LazyThreadSafetyMode.ExecutionAndPublication); + internal static MethodInfo IsTypeIgnoredMethodInfo => isTypeIgnoredMethodInfo.Value; + + static FastClonerExprGenerator() + { + fieldSetMethod = typeof(FieldInfo).GetMethod(nameof(FieldInfo.SetValue), [typeof(object), typeof(object)])!; + } + + private static MethodInfo GetClassCloneMethod(Type memberType, bool useShallowClassClone, bool skipCycleTracking) + { + if (useShallowClassClone) + return StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassShallowAndTrack; + return skipCycleTracking ? StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassInternalNoTracking : StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassInternal; + } + + private static MethodInfo GetDefaultCloneMethod(Type memberType) + { + return memberType.IsValueType() ? StaticMethodInfos.DeepClonerGeneratorMethods.MakeStructCloneMethodInfo(memberType) : StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassInternal; + } + + internal static object? GenerateClonerInternal(Type realType, bool asObject, bool skipCycleTracking = false, bool useShallowClassClone = false) => GenerateProcessMethod(realType, asObject && realType.IsValueType(), new ExpressionPosition(0, 0), skipCycleTracking, useShallowClassClone); + private static bool MemberIsIgnored(MemberInfo memberInfo) + { + return GetMemberBehavior(memberInfo) == CloneBehavior.Ignore; + } + + internal static bool MemberIsShallow(MemberInfo memberInfo) + { + return GetMemberBehavior(memberInfo) == CloneBehavior.Shallow; + } + + /// + /// Returns true if the member should have its reference copied directly without deep cloning. + /// This applies to both Shallow and Reference behaviors. + /// + internal static bool MemberShouldCopyReference(MemberInfo memberInfo) + { + CloneBehavior? behavior = GetMemberBehavior(memberInfo); + return behavior is CloneBehavior.Shallow or CloneBehavior.Reference; + } + + /// + /// Gets the clone behavior for a type by checking for FastClonerBehaviorAttribute on the type definition. + /// + internal static CloneBehavior? GetTypeBehavior(Type type) + { + if (FastCloner.DisableOptionalFeatures) + return null; + return FastClonerCache.GetOrAddAttributedTypeBehavior(type, static t => + { + FastClonerBehaviorAttribute? behaviorAttr = t.GetCustomAttribute(); + return behaviorAttr?.Behavior; + }); + } + + /// + /// Gets the clone behavior for a member by checking: + /// 1. Member-level FastClonerBehaviorAttribute (highest priority) + /// 2. [NonSerialized] attribute (treat as Ignore) + /// 3. Backing field's corresponding property + /// 4. Type-level FastClonerBehaviorAttribute on the member's type (lowest priority) + /// + internal static CloneBehavior? GetMemberBehavior(MemberInfo memberInfo) + { + if (FastCloner.DisableOptionalFeatures) + return null; + return FastClonerCache.GetOrAddMemberBehavior(memberInfo, mi => + { + // 1. Check for member-level FastClonerBehaviorAttribute (base class covers all derived attributes) + FastClonerBehaviorAttribute? behaviorAttr = mi.GetCustomAttribute(); + if (behaviorAttr is not null) + return behaviorAttr.Behavior; + // 2. Check [NonSerialized] (treat as Ignore) + NonSerializedAttribute? nonSerialized = mi.GetCustomAttribute(); + if (nonSerialized is not null) + return CloneBehavior.Ignore; + // 3. For backing fields of auto-implemented properties, check the corresponding property + // Backing fields are named like "k__BackingField" + if (mi is FieldInfo field && field.Name.StartsWith("<") && field.Name.EndsWith(">k__BackingField")) + { + string propertyName = field.Name.Substring(1, field.Name.Length - ">k__BackingField".Length - 1); + // Use DeclaredOnly to avoid AmbiguousMatchException when property is hidden in derived class + PropertyInfo? property = field.DeclaringType?.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (property != null) + { + FastClonerBehaviorAttribute? propBehavior = property.GetCustomAttribute(); + if (propBehavior is not null) + return propBehavior.Behavior; + } + } + + // 4. Check for type-level attribute on the member's type + Type? memberType = mi switch + { + FieldInfo f => f.FieldType, + PropertyInfo p => p.PropertyType, + EventInfo e => e.EventHandlerType, + _ => null + }; + if (memberType != null) + { + CloneBehavior? typeBehavior = GetTypeBehavior(memberType); + if (typeBehavior.HasValue) + return typeBehavior.Value; + } + + return null; + }); + } + + internal static FastClonerCache.TypeShape GetTypeShape(Type type) + { + return FastClonerCache.GetOrAddTypeShape(type, BuildTypeShape); + } + + internal static bool CalculateTypeContainsIgnoredMembers(Type type) + { + return GetTypeShape(type).ContainsIgnoredMembers; + } + + private static bool TypeHasReadonlyFields(Type type) + { + return GetTypeShape(type).HasReadonlyFields; + } + + private static void AddStructReadonlyFieldsCloneExpressions(List expressionList, ParameterExpression fromLocal, ParameterExpression boxedToLocal, ParameterExpression state, Type type, bool useShallowClassClone = false, bool skipCycleTracking = false) + { + FastClonerCache.TypeShape typeShape = GetTypeShape(type); + MemberInfo[] members = typeShape.Members; + Dictionary? ignoredEventDetails = typeShape.IgnoredEventDetails; + foreach (MemberInfo member in members) + { + if (member is not FieldInfo fieldInfo || !fieldInfo.IsInitOnly) + { + continue; + } + + Type memberType = fieldInfo.FieldType; + bool shouldBeIgnored = false; + if (MemberIsIgnored(member)) + { + shouldBeIgnored = true; + } + else if (ignoredEventDetails is not null && ignoredEventDetails.TryGetValue(fieldInfo.Name, out Type? evtType)) + { + if (evtType == memberType) + { + shouldBeIgnored = true; + } + } + else if (FastClonerCache.IsTypeIgnored(memberType)) + { + shouldBeIgnored = true; + } + + if (shouldBeIgnored) + { + expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), fieldSetMethod, boxedToLocal, Expression.Convert(Expression.Default(memberType), typeof(object)))); + continue; + } + + if (FastClonerSafeTypes.CanReturnSameObject(memberType)) + { + continue; + } + + // Check if member should copy reference directly (Shallow or Reference behavior) + if (MemberShouldCopyReference(member)) + { + // For shallow/reference clone, just copy the reference/value directly + Expression originalValue = Expression.MakeMemberAccess(fromLocal, member); + expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), fieldSetMethod, boxedToLocal, Expression.Convert(originalValue, typeof(object)))); + continue; + } + + Expression originalMemberValue = Expression.MakeMemberAccess(fromLocal, member); + Expression clonedValueExpression; + if (memberType.IsValueType()) + { + MethodInfo structClone = StaticMethodInfos.DeepClonerGeneratorMethods.MakeStructCloneMethodInfo(memberType); + clonedValueExpression = Expression.Call(structClone, originalMemberValue, state); + } + else + { + MethodInfo classCloneMethod = GetClassCloneMethod(memberType, useShallowClassClone, skipCycleTracking); + if (!useShallowClassClone && !skipCycleTracking) + { + Expression deepCall = Expression.Call(classCloneMethod, originalMemberValue, state); + Expression shallowCall = Expression.Call(StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassShallowAndTrack, originalMemberValue, state); + Expression selected = Expression.Condition(Expression.Property(state, StaticMethodInfos.DeepCloneStateProperties.UseWorkList), shallowCall, deepCall); + clonedValueExpression = Expression.Convert(selected, memberType); + } + else + { + Expression call = Expression.Call(classCloneMethod, originalMemberValue, state); + clonedValueExpression = Expression.Convert(call, memberType); + } + } + + expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), fieldSetMethod, boxedToLocal, Expression.Convert(clonedValueExpression, typeof(object)))); + } + } + + internal static void ForceSetField(FieldInfo field, object obj, object value) + { + field.SetValue(obj, value); + } + + internal readonly record struct ExpressionPosition(int Depth, int Index) + { + public ExpressionPosition Next() => this with + { + Index = Index + 1 + }; + } + + private static LabelTarget CreateLoopLabel(ExpressionPosition position) + { + string str = $"Loop_{position.Depth}_{position.Index}"; + return Expression.Label(str); + } + + private static bool ShouldDeepCloneStructReadonlyFields(Type type) + { + if (type.Namespace == null) + { + return true; + } + + if (readonlyStructSafeHandleNamespaces.ContainsAnyPattern(type.Namespace + ".")) + { + return false; + } + + return !FastClonerCache.GetOrAddIsTypeSafeHandle(type, t => t.GetCustomAttribute() != null); + } + + internal static object? GenerateProcessMethod(Type realType, bool asObject) => GenerateProcessMethod(realType, asObject && realType.IsValueType(), new ExpressionPosition(0, 0)); + public static bool IsListType(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); + public static bool IsSetType(Type type) => type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)); + public static bool IsConcurrentBagOrQueue(Type type) + { + if (!type.IsGenericType) + return false; + if (typeof(ConcurrentBag<>).IsAssignableFrom(type)) + return true; + Type open = type.GetGenericTypeDefinition(); + return open == typeof(ConcurrentBag<>) || open == typeof(ConcurrentQueue<>); + } + + public static bool IsDictionaryType(Type type) => typeof(IDictionary).IsAssignableFrom(type) || type.GetInterfaces().Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<, >) || i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<, >))); + private readonly struct ConstructorInfoEx + { + public ConstructorInfo Constructor { get; } + public int ParameterCount { get; } + public bool HasOptionalParameters { get; } + + public ConstructorInfoEx(ConstructorInfo constructor) + { + Constructor = constructor; + ParameterInfo[] parameters = constructor.GetParameters(); + ParameterCount = parameters.Length; + HasOptionalParameters = ParameterCount > 0 && parameters.All(p => p.HasDefaultValue); + } + } + + private static ConstructorInfoEx? FindCallableConstructor(Type type) + { + // take parameterless constructor + ConstructorInfo? ctor = type.GetConstructor(Type.EmptyTypes); + return ctor != null ? new ConstructorInfoEx(ctor) : null; + // using any other constructor that can be called without arguments increases chances we trigger side effects + // we fall back to memberwise cloning instead + // ctor = type.GetConstructors().FirstOrDefault(c => c.GetParameters().All(p => p.HasDefaultValue)); + // return ctor != null ? new ConstructorInfoEx(ctor) : null; + } + + /// + /// Finds a constructor that takes an int capacity parameter. + /// This is used to preallocate collections for better performance. + /// + private static ConstructorInfo? FindCapacityConstructor(Type type) + { + return type.GetConstructor([typeof(int)]); + } + + private static NewExpression CreateNewExpressionWithCtor(ConstructorInfoEx ctorInfoEx) + { + if (ctorInfoEx.ParameterCount == 0) + { + return Expression.New(ctorInfoEx.Constructor); + } + + // For constructors with optional parameters, create default values + Expression[] arguments = ctorInfoEx.Constructor.GetParameters().Select(p => Expression.Constant(p.HasDefaultValue ? p.DefaultValue : GetDefaultValue(p.ParameterType), p.ParameterType)).ToArray(); + return Expression.New(ctorInfoEx.Constructor, arguments); + } + + private static object? GetDefaultValue(Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + + private static void AddMemberCloneExpressions(List expressionList, ParameterExpression fromLocal, ParameterExpression toLocal, ParameterExpression state, Type type, bool skipReadonly = false, bool useShallowClassClone = false, bool skipCycleTracking = false) + { + ExpressionPosition currentPosition = new ExpressionPosition(0, 0); + FastClonerCache.TypeShape typeShape = GetTypeShape(type); + MemberInfo[] members = typeShape.Members; + Dictionary? ignoredEventDetails = typeShape.IgnoredEventDetails; + foreach (MemberInfo member in members) + { + Type memberType = member switch + { + FieldInfo fi => fi.FieldType, + PropertyInfo pi => pi.PropertyType, + _ => throw new ArgumentException($"Unsupported member type: {member.GetType()}")}; + bool canAssignDirect = member switch + { + FieldInfo fi => !fi.IsInitOnly, + PropertyInfo pi => pi.CanWrite, + _ => false + }; + if (MemberIsIgnored(member)) + { + if (canAssignDirect) + { + expressionList.Add(Expression.Assign(Expression.MakeMemberAccess(toLocal, member), Expression.Default(memberType))); + } + + continue; + } + + if (member is FieldInfo fieldInfoForEventCheck && ignoredEventDetails is not null && ignoredEventDetails.TryGetValue(fieldInfoForEventCheck.Name, out Type? evtType)) + { + if (evtType == memberType) + { + if (canAssignDirect) + { + expressionList.Add(Expression.Assign(Expression.MakeMemberAccess(toLocal, member), Expression.Default(memberType))); + } + + continue; + } + } + + if (FastClonerCache.IsTypeIgnored(memberType)) + { + if (canAssignDirect) + { + expressionList.Add(Expression.Assign(Expression.MakeMemberAccess(toLocal, member), Expression.Default(memberType))); + } + + continue; + } + + if (member is PropertyInfo piLocal) + { + if (piLocal.CanWrite && MemberIsIgnored(piLocal)) + { + expressionList.Add(Expression.Assign(Expression.Property(toLocal, piLocal), Expression.Default(piLocal.PropertyType))); + } + + continue; + } + + if (!FastClonerSafeTypes.CanReturnSameObject(memberType)) + { + bool shouldBeIgnored = false; + if (MemberIsIgnored(member)) + { + shouldBeIgnored = true; + } + else if (member is FieldInfo fi) + { + if (ignoredEventDetails is not null && ignoredEventDetails.TryGetValue(fi.Name, out Type? eventHandlerTypeFromCache)) + { + if (eventHandlerTypeFromCache == fi.FieldType) + { + shouldBeIgnored = true; + } + } + } + + if (shouldBeIgnored) + { + if (canAssignDirect) + { + expressionList.Add(Expression.Assign(Expression.MakeMemberAccess(toLocal, member), Expression.Default(memberType))); + } + + continue; + } + + // Check if member should copy reference directly (Shallow or Reference behavior) + if (MemberShouldCopyReference(member)) + { + MemberExpression shallowSourceValue = Expression.MakeMemberAccess(fromLocal, member); + switch (member) + { + case FieldInfo fieldInfo: + { + bool isReadonly = readonlyFields.GetOrAdd(fieldInfo, f => f.IsInitOnly); + if (isReadonly) + { + if (!skipReadonly) + { + expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), fieldSetMethod, Expression.Convert(toLocal, typeof(object)), Expression.Convert(shallowSourceValue, typeof(object)))); + } + } + else + { + expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), shallowSourceValue)); + } + + break; + } + + case PropertyInfo { CanWrite: true }: + { + expressionList.Add(Expression.Assign(Expression.MakeMemberAccess(toLocal, member), shallowSourceValue)); + break; + } + } + + continue; + } + + Expression clonedValueExpression; + MemberExpression getMemberValue = Expression.MakeMemberAccess(fromLocal, member); + Expression originalMemberValue = getMemberValue; + if (memberType.IsValueType()) + { + MethodInfo structClone = StaticMethodInfos.DeepClonerGeneratorMethods.MakeStructCloneMethodInfo(memberType); + clonedValueExpression = Expression.Call(structClone, originalMemberValue, state); + } + else + { + MethodInfo classCloneMethod = GetClassCloneMethod(memberType, useShallowClassClone, skipCycleTracking); + if (!useShallowClassClone && !skipCycleTracking) + { + Expression deepCall = Expression.Call(classCloneMethod, originalMemberValue, state); + Expression shallowCall = Expression.Call(StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassShallowAndTrack, originalMemberValue, state); + Expression selected = Expression.Condition(Expression.Property(state, StaticMethodInfos.DeepCloneStateProperties.UseWorkList), shallowCall, deepCall); + clonedValueExpression = Expression.Convert(selected, memberType); + } + else + { + Expression call = Expression.Call(classCloneMethod, originalMemberValue, state); + clonedValueExpression = Expression.Convert(call, memberType); + } + } + + switch (member) + { + case FieldInfo fieldInfo: + { + bool isReadonly = readonlyFields.GetOrAdd(fieldInfo, f => f.IsInitOnly); + if (isReadonly) + { + if (skipReadonly) + break; + expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), fieldSetMethod, Expression.Convert(toLocal, typeof(object)), Expression.Convert(clonedValueExpression, typeof(object)))); + } + else + { + expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), clonedValueExpression)); + } + + break; + } + + case PropertyInfo { CanWrite: true }: + { + expressionList.Add(Expression.Assign(Expression.MakeMemberAccess(toLocal, member), clonedValueExpression)); + break; + } + } + + currentPosition = currentPosition.Next(); + } + } + } + + private static object GenerateMemberwiseCloner(Type type, ExpressionPosition position) + { + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + ParameterExpression toLocal = Expression.Variable(type); + ParameterExpression fromLocal = Expression.Variable(type); + List expressionList = []; + if (!type.IsValueType()) + { + MethodInfo methodInfo = StaticMethodInfos.CommonMethods.DirectCloneObject; + expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(methodInfo, from), type))); + expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); + expressionList.Add(Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, toLocal)); + AddMemberCloneExpressions(expressionList, fromLocal, toLocal, state, type, skipReadonly: false); + expressionList.Add(Expression.Convert(toLocal, typeof(object))); + List blockParams = [fromLocal, toLocal]; + return Expression.Lambda>(Expression.Block(blockParams, expressionList), from, state).Compile(); + } + + expressionList.Add(Expression.Assign(toLocal, Expression.Unbox(from, type))); + expressionList.Add(Expression.Assign(fromLocal, toLocal)); + if (TypeHasReadonlyFields(type) && ShouldDeepCloneStructReadonlyFields(type)) + { + AddMemberCloneExpressions(expressionList, fromLocal, toLocal, state, type, skipReadonly: true); + ParameterExpression boxedToLocal = Expression.Variable(typeof(object)); + expressionList.Add(Expression.Assign(boxedToLocal, Expression.Convert(toLocal, typeof(object)))); + AddStructReadonlyFieldsCloneExpressions(expressionList, fromLocal, boxedToLocal, state, type); + expressionList.Add(Expression.Assign(toLocal, Expression.Unbox(boxedToLocal, type))); + expressionList.Add(Expression.Convert(toLocal, typeof(object))); + List blockParams = [fromLocal, toLocal, boxedToLocal]; + return Expression.Lambda>(Expression.Block(blockParams, expressionList), from, state).Compile(); + } + else + { + AddMemberCloneExpressions(expressionList, fromLocal, toLocal, state, type, skipReadonly: false); + expressionList.Add(Expression.Convert(toLocal, typeof(object))); + List blockParams = [fromLocal, toLocal]; + return Expression.Lambda>(Expression.Block(blockParams, expressionList), from, state).Compile(); + } + } + + private delegate object ProcessMethodDelegate(Type type, bool unboxStruct, ExpressionPosition position); + private static readonly FrozenDictionary knownTypeProcessors = new Dictionary + { + [typeof(ExpandoObject)] = (_, _, position) => GenerateExpandoObjectProcessor(position), + [typeof(HttpRequestOptions)] = (_, _, position) => GenerateHttpRequestOptionsProcessor(position), + [typeof(Array)] = (type, _, _) => GenerateProcessArrayMethod(type), + [typeof(System.Text.Json.Nodes.JsonNode)] = (_, _, position) => GenerateJsonNodeProcessorModern(position), + [typeof(System.Text.Json.Nodes.JsonObject)] = (_, _, position) => GenerateJsonNodeProcessorModern(position), + [typeof(System.Text.Json.Nodes.JsonArray)] = (_, _, position) => GenerateJsonNodeProcessorModern(position), + [typeof(System.Text.Json.Nodes.JsonValue)] = (_, _, position) => GenerateJsonNodeProcessorModern(position), + }.ToFrozenDictionary(); + private static readonly AhoCorasick badTypes = new AhoCorasick(["Castle.Proxies.", "System.Data.Entity.DynamicProxies.", "NHibernate.Proxy."]); + private static readonly AhoCorasick readonlyStructSafeHandleNamespaces = new AhoCorasick(["System.Net.", "System.Reflection.", "System.IO.", "System.Runtime.", "System.Threading.", "System.Text.Json.", "System.Diagnostics."]); + private static readonly Dictionary> specialNamespaces = new Dictionary> + { + // these can be trusted to have their Clone() implemented properly + { + "System.Drawing", + CloneIClonable + }, + { + "System.Globalization", + CloneIClonable + } + }; + private static bool IsCloneable(Type type) + { + if (type.FullName is null) + { + return false; + } + + return !badTypes.ContainsAnyPattern(type.FullName); + } + + private static FastClonerCache.TypeShape BuildTypeShape(Type type) + { + List members = []; + Dictionary? ignoredEventDetails = null; + List cycleFieldTypes = new List(); + bool hasReadonlyFields = false; + bool containsIgnoredMembers = false; + bool hasDirectSelfReference = false; + bool includeMemberMetadata = true; + Type? currentType = type; + while (currentType != null && currentType != typeof(object)) + { + if (currentType == typeof(ContextBoundObject)) + { + includeMemberMetadata = false; + } + + FieldInfo[] fields = currentType.GetDeclaredFields(); + for (int i = 0; i < fields.Length; i++) + { + FieldInfo field = fields[i]; + cycleFieldTypes.Add(field.FieldType); + if (!includeMemberMetadata) + { + continue; + } + + members.Add(field); + if (field.IsInitOnly) + { + hasReadonlyFields = true; + } + + if (MemberIsIgnored(field)) + { + containsIgnoredMembers = true; + } + + Type fieldType = field.FieldType; + if (fieldType == type || fieldType.IsArray && fieldType.GetElementType() == type) + { + hasDirectSelfReference = true; + } + } + + if (includeMemberMetadata) + { + PropertyInfo[] properties = currentType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + for (int i = 0; i < properties.Length; i++) + { + PropertyInfo property = properties[i]; + if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length != 0) + { + continue; + } + + members.Add(property); + if (MemberIsIgnored(property)) + { + containsIgnoredMembers = true; + } + } + + EventInfo[] events = currentType.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + for (int i = 0; i < events.Length; i++) + { + EventInfo evtInfo = events[i]; + if (MemberIsIgnored(evtInfo) && evtInfo.EventHandlerType is not null) + { + containsIgnoredMembers = true; + ignoredEventDetails ??= new Dictionary(); + ignoredEventDetails[evtInfo.Name] = evtInfo.EventHandlerType; + } + } + } + + currentType = currentType.BaseType; + } + + return new FastClonerCache.TypeShape + { + Members = members.ToArray(), + IgnoredEventDetails = ignoredEventDetails, + CycleFieldTypes = cycleFieldTypes.ToArray(), + HasReadonlyFields = hasReadonlyFields, + ContainsIgnoredMembers = containsIgnoredMembers, + HasDirectSelfReference = hasDirectSelfReference + }; + } + + private static object? CloneIClonable(Type type) + { + if (typeof(ICloneable).IsAssignableFrom(type)) + { + return (Func)((obj, state) => + { + object result = ((ICloneable)obj).Clone(); + state.AddKnownRef(obj, result); + return result; + }); + } + + return null; + } + + private static object? GenerateProcessMethod(Type type, bool unboxStruct, ExpressionPosition position, bool skipCycleTracking = false, bool useShallowClassClone = false) + { + if (!IsCloneable(type)) + { + return null; + } + + Type methodType = unboxStruct || type.IsClass() ? typeof(object) : type; + if (FastClonerCache.IsTypeIgnored(type)) + { + ParameterExpression pFrom = Expression.Parameter(methodType, "fromParam"); + ParameterExpression pState = Expression.Parameter(typeof(FastCloneState), "stateParam"); + Type funcGenericType = typeof(Func<,, >).MakeGenericType(methodType, typeof(FastCloneState), methodType); + Expression resultExpression; + if (type.IsValueType && !unboxStruct) + { + resultExpression = Expression.Default(type); + } + else + { + resultExpression = Expression.Constant(null, methodType); + } + + return Expression.Lambda(funcGenericType, resultExpression, pFrom, pState).Compile(); + } + + if (knownTypeProcessors.TryGetValue(type, out ProcessMethodDelegate? handler)) + { + return handler.Invoke(type, unboxStruct, position); + } + + if (CustomTypeHandlers.TryGetValue(type, out Func? contribHandler)) + { + return contribHandler.Invoke(type, unboxStruct, position); + } + + if (type.Namespace is not null && specialNamespaces.TryGetValue(type.Namespace, out Func? cloneMethod)) + { + object? special = cloneMethod.Invoke(type); + if (special is not null) + { + return special; + } + } + + if (IsDictionaryType(type)) + { + return GenerateProcessDictionaryMethod(type, position); + } + + if (IsListType(type)) + { + return GenerateProcessListMethod(type); + } + + if (IsConcurrentBagOrQueue(type)) + { + return GenerateProcessConcurrentBagOrQueueMethod(type, position); + } + + if (IsSetType(type)) + { + return GenerateProcessSetMethod(type, position); + } + + if (type.IsArray) + { + return GenerateProcessArrayMethod(type); + } + + if (type.FullName is not null && type.FullName.StartsWith("System.Tuple`")) + { + Type[] genericArguments = type.GenericArguments(); + if (genericArguments.Length < 10 && genericArguments.All(FastClonerSafeTypes.CanReturnSameObject)) + { + return GenerateProcessTupleMethod(type); + } + } + + List expressionList = []; + ParameterExpression from = Expression.Parameter(methodType); + ParameterExpression fromLocal = from; + ParameterExpression toLocal = Expression.Variable(type); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + if (!type.IsValueType()) + { + MethodInfo methodInfo = StaticMethodInfos.CommonMethods.DirectCloneObject; + expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(methodInfo, from), type))); + fromLocal = Expression.Variable(type); + expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); + if (!skipCycleTracking) + { + expressionList.Add(Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, toLocal)); + } + } + else + { + if (unboxStruct) + { + expressionList.Add(Expression.Assign(toLocal, Expression.Unbox(from, type))); + fromLocal = Expression.Variable(type); + expressionList.Add(Expression.Assign(fromLocal, toLocal)); + } + else + { + expressionList.Add(Expression.Assign(toLocal, from)); + } + } + + if (type.IsValueType && TypeHasReadonlyFields(type) && ShouldDeepCloneStructReadonlyFields(type)) + { + AddMemberCloneExpressions(expressionList, fromLocal, toLocal, state, type, skipReadonly: true, useShallowClassClone: useShallowClassClone, skipCycleTracking: skipCycleTracking); + ParameterExpression boxedToLocal = Expression.Variable(typeof(object)); + expressionList.Add(Expression.Assign(boxedToLocal, Expression.Convert(toLocal, typeof(object)))); + AddStructReadonlyFieldsCloneExpressions(expressionList, fromLocal, boxedToLocal, state, type, useShallowClassClone: useShallowClassClone, skipCycleTracking: skipCycleTracking); + expressionList.Add(Expression.Assign(toLocal, Expression.Unbox(boxedToLocal, type))); + expressionList.Add(Expression.Convert(toLocal, methodType)); + Type makeGenericType = typeof(Func<,, >).MakeGenericType(methodType, typeof(FastCloneState), methodType); + List blkParams = []; + if (from != fromLocal) + { + blkParams.Add(fromLocal); + } + + blkParams.Add(toLocal); + blkParams.Add(boxedToLocal); + return Expression.Lambda(makeGenericType, Expression.Block(blkParams, expressionList), from, state).Compile(); + } + + AddMemberCloneExpressions(expressionList, fromLocal, toLocal, state, type, skipReadonly: false, useShallowClassClone: useShallowClassClone, skipCycleTracking: skipCycleTracking); + expressionList.Add(Expression.Convert(toLocal, methodType)); + Type funcType = typeof(Func<,, >).MakeGenericType(methodType, typeof(FastCloneState), methodType); + List blockParams = []; + if (from != fromLocal) + { + blockParams.Add(fromLocal); + } + + blockParams.Add(toLocal); + return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, state).Compile(); + } + + private static object GenerateHttpRequestOptionsProcessor(ExpressionPosition position) + { + if (FastClonerCache.IsTypeIgnored(typeof(HttpRequestOptions))) + { + ParameterExpression pFrom = Expression.Parameter(typeof(object)); + ParameterExpression pState = Expression.Parameter(typeof(FastCloneState)); + return Expression.Lambda>(pFrom, pFrom, pState).Compile(); + } + + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + ParameterExpression result = Expression.Variable(typeof(HttpRequestOptions)); + ParameterExpression tempMessage = Expression.Variable(typeof(HttpRequestMessage)); + ParameterExpression fromOptions = Expression.Variable(typeof(HttpRequestOptions)); + ConstructorInfo constructor = typeof(HttpRequestMessage).GetConstructor(Type.EmptyTypes)!; + BlockExpression block = Expression.Block([result, tempMessage, fromOptions], Expression.Assign(fromOptions, Expression.Convert(from, typeof(HttpRequestOptions))), Expression.Assign(tempMessage, Expression.New(constructor)), Expression.Assign(result, Expression.Property(tempMessage, "Options")), Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, result), Expression.Assign(result, Expression.Convert(Expression.Call(StaticMethodInfos.CommonMethods.DirectCloneObject, fromOptions), typeof(HttpRequestOptions))), Expression.Call(tempMessage, StaticMethodInfos.CommonMethods.Dispose), result); + return Expression.Lambda>(block, from, state).Compile(); + } + + private static object GenerateExpandoObjectProcessor(ExpressionPosition position) + { + return GenerateMemberwiseCloner(typeof(ExpandoObject), position); + } + + private static object GenerateProcessListMethod(Type type) + { + Type elementType = type.GetGenericArguments()[0]; + string methodName; + Type methodGenericArg; + if (FastClonerSafeTypes.CanReturnSameObject(elementType)) + { + methodName = nameof(FastClonerGenerator.CloneListSafeInternal); + methodGenericArg = elementType; + } + else if (elementType.IsValueType) + { + methodName = nameof(FastClonerGenerator.CloneListStructInternal); + methodGenericArg = elementType; + } + else + { + methodName = nameof(FastClonerGenerator.CloneListClassInternal); + methodGenericArg = elementType; + } + + MethodInfo methodInfo = typeof(FastClonerGenerator).GetPrivateStaticMethod(methodName)!.MakeGenericMethod(methodGenericArg); + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + MethodCallExpression call = Expression.Call(methodInfo, Expression.Convert(from, type), state); + Type funcType = typeof(Func<,, >).MakeGenericType(typeof(object), typeof(FastCloneState), typeof(object)); + return Expression.Lambda(funcType, Expression.Convert(call, typeof(object)), from, state).Compile(); + } + + private static object GenerateProcessDictionaryMethod(Type type, ExpressionPosition position) + { + Type[] genericArguments = type.GenericArguments(); + return genericArguments.Length switch + { + 0 => GenerateNonGenericDictionaryProcessor(type, position), + 1 => HandleSingleGenericArgument(type, genericArguments[0], position), + 2 => GenerateDictionaryProcessor(type, genericArguments[0], genericArguments[1], true, position), + _ => throw new ArgumentException($"Unexpected number of generic arguments: {genericArguments.Length}")}; + } + + private static object GenerateNonGenericDictionaryProcessor(Type type, ExpressionPosition position) + { + Type? dictionaryInterface = type.GetInterfaces().FirstOrDefault(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<, >) || i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<, >))); + if (dictionaryInterface is not null) + { + Type[] interfaceArgs = dictionaryInterface.GetGenericArguments(); + return GenerateDictionaryProcessor(type, interfaceArgs[0], interfaceArgs[1], true, position); + } + + return GenerateDictionaryProcessor(type, typeof(object), typeof(object), false, position); + } + + private static object HandleSingleGenericArgument(Type type, Type genericArg, ExpressionPosition position) + { + if (genericArg.IsGenericType && genericArg.GetGenericTypeDefinition() == typeof(KeyValuePair<, >)) + { + Type[] kvpArguments = genericArg.GetGenericArguments(); + return GenerateDictionaryProcessor(type, kvpArguments[0], kvpArguments[1], true, position); + } + + if (typeof(IDictionary).IsAssignableFrom(type)) + { + return GenerateDictionaryProcessor(type, typeof(object), genericArg, true, position); + } + + throw new ArgumentException($"Unsupported dictionary type with single generic argument: {type.FullName}"); + } + + private static object GenerateDictionaryProcessor(Type dictType, Type keyType, Type valueType, bool isGeneric, ExpressionPosition position) + { + bool isImmutable = IsImmutableCollection(dictType); + switch (isImmutable) + { + case false when dictType.IsGenericType && dictType.GetGenericTypeDefinition() == typeof(Dictionary<, >) && FastClonerSafeTypes.HasStableHashSemantics(keyType) && (keyType.IsValueType || FastClonerSafeTypes.CanReturnSameObject(keyType)): + return GenerateAdaptiveDictionaryProcessor(dictType, keyType, valueType, position); + case false when FastClonerSafeTypes.HasStableHashSemantics(keyType) && !FastClonerCache.IsTypeIgnored(keyType) && !FastClonerCache.IsTypeIgnored(valueType): + return GenerateMemberwiseCloner(dictType, position); + } + + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + LabelTarget returnNullLabel = Expression.Label(typeof(object)); + ConditionalExpression nullCheck = Expression.IfThen(Expression.Equal(from, Expression.Constant(null)), Expression.Return(returnNullLabel, Expression.Constant(null))); + if (isImmutable) + { + return GenerateImmutableDictionaryProcessor(dictType, keyType, valueType, from, state, returnNullLabel, nullCheck); + } + + // read-only + bool isReadOnly = dictType.Name.Contains("ReadOnly", StringComparison.InvariantCultureIgnoreCase) || (dictType.IsGenericType && dictType.GetGenericTypeDefinition() == typeof(ReadOnlyDictionary<, >)); + Type innerDictType = isReadOnly ? typeof(Dictionary<, >).MakeGenericType(keyType, valueType) : dictType; + // Check constructors + ConstructorInfoEx? ctorInfo = isReadOnly ? dictType.GetConstructor([innerDictType])is { } ctor ? new ConstructorInfoEx(ctor) : null : FindCallableConstructor(dictType); + // If we can't find appropriate constructor, fall back to memberwise cloning + if (ctorInfo is null) + { + return GenerateMemberwiseCloner(dictType, position); + } + + ParameterExpression result = Expression.Variable(dictType); + ParameterExpression innerDict = isReadOnly ? Expression.Variable(innerDictType) : result; + // Create a typed reference to get Count for capacity preallocation + ParameterExpression typedFrom = Expression.Variable(dictType); + BinaryExpression assignTypedFrom = Expression.Assign(typedFrom, Expression.Convert(from, dictType)); + // Create instance of inner dictionary with capacity preallocation + ConstructorInfo? capacityCtor = FindCapacityConstructor(innerDictType); + ConstructorInfoEx? innerDictCtorInfo = FindCallableConstructor(innerDictType); + if (innerDictCtorInfo is null && capacityCtor is null) + { + return GenerateMemberwiseCloner(dictType, position); + } + + Expression createInnerDict; + if (capacityCtor is not null) + { + // Use capacity constructor with Count from source dictionary + PropertyInfo? countProperty = dictType.GetProperty("Count"); + if (countProperty is not null) + { + Expression countExpr = Expression.Property(typedFrom, countProperty); + createInnerDict = Expression.Assign(innerDict, Expression.New(capacityCtor, countExpr)); + } + else + { + // Fall back to parameterless constructor if Count property not found + createInnerDict = Expression.Assign(innerDict, CreateNewExpressionWithCtor(innerDictCtorInfo!.Value)); + } + } + else + { + createInnerDict = Expression.Assign(innerDict, CreateNewExpressionWithCtor(innerDictCtorInfo!.Value)); + } + + // If ReadOnlyDictionary, use inner Dictionary + Expression createResult = isReadOnly ? Expression.Assign(result, Expression.New(dictType.GetConstructor([innerDictType])!, innerDict)) : createInnerDict; + // Add reference to state for cycle detection + Expression addRef = Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, result); + Type methodSourceType = isReadOnly ? innerDictType : dictType; + MethodInfo? addMethod = methodSourceType.GetMethod("Add", [keyType, valueType]) ?? methodSourceType.GetMethods().FirstOrDefault(m => m.Name == "TryAdd" && m.GetParameters().Length is 2 && m.GetParameters()[0].ParameterType == keyType && m.GetParameters()[1].ParameterType == valueType); + if (addMethod is null) + { + return GenerateMemberwiseCloner(dictType, position); + } + + // Setup enumerator + Type enumeratorType = isGeneric ? typeof(IEnumerator<>).MakeGenericType(typeof(KeyValuePair<, >).MakeGenericType(keyType, valueType)) : typeof(IDictionaryEnumerator); + ParameterExpression enumerator = Expression.Variable(enumeratorType); + // Get clone methods + MethodInfo keyCloneMethod = GetDefaultCloneMethod(keyType); + MethodInfo valueCloneMethod = GetDefaultCloneMethod(valueType); + BlockExpression iterationBlock = isGeneric ? GenerateGenericDictionaryIteration(enumerator, keyType, valueType, keyCloneMethod, valueCloneMethod, innerDict, addMethod, state, position) : GenerateNonGenericDictionaryIteration(enumerator, keyCloneMethod, valueCloneMethod, innerDict, addMethod, state, position); + Type enumerableType = isGeneric ? typeof(IEnumerable<>).MakeGenericType(typeof(KeyValuePair<, >).MakeGenericType(keyType, valueType)) : typeof(IDictionary); + MethodInfo? getEnumeratorMethod = enumerableType.GetMethod("GetEnumerator"); + if (getEnumeratorMethod is null) + { + throw new InvalidOperationException($"Cannot find GetEnumerator method for type {enumerableType.FullName}"); + } + + Expression getEnumerator = Expression.Assign(enumerator, Expression.Convert(Expression.Call(Expression.Convert(from, enumerableType), getEnumeratorMethod), enumeratorType)); + // Combine into final expression + List blockVars = isReadOnly ? [result, innerDict, enumerator, typedFrom] : [result, enumerator, typedFrom]; + BlockExpression block = Expression.Block(blockVars, nullCheck, assignTypedFrom, createInnerDict, createResult, addRef, getEnumerator, iterationBlock, Expression.Label(returnNullLabel, Expression.Convert(result, typeof(object)))); + return Expression.Lambda>(block, from, state).Compile(); + } + + private static object GenerateImmutableDictionaryProcessor(Type dictType, Type keyType, Type valueType, ParameterExpression from, ParameterExpression state, LabelTarget returnNullLabel, Expression nullCheck) + { + bool ignoreKeyType = FastClonerCache.IsTypeIgnored(keyType); + bool ignoreValueType = FastClonerCache.IsTypeIgnored(valueType); + ParameterExpression typedFrom = Expression.Variable(dictType); + ParameterExpression result = Expression.Variable(dictType); + BinaryExpression castFrom = Expression.Assign(typedFrom, Expression.Convert(from, dictType)); + MethodInfo? addMethod = dictType.GetMethods(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(m => m.Name == "Add" && m.ReturnType == dictType && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType == keyType && m.GetParameters()[1].ParameterType == valueType); + if (addMethod == null) + { + return Expression.Lambda>(from, from, state).Compile(); + } + + MethodInfo? emptyMethod = dictType.GetMethod("Empty", BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null); + MethodInfo? createMethod = dictType.GetMethod("Create", BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null); + // try to make an empty copy + Expression createEmpty; + if (emptyMethod != null) + { + createEmpty = Expression.Assign(result, Expression.Call(null, emptyMethod)); + } + else if (createMethod != null) + { + createEmpty = Expression.Assign(result, Expression.Call(null, createMethod)); + } + else + { + MethodInfo? clearMethod = dictType.GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance); + if (clearMethod != null && clearMethod.ReturnType == dictType) + { + createEmpty = Expression.Assign(result, Expression.Call(typedFrom, clearMethod)); + } + else + { + // bail + return Expression.Lambda>(from, from, state).Compile(); + } + } + + MethodInfo keyCloneMethod = GetDefaultCloneMethod(keyType); + MethodInfo valueCloneMethod = GetDefaultCloneMethod(valueType); + Type enumeratorType = typeof(IEnumerator<>).MakeGenericType(typeof(KeyValuePair<, >).MakeGenericType(keyType, valueType)); + ParameterExpression enumerator = Expression.Variable(enumeratorType); + Type enumerableType = typeof(IEnumerable<>).MakeGenericType(typeof(KeyValuePair<, >).MakeGenericType(keyType, valueType)); + MethodInfo? getEnumeratorMethod = enumerableType.GetMethod("GetEnumerator"); + BinaryExpression assignEnumerator = Expression.Assign(enumerator, Expression.Call(Expression.Convert(typedFrom, enumerableType), getEnumeratorMethod)); + // iterate over pairs + PropertyInfo? current = enumeratorType.GetProperty("Current"); + Type kvpType = typeof(KeyValuePair<, >).MakeGenericType(keyType, valueType); + ParameterExpression kvp = Expression.Variable(kvpType); + ParameterExpression key = Expression.Variable(keyType); + ParameterExpression value = Expression.Variable(valueType); + LabelTarget breakLabel = Expression.Label("LoopBreak"); + Expression originalKeyProperty = Expression.Property(kvp, "Key"); + Expression clonedKeyCall = Expression.Call(keyCloneMethod, originalKeyProperty, state); + if (!keyType.IsValueType()) + { + clonedKeyCall = Expression.Convert(clonedKeyCall, keyType); + } + + Expression keyForAdd = ignoreKeyType ? originalKeyProperty : clonedKeyCall; + Expression originalValueProperty = Expression.Property(kvp, "Value"); + Expression clonedValueCall = Expression.Call(valueCloneMethod, originalValueProperty, state); + if (!valueType.IsValueType()) + { + clonedValueCall = Expression.Convert(clonedValueCall, valueType); + } + + Expression valueForAdd = ignoreValueType ? originalValueProperty : clonedValueCall; + BlockExpression loopBody = Expression.Block([kvp, key, value], Expression.Assign(kvp, Expression.Property(enumerator, current)), Expression.Assign(key, keyForAdd), Expression.Assign(value, valueForAdd), Expression.Assign(result, Expression.Call(result, addMethod, key, value))); + LoopExpression loop = Expression.Loop(Expression.IfThenElse(Expression.Call(enumerator, StaticMethodInfos.CommonMethods.EnumeratorMoveNext), loopBody, Expression.Break(breakLabel)), breakLabel); + // detect cycles + MethodCallExpression addRef = Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, result); + BlockExpression block = Expression.Block([typedFrom, result, enumerator], nullCheck, castFrom, createEmpty, assignEnumerator, loop, addRef, Expression.Label(returnNullLabel, Expression.Convert(result, typeof(object)))); + return Expression.Lambda>(block, from, state).Compile(); + } + + /// + /// Note: this is based on a few heuristics, "best effort". + /// + /// + /// + private static bool IsImmutableCollection(Type type) + { + return FastClonerCache.GetOrAddImmutableCollectionStatus(type, static t => + { + if (t.Namespace == "System.Collections.Immutable") + { + return true; + } + + if (t.GetInterfaces().Any(x => x.Namespace == "System.Collections.Immutable")) + { + return true; + } + + IList attrs = CustomAttributeData.GetCustomAttributes(t); + for (int i = 0; i < attrs.Count; i++) + { + Type attrType = attrs[i].AttributeType; + string? attrNs = attrType.Namespace; + if ((attrNs is not null && attrNs.Contains("Immutable", StringComparison.Ordinal)) || attrType.Name.Contains("Immutable", StringComparison.Ordinal)) + { + return true; + } + } + + return t.Name.Contains("Immutable", StringComparison.Ordinal); + }); + } + + private static BlockExpression GenerateGenericDictionaryIteration(ParameterExpression enumerator, Type keyType, Type valueType, MethodInfo keyCloneMethod, MethodInfo valueCloneMethod, ParameterExpression local, MethodInfo addMethod, ParameterExpression state, ExpressionPosition position) + { + bool ignoreKeyType = FastClonerCache.IsTypeIgnored(keyType); + bool ignoreValueType = FastClonerCache.IsTypeIgnored(valueType); + PropertyInfo current = enumerator.Type.GetProperty(nameof(IEnumerator.Current))!; + LabelTarget breakLabel = CreateLoopLabel(position); + Type dictionaryType = local.Type; + bool isSingleGenericParameter = dictionaryType.GetGenericArguments().Length is 1; + if (isSingleGenericParameter) + { + Type singleGenericType = dictionaryType.GetGenericArguments()[0]; + if (singleGenericType.IsGenericType && singleGenericType.GetGenericTypeDefinition() == typeof(KeyValuePair<, >)) + { + Type[] kvpTypes = singleGenericType.GetGenericArguments(); + ParameterExpression kvp = Expression.Variable(singleGenericType); + BinaryExpression assignKvp = Expression.Assign(kvp, Expression.Property(enumerator, current)); + Expression originalSingleKey = Expression.Property(kvp, "Key"); + Expression clonedSingleKeyCall = Expression.Call(keyCloneMethod, originalSingleKey, state); + if (!kvpTypes[0].IsValueType()) + { + clonedSingleKeyCall = Expression.Convert(clonedSingleKeyCall, kvpTypes[0]); + } + + bool ignoreSingleKeyType = kvpTypes[0] == keyType ? ignoreKeyType : FastClonerCache.IsTypeIgnored(kvpTypes[0]); + Expression keyForNewKvp = ignoreSingleKeyType ? originalSingleKey : clonedSingleKeyCall; + Expression originalSingleValue = Expression.Property(kvp, "Value"); + Expression clonedSingleValueCall = Expression.Call(valueCloneMethod, originalSingleValue, state); + if (!kvpTypes[1].IsValueType()) + clonedSingleValueCall = Expression.Convert(clonedSingleValueCall, kvpTypes[1]); + bool ignoreSingleValueType = kvpTypes[1] == valueType ? ignoreValueType : FastClonerCache.IsTypeIgnored(kvpTypes[1]); + Expression valueForNewKvp = ignoreSingleValueType ? originalSingleValue : clonedSingleValueCall; + NewExpression newKvp = Expression.New(singleGenericType.GetConstructor([kvpTypes[0], kvpTypes[1]])!, Expression.Convert(keyForNewKvp, kvpTypes[0]), Expression.Convert(valueForNewKvp, kvpTypes[1])); + MethodInfo collectionAddMethod = dictionaryType.GetMethod("Add", [singleGenericType])!; + MethodCallExpression addKvp = Expression.Call(local, collectionAddMethod, newKvp); + LoopExpression loop = Expression.Loop(Expression.IfThenElse(Expression.Call(enumerator, StaticMethodInfos.CommonMethods.EnumeratorMoveNext), Expression.Block([kvp], assignKvp, addKvp), Expression.Break(breakLabel)), breakLabel); + return Expression.Block(loop); + } + } + + { + Type kvpType = typeof(KeyValuePair<, >).MakeGenericType(keyType, valueType); + ParameterExpression kvp = Expression.Variable(kvpType); + ParameterExpression key = Expression.Variable(keyType); + ParameterExpression value = Expression.Variable(valueType); + BinaryExpression assignKvp = Expression.Assign(kvp, Expression.Property(enumerator, current)); + Expression originalKeyProperty = Expression.Property(kvp, "Key"); + Expression clonedKeyCall = Expression.Call(keyCloneMethod, originalKeyProperty, state); + if (!keyType.IsValueType()) + { + clonedKeyCall = Expression.Convert(clonedKeyCall, keyType); + } + + Expression keyToAssign = ignoreKeyType ? originalKeyProperty : clonedKeyCall; + Expression originalValueProperty = Expression.Property(kvp, "Value"); + Expression clonedValueCall = Expression.Call(valueCloneMethod, originalValueProperty, state); + if (!valueType.IsValueType()) + { + clonedValueCall = Expression.Convert(clonedValueCall, valueType); + } + + Expression valueToAssign = ignoreValueType ? valueType.IsValueType ? Expression.Default(valueType) : Expression.Constant(null, valueType) : clonedValueCall; + BinaryExpression assignKey = Expression.Assign(key, keyToAssign); + BinaryExpression assignValue = Expression.Assign(value, valueToAssign); + MethodCallExpression addKvp = Expression.Call(local, addMethod, key, value); + LoopExpression loop = Expression.Loop(Expression.IfThenElse(Expression.Call(enumerator, StaticMethodInfos.CommonMethods.EnumeratorMoveNext), Expression.Block([kvp, key, value], assignKvp, assignKey, assignValue, addKvp), Expression.Break(breakLabel)), breakLabel); + return Expression.Block(loop); + } + } + + private static BlockExpression GenerateNonGenericDictionaryIteration(ParameterExpression enumerator, MethodInfo keyCloneMethod, MethodInfo valueCloneMethod, ParameterExpression local, MethodInfo addMethod, ParameterExpression state, ExpressionPosition position) + { + MemberExpression current = Expression.Property(enumerator, nameof(IDictionaryEnumerator.Entry)); + ParameterExpression key = Expression.Variable(typeof(object)); + ParameterExpression value = Expression.Variable(typeof(object)); + Expression originalKeyObject = Expression.Property(current, "Key"); + Expression clonedKeyCall = Expression.Call(keyCloneMethod, originalKeyObject, state); + Expression keyRuntimeType = Expression.Call(originalKeyObject, StaticMethodInfos.CommonMethods.ObjectGetType); + Expression isKeyIgnored = Expression.Call(null, IsTypeIgnoredMethodInfo, keyRuntimeType); + Expression keyToAssign = Expression.Condition(Expression.OrElse(Expression.Equal(originalKeyObject, Expression.Constant(null, typeof(object))), isKeyIgnored), originalKeyObject, clonedKeyCall); + Expression originalValueObject = Expression.Property(current, "Value"); + Expression clonedValueCall = Expression.Call(valueCloneMethod, originalValueObject, state); + Expression valueRuntimeType = Expression.Call(originalValueObject, StaticMethodInfos.CommonMethods.ObjectGetType); + Expression isValueIgnored = Expression.Call(null, IsTypeIgnoredMethodInfo, valueRuntimeType); + Expression valueToAssign = Expression.Condition(Expression.OrElse(Expression.Equal(originalValueObject, Expression.Constant(null, typeof(object))), isValueIgnored), originalValueObject, clonedValueCall); + BinaryExpression assignKey = Expression.Assign(key, keyToAssign); + BinaryExpression assignValue = Expression.Assign(value, valueToAssign); + MethodCallExpression addEntry = Expression.Call(local, addMethod, key, value); + LabelTarget breakLabel = CreateLoopLabel(position); + LoopExpression loop = Expression.Loop(Expression.IfThenElse(Expression.Call(enumerator, StaticMethodInfos.CommonMethods.EnumeratorMoveNext), Expression.Block([key, value], assignKey, assignValue, addEntry), Expression.Break(breakLabel)), breakLabel); + return Expression.Block(loop); + } + + private static object GenerateAdaptiveDictionaryProcessor(Type dictType, Type keyType, Type valueType, ExpressionPosition position) + { + byte variant = FastClonerSafeTypes.CanReturnSameObject(valueType) ? (byte)0 : valueType.IsValueType ? (byte)1 : (byte)2; + AdaptiveDictionaryFactoryKey key = new AdaptiveDictionaryFactoryKey(keyType.TypeHandle.Value, valueType.TypeHandle.Value, variant); + Func factory = adaptiveDictionaryFactoryCache.GetOrAdd(key, _ => + { + string methodName = variant switch + { + 0 => nameof(CreateAdaptiveDictionaryProcessorSafe), + 1 => nameof(CreateAdaptiveDictionaryProcessorStruct), + _ => nameof(CreateAdaptiveDictionaryProcessorClass)}; + MethodInfo method = typeof(FastClonerExprGenerator).GetPrivateStaticMethod(methodName)!.MakeGenericMethod(keyType, valueType); + return (Func)method.CreateDelegate(typeof(Func)); + }); + return factory(position); + } + + private readonly struct AdaptiveDictionaryFactoryKey(IntPtr keyHandle, IntPtr valueHandle, byte variant) : IEquatable + { + private readonly IntPtr keyHandle = keyHandle; + private readonly IntPtr valueHandle = valueHandle; + private readonly byte variant = variant; + public bool Equals(AdaptiveDictionaryFactoryKey other) + { + return keyHandle == other.keyHandle && valueHandle == other.valueHandle && variant == other.variant; + } + + public override bool Equals(object? obj) + { + return obj is AdaptiveDictionaryFactoryKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + int hash = keyHandle.GetHashCode(); + hash = (hash * 397) ^ valueHandle.GetHashCode(); + return (hash * 397) ^ variant.GetHashCode(); + } + } + } + + private static object CreateAdaptiveDictionaryProcessorSafe(ExpressionPosition position) + where TKey : notnull + { + Func memberwise = (Func)GenerateMemberwiseCloner(typeof(Dictionary), position); + return (Func)((obj, state) => + { + Dictionary? typed = (Dictionary? )obj; + if (typed is null) + return null !; + return typed.Count <= AdaptiveDictionaryRebuildThreshold ? FastClonerGenerator.CloneDictionarySafeInternal(typed, state)! : memberwise(obj, state); + }); + } + + private static object CreateAdaptiveDictionaryProcessorStruct(ExpressionPosition position) + where TKey : notnull where TValue : struct + { + Func memberwise = (Func)GenerateMemberwiseCloner(typeof(Dictionary), position); + return (Func)((obj, state) => + { + Dictionary? typed = (Dictionary? )obj; + if (typed is null) + return null !; + return typed.Count <= AdaptiveDictionaryRebuildThreshold ? FastClonerGenerator.CloneDictionaryStructValueInternal(typed, state)! : memberwise(obj, state); + }); + } + + private static object CreateAdaptiveDictionaryProcessorClass(ExpressionPosition position) + where TKey : notnull where TValue : class + { + Func memberwise = (Func)GenerateMemberwiseCloner(typeof(Dictionary), position); + return (Func)((obj, state) => + { + Dictionary? typed = (Dictionary? )obj; + if (typed is null) + return null !; + return typed.Count <= AdaptiveDictionaryRebuildThreshold ? FastClonerGenerator.CloneDictionaryClassValueInternal(typed, state)! : memberwise(obj, state); + }); + } + + private static object GenerateProcessConcurrentBagOrQueueMethod(Type type, ExpressionPosition position) + { + if (FastClonerCache.IsTypeIgnored(type)) + { + ParameterExpression pFrom = Expression.Parameter(typeof(object)); + ParameterExpression pState = Expression.Parameter(typeof(FastCloneState)); + return Expression.Lambda>(pFrom, pFrom, pState).Compile(); + } + + Type elementType = type.GetGenericArguments()[0]; + MethodInfo cloneElementMethod = GetDefaultCloneMethod(elementType); + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + ParameterExpression local = Expression.Variable(type); + // Constructor + BinaryExpression assign = Expression.Assign(local, Expression.New(type.GetConstructor(Type.EmptyTypes)!)); + // Add Method + MethodInfo? addMethod = type.GetMethod("Add", [elementType]) ?? type.GetMethod("Enqueue", [elementType]); + if (addMethod == null) + return GenerateMemberwiseCloner(type, position); + // Foreach + BlockExpression foreachBlock = GenerateForeachBlock(from, elementType, null, cloneElementMethod, null, local, addMethod, state, position); + Type funcType = typeof(Func); + return Expression.Lambda(funcType, Expression.Block([local], assign, Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, local), foreachBlock, local), from, state).Compile(); + } + + private static object GenerateProcessSetMethod(Type type, ExpressionPosition position) + { + if (FastClonerCache.IsTypeIgnored(type)) + { + ParameterExpression pFrom = Expression.Parameter(typeof(object)); + ParameterExpression pState = Expression.Parameter(typeof(FastCloneState)); + return Expression.Lambda>(pFrom, pFrom, pState).Compile(); + } + + Type elementType = type.GenericArguments()[0]; + // Fast path check first - avoid creating expressions if we don't need them + bool isImmutable = IsImmutableCollection(type); + if (!isImmutable && FastClonerSafeTypes.HasStableHashSemantics(elementType) && !FastClonerCache.IsTypeIgnored(elementType)) + { + return GenerateMemberwiseCloner(type, position); + } + + // Now create expressions for immutable or slow path + MethodInfo cloneElementMethod = GetDefaultCloneMethod(elementType); + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + if (isImmutable) + { + return GenerateImmutableSetProcessor(type, elementType, from, state, position); + } + + ParameterExpression local = Expression.Variable(type); + bool isReadOnly = type.Name.Contains("ReadOnly", StringComparison.InvariantCultureIgnoreCase); + // Use HashSet as inner collection + Type innerSetType = isReadOnly ? typeof(HashSet<>).MakeGenericType(elementType) : type; + ParameterExpression innerSet = isReadOnly ? Expression.Variable(innerSetType) : local; + // Create a typed reference to get Count for capacity preallocation + ParameterExpression typedFrom = Expression.Variable(type); + BinaryExpression assignTypedFrom = Expression.Assign(typedFrom, Expression.Convert(from, type)); + // Try to use capacity constructor for better performance + ConstructorInfo? capacityCtor = FindCapacityConstructor(innerSetType); + ConstructorInfo? parameterlessCtor = innerSetType.GetConstructor(Type.EmptyTypes); + if (capacityCtor is null && parameterlessCtor is null) + { + return GenerateMemberwiseCloner(type, position); + } + + Expression assignInnerSet; + if (capacityCtor is not null) + { + // Use capacity constructor with Count from source set + PropertyInfo? countProperty = type.GetProperty("Count"); + if (countProperty is not null) + { + Expression countExpr = Expression.Property(typedFrom, countProperty); + assignInnerSet = Expression.Assign(innerSet, Expression.New(capacityCtor, countExpr)); + } + else + { + // Fall back to parameterless constructor if Count property not found + assignInnerSet = Expression.Assign(innerSet, Expression.New(parameterlessCtor!)); + } + } + else + { + assignInnerSet = Expression.Assign(innerSet, Expression.New(parameterlessCtor!)); + } + + // Get Add method from inner set + MethodInfo? addMethod = innerSetType.GetMethod("Add", [elementType]) ?? typeof(ISet<>).MakeGenericType(elementType).GetMethod("Add") ?? innerSetType.GetMethod("Add") ?? innerSetType.GetMethod("TryAdd"); + if (addMethod == null) + { + return GenerateMemberwiseCloner(type, position); + } + + // Generate foreach block using inner set + BlockExpression foreachBlock = GenerateForeachBlock(from, elementType, null, cloneElementMethod, null, innerSet, addMethod, state, position); + Expression createMutableColl = assignInnerSet; + Expression createFinalCollShell = isReadOnly ? Expression.Assign(local, Expression.New(type.GetConstructor([innerSetType])!, innerSet)) : Expression.Empty(); + Type funcType = typeof(Func); + List setBlockVars = isReadOnly ? [local, innerSet, typedFrom] : [local, typedFrom]; + return Expression.Lambda(funcType, Expression.Block(setBlockVars, assignTypedFrom, createMutableColl, createFinalCollShell, Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, local), foreachBlock, local), from, state).Compile(); + } + + private static object GenerateImmutableSetProcessor(Type setType, Type elementType, ParameterExpression from, ParameterExpression state, ExpressionPosition position) + { + bool ignoreElementType = FastClonerCache.IsTypeIgnored(elementType); + ParameterExpression typedFrom = Expression.Variable(setType); + ParameterExpression result = Expression.Variable(setType); + LabelTarget returnNullLabel = Expression.Label(typeof(object)); + Expression nullCheck = Expression.IfThen(Expression.Equal(from, Expression.Constant(null)), Expression.Return(returnNullLabel, Expression.Constant(null))); + BinaryExpression castFrom = Expression.Assign(typedFrom, Expression.Convert(from, setType)); + MethodInfo? addMethod = setType.GetMethods(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(m => m.Name == "Add" && m.ReturnType == setType && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == elementType); + if (addMethod == null) + { + addMethod = setType.GetMethods(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(m => m.Name == "Union" && m.ReturnType == setType); + if (addMethod == null) + { + return GenerateMemberwiseCloner(setType, position); + } + } + + MethodInfo? emptyMethod = setType.GetMethod("Empty", BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null); + MethodInfo? createMethod = setType.GetMethod("Create", BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null); + Expression createEmpty; + if (emptyMethod != null) + { + createEmpty = Expression.Assign(result, Expression.Call(null, emptyMethod)); + } + else if (createMethod != null) + { + createEmpty = Expression.Assign(result, Expression.Call(null, createMethod)); + } + else + { + MethodInfo? clearMethod = setType.GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance); + if (clearMethod != null && clearMethod.ReturnType == setType) + { + createEmpty = Expression.Assign(result, Expression.Call(typedFrom, clearMethod)); + } + else + { + return Expression.Lambda>(from, from, state).Compile(); + } + } + + Type enumeratorType = typeof(IEnumerator<>).MakeGenericType(elementType); + ParameterExpression enumerator = Expression.Variable(enumeratorType); + Type enumerableType = typeof(IEnumerable<>).MakeGenericType(elementType); + MethodInfo? getEnumeratorMethod = enumerableType.GetMethod("GetEnumerator"); + BinaryExpression assignEnumerator = Expression.Assign(enumerator, Expression.Call(Expression.Convert(typedFrom, enumerableType), getEnumeratorMethod)); + PropertyInfo? current = enumeratorType.GetProperty("Current"); + ParameterExpression element = Expression.Variable(elementType); + LabelTarget breakLabel = Expression.Label("LoopBreak"); + MethodInfo elementCloneMethod = GetDefaultCloneMethod(elementType); + Expression originalElementProperty = Expression.Property(enumerator, current!); + Expression clonedElementCall = Expression.Call(elementCloneMethod, originalElementProperty, state); + if (!elementType.IsValueType()) + { + clonedElementCall = Expression.Convert(clonedElementCall, elementType); + } + + Expression elementForAdd = ignoreElementType ? elementType.IsValueType ? Expression.Default(elementType) : Expression.Constant(null, elementType) : clonedElementCall; + BlockExpression loopBody = Expression.Block([element], Expression.Assign(element, elementForAdd), Expression.Assign(result, Expression.Call(result, addMethod, element))); + LoopExpression loop = Expression.Loop(Expression.IfThenElse(Expression.Call(enumerator, StaticMethodInfos.CommonMethods.EnumeratorMoveNext), loopBody, Expression.Break(breakLabel)), breakLabel); + BlockExpression block = Expression.Block([typedFrom, result, enumerator], nullCheck, castFrom, createEmpty, Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, result), assignEnumerator, loop, Expression.Label(returnNullLabel, Expression.Convert(result, typeof(object)))); + return Expression.Lambda>(block, from, state).Compile(); + } + + private static object GenerateProcessArrayMethod(Type type) + { + if (FastClonerCache.IsTypeIgnored(type)) + { + ParameterExpression pFrom = Expression.Parameter(typeof(object)); + ParameterExpression pState = Expression.Parameter(typeof(FastCloneState)); + Type ft = typeof(Func<,, >).MakeGenericType(typeof(object), typeof(FastCloneState), typeof(object)); + return Expression.Lambda(ft, pFrom, pFrom, pState).Compile(); + } + + Type? elementType = type.GetElementType(); + int rank = type.GetArrayRank(); + MethodInfo methodInfo; + // multidim or not zero-based arrays + if (rank != 1 || type != elementType?.MakeArrayType()) + { + if (rank == 2 && type == elementType?.MakeArrayType(2)) + { + methodInfo = typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.Clone2DimArrayInternal))!.MakeGenericMethod(elementType); + } + else + { + methodInfo = typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.CloneAbstractArrayInternal))!; + } + } + else + { + string methodName; + if (FastClonerCache.IsTypeIgnored(elementType)) + { + methodName = elementType.IsValueType ? nameof(FastClonerGenerator.Clone1DimArrayStructInternal) : nameof(FastClonerGenerator.Clone1DimArrayClassInternal); + } + else if (FastClonerSafeTypes.CanReturnSameObject(elementType)) + { + methodName = nameof(FastClonerGenerator.Clone1DimArraySafeInternal); + } + else if (elementType.IsValueType()) + { + methodName = nameof(FastClonerGenerator.Clone1DimArrayStructInternal); + } + else + { + methodName = nameof(FastClonerGenerator.Clone1DimArrayClassInternal); + } + + methodInfo = typeof(FastClonerGenerator).GetPrivateStaticMethod(methodName)!.MakeGenericMethod(elementType); + } + + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + MethodCallExpression call = Expression.Call(methodInfo, Expression.Convert(from, type), state); + Type funcType = typeof(Func<,, >).MakeGenericType(typeof(object), typeof(FastCloneState), typeof(object)); + return Expression.Lambda(funcType, call, from, state).Compile(); + } + + private static BlockExpression GenerateForeachBlock(ParameterExpression from, Type keyType, Type? valueType, MethodInfo cloneKeyMethod, MethodInfo? cloneValueMethod, ParameterExpression local, MethodInfo addMethod, ParameterExpression state, ExpressionPosition position) + { + Type enumeratorType = typeof(IEnumerator<>).MakeGenericType(valueType == null ? keyType : typeof(KeyValuePair<, >).MakeGenericType(keyType, valueType)); + ParameterExpression enumerator = Expression.Variable(enumeratorType); + MethodInfo moveNext = StaticMethodInfos.CommonMethods.EnumeratorMoveNext; + PropertyInfo current = enumeratorType.GetProperty(nameof(IEnumerator.Current))!; + MethodInfo getEnumerator = typeof(IEnumerable).GetMethod(nameof(IEnumerable.GetEnumerator))!; + LabelTarget breakLabel = CreateLoopLabel(position); + LoopExpression loop = Expression.Loop(Expression.IfThenElse(Expression.Call(enumerator, moveNext), Expression.Block(valueType is null ? GenerateSetAddBlock(enumerator, current, keyType, cloneKeyMethod, local, addMethod, state) : GenerateDictionaryAddBlock(enumerator, current, keyType, valueType, cloneKeyMethod, cloneValueMethod!, local, addMethod, state)), Expression.Break(breakLabel)), breakLabel); + return Expression.Block([enumerator], Expression.Assign(enumerator, Expression.Convert(Expression.Call(Expression.Convert(from, typeof(IEnumerable)), getEnumerator), enumeratorType)), loop); + } + + private static BlockExpression GenerateSetAddBlock(ParameterExpression enumerator, PropertyInfo current, Type elementType, MethodInfo cloneElementMethod, ParameterExpression local, MethodInfo addMethod, ParameterExpression state) + { + bool ignoreElementType = FastClonerCache.IsTypeIgnored(elementType); + ParameterExpression elementVar = Expression.Variable(elementType); + Expression originalElement = Expression.Property(enumerator, current); + Expression clonedElementCall = Expression.Call(cloneElementMethod, originalElement, state); + if (!elementType.IsValueType()) + { + clonedElementCall = Expression.Convert(clonedElementCall, elementType); + } + + Expression elementToAssign = ignoreElementType ? originalElement : clonedElementCall; + BinaryExpression assignElement = Expression.Assign(elementVar, elementToAssign); + MethodCallExpression addElement = Expression.Call(local, addMethod, elementVar); + return Expression.Block([elementVar], assignElement, addElement); + } + + private static BlockExpression GenerateDictionaryAddBlock(ParameterExpression enumerator, PropertyInfo current, Type keyType, Type valueType, MethodInfo cloneKeyMethod, MethodInfo cloneValueMethod, ParameterExpression local, MethodInfo addMethod, ParameterExpression state) + { + bool ignoreKeyType = FastClonerCache.IsTypeIgnored(keyType); + bool ignoreValueType = FastClonerCache.IsTypeIgnored(valueType); + Type kvpType = typeof(KeyValuePair<, >).MakeGenericType(keyType, valueType); + ParameterExpression kvp = Expression.Variable(kvpType); + BinaryExpression assignKvp = Expression.Assign(kvp, Expression.Property(enumerator, current)); + ParameterExpression keyVar = Expression.Variable(keyType); + ParameterExpression valueVar = Expression.Variable(valueType); + Expression originalKeyProperty = Expression.Property(kvp, "Key"); + Expression clonedKeyCall = Expression.Call(cloneKeyMethod, originalKeyProperty, state); + if (!keyType.IsValueType()) + { + clonedKeyCall = Expression.Convert(clonedKeyCall, keyType); + } + + Expression keyToAssign = ignoreKeyType ? originalKeyProperty : clonedKeyCall; + Expression originalValueProperty = Expression.Property(kvp, "Value"); + Expression clonedValueCall = Expression.Call(cloneValueMethod, originalValueProperty, state); + if (!valueType.IsValueType()) + { + clonedValueCall = Expression.Convert(clonedValueCall, valueType); + } + + Expression valueToAssign = ignoreValueType ? originalValueProperty : clonedValueCall; + BinaryExpression assignKey = Expression.Assign(keyVar, keyToAssign); + BinaryExpression assignValue = Expression.Assign(valueVar, valueToAssign); + MethodCallExpression addKvp = Expression.Call(local, addMethod, keyVar, valueVar); + return Expression.Block([kvp, keyVar, valueVar], assignKvp, assignKey, assignValue, addKvp); + } + + private static object GenerateProcessTupleMethod(Type type) + { + if (FastClonerCache.IsTypeIgnored(type)) + { + ParameterExpression pFrom = Expression.Parameter(typeof(object)); + ParameterExpression pState = Expression.Parameter(typeof(FastCloneState)); + Type ft = typeof(Func<,, >).MakeGenericType(typeof(object), typeof(FastCloneState), typeof(object)); + return Expression.Lambda(ft, pFrom, pFrom, pState).Compile(); + } + + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + ParameterExpression local = Expression.Variable(type); + BinaryExpression assign = Expression.Assign(local, Expression.Convert(from, type)); + Type funcType = typeof(Func); + int tupleLength = type.GenericArguments().Length; + BinaryExpression constructor = Expression.Assign(local, Expression.New(type.GetPublicConstructors().First(x => x.GetParameters().Length == tupleLength), type.GetPublicProperties().OrderBy(x => x.Name).Where(x => x.CanRead && x.Name.StartsWith("Item") && char.IsDigit(x.Name[4])).Select(x => Expression.Property(local, x.Name)))); + return Expression.Lambda(funcType, Expression.Block([local], assign, constructor, Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, local), Expression.Convert(local, typeof(object))), from, state).Compile(); + } + + private static readonly Lazy jsonNodeDeepCloneMethod = new Lazy(() => typeof(System.Text.Json.Nodes.JsonNode).GetMethod("DeepClone")!, LazyThreadSafetyMode.ExecutionAndPublication); + private static object GenerateJsonNodeProcessorModern(ExpressionPosition position) + { + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); + Expression castToJsonNode = Expression.Convert(from, typeof(System.Text.Json.Nodes.JsonNode)); + Expression deepCloneCall = Expression.Call(castToJsonNode, jsonNodeDeepCloneMethod.Value); + Expression result = Expression.Convert(deepCloneCall, typeof(object)); + return Expression.Lambda>(result, from, state).Compile(); + } +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerGenerator.cs b/src/Foundatio/FastCloner/Code/FastClonerGenerator.cs new file mode 100644 index 000000000..0fc9b7f42 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerGenerator.cs @@ -0,0 +1,1334 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System; +using System.Collections.Generic; + +namespace Foundatio.FastCloner.Code; +internal static class BlittableHelper +{ + public static readonly bool IsBlittable; + public static readonly int Size; + static BlittableHelper() + { + Type type = typeof(T); + if (type.IsPrimitive && type != typeof(bool) && type != typeof(char)) + { + IsBlittable = true; + Size = Marshal.SizeOf(type); + return; + } + + IsBlittable = false; + Size = 0; + } +} + +internal static class FastArrayCopy +{ + private static readonly Func CloneArray = CreateCloneArray(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[] Clone(T[] source) => CloneArray(source); + private static Func CreateCloneArray() + { + if (BlittableHelper.IsBlittable) + return CloneWithBlockCopy; + return CloneManaged; + } + + private static T[] CloneManaged(T[] source) => (T[])source.Clone(); + private static T[] CloneWithBlockCopy(T[] source) + { + int length = source.Length; + T[] destination = new T[length]; + if (length > 0) + Buffer.BlockCopy(source, 0, destination, 0, length * BlittableHelper.Size); + return destination; + } +} + +internal static class FastClonerGenerator +{ + private static readonly Type DictionaryInterfaceDefinition = typeof(IDictionary<, >); + private static readonly Type EnumerableInterfaceDefinition = typeof(IEnumerable<>); + private struct TypeCloneDispatchCache + { + private Type? typeA; + private FastClonerCache.TypeCloneMetadata? metadataA; + private Func? recursiveA; + private Type? typeB; + private FastClonerCache.TypeCloneMetadata? metadataB; + private Func? recursiveB; + private Type? typeC; + private FastClonerCache.TypeCloneMetadata? metadataC; + private Func? recursiveC; + private Type? typeD; + private FastClonerCache.TypeCloneMetadata? metadataD; + private Func? recursiveD; + public void Resolve(Type runtimeType, FastCloneState state, out FastClonerCache.TypeCloneMetadata metadata, out Func? recursiveCloner) + { + if (runtimeType == typeA && metadataA is not null) + { + metadata = metadataA; + recursiveCloner = recursiveA; + return; + } + + if (runtimeType == typeB && metadataB is not null) + { + metadata = metadataB; + recursiveCloner = recursiveB; + PromoteSecondToFirst(); + return; + } + + if (runtimeType == typeC && metadataC is not null) + { + metadata = metadataC; + recursiveCloner = recursiveC; + PromoteThirdToFirst(); + return; + } + + if (runtimeType == typeD && metadataD is not null) + { + metadata = metadataD; + recursiveCloner = recursiveD; + PromoteFourthToFirst(); + return; + } + + metadata = GetTypeMetadata(runtimeType, state); + recursiveCloner = metadata.RecursiveCloner; + typeD = typeC; + metadataD = metadataC; + recursiveD = recursiveC; + typeC = typeB; + metadataC = metadataB; + recursiveC = recursiveB; + typeB = typeA; + metadataB = metadataA; + recursiveB = recursiveA; + typeA = runtimeType; + metadataA = metadata; + recursiveA = recursiveCloner; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("ReSharper", "SwapViaDeconstruction")] // not compatible with the old tfms, not worth the effort + private void PromoteSecondToFirst() + { + Type? previousTypeA = typeA; + typeA = typeB; + typeB = previousTypeA; + FastClonerCache.TypeCloneMetadata? previousMetadataA = metadataA; + metadataA = metadataB; + metadataB = previousMetadataA; + Func? previousRecursiveA = recursiveA; + recursiveA = recursiveB; + recursiveB = previousRecursiveA; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("ReSharper", "SwapViaDeconstruction")] + private void PromoteThirdToFirst() + { + Type? previousTypeA = typeA; + Type? previousTypeB = typeB; + typeA = typeC; + typeB = previousTypeA; + typeC = previousTypeB; + FastClonerCache.TypeCloneMetadata? previousMetadataA = metadataA; + FastClonerCache.TypeCloneMetadata? previousMetadataB = metadataB; + metadataA = metadataC; + metadataB = previousMetadataA; + metadataC = previousMetadataB; + Func? previousRecursiveA = recursiveA; + Func? previousRecursiveB = recursiveB; + recursiveA = recursiveC; + recursiveB = previousRecursiveA; + recursiveC = previousRecursiveB; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("ReSharper", "SwapViaDeconstruction")] + private void PromoteFourthToFirst() + { + Type? previousTypeA = typeA; + Type? previousTypeB = typeB; + Type? previousTypeC = typeC; + typeA = typeD; + typeB = previousTypeA; + typeC = previousTypeB; + typeD = previousTypeC; + FastClonerCache.TypeCloneMetadata? previousMetadataA = metadataA; + FastClonerCache.TypeCloneMetadata? previousMetadataB = metadataB; + FastClonerCache.TypeCloneMetadata? previousMetadataC = metadataC; + metadataA = metadataD; + metadataB = previousMetadataA; + metadataC = previousMetadataB; + metadataD = previousMetadataC; + Func? previousRecursiveA = recursiveA; + Func? previousRecursiveB = recursiveB; + Func? previousRecursiveC = recursiveC; + recursiveA = recursiveD; + recursiveB = previousRecursiveA; + recursiveC = previousRecursiveB; + recursiveD = previousRecursiveC; + } + } + + internal static FastClonerCache.TypeCloneMetadata GetTypeMetadata(Type type) => FastClonerCache.GetOrAddTypeMetadata(type, BuildTypeMetadata); + private static FastClonerCache.TypeCloneMetadata GetTypeMetadata(Type type, FastCloneState state) + { + if (state.TryGetCachedTypeMetadata(type, out FastClonerCache.TypeCloneMetadata cached)) + return cached; + FastClonerCache.TypeCloneMetadata metadata = FastClonerCache.GetOrAddTypeMetadata(type, BuildTypeMetadata); + state.CacheTypeMetadata(type, metadata); + return metadata; + } + + private static FastClonerCache.TypeCloneMetadata BuildTypeMetadata(Type type) + { + FastClonerCache.TypeShape typeShape = FastClonerExprGenerator.GetTypeShape(type); + bool isSafe = FastClonerSafeTypes.CanReturnSameObject(type) && (!type.IsValueType || type.IsPrimitive || type.IsEnum); + bool canHaveCycles = !type.IsValueType && FastClonerCache.GetOrAddCanHaveCycles(type, CalculateCanHaveCycles); + bool canSkipReferenceTracking = type.IsValueType && !ValueTypeContainsReferenceFieldsCached(type); + bool hasDirectSelfReference = canHaveCycles && typeShape.HasDirectSelfReference; + bool hasBehaviorSensitiveMembers = !FastCloner.DisableOptionalFeatures && typeShape.ContainsIgnoredMembers; + bool requiresSpecializedCloner = type.IsArray || FastClonerExprGenerator.IsDictionaryType(type) || FastClonerExprGenerator.IsSetType(type) || FastClonerExprGenerator.IsListType(type); + Func? recursive = isSafe ? null : (Func? )FastClonerCache.GetOrAddClass(type, t => GenerateClonerRecursive(t, canSkipReferenceTracking)); + FastClonerCache.CyclePolicy cyclePolicy = !canHaveCycles ? FastClonerCache.CyclePolicy.None : hasDirectSelfReference ? FastClonerCache.CyclePolicy.Worklist : FastClonerCache.CyclePolicy.TrackReferences; + return new FastClonerCache.TypeCloneMetadata + { + Type = type, + IsSafe = isSafe, + CanHaveCycles = canHaveCycles, + CanSkipReferenceTracking = canSkipReferenceTracking, + HasDirectSelfReference = hasDirectSelfReference, + HasBehaviorSensitiveMembers = hasBehaviorSensitiveMembers, + RequiresSpecializedCloner = requiresSpecializedCloner, + CollectionStrategy = requiresSpecializedCloner ? FastClonerCache.CollectionCloneStrategy.SpecializedRebuild : FastClonerCache.CollectionCloneStrategy.MemberwiseFast, + ExecutionMode = isSafe ? FastClonerCache.CloneExecutionMode.SafeReturn : FastClonerCache.CloneExecutionMode.MemberwiseThenPatch, + CyclePolicy = cyclePolicy, + RecursiveCloner = recursive + }; + } + + public static T? CloneObject(T? obj) + { + if (obj is null) + { + return default; + } + + Type concreteTypeOfObj = obj.GetType(); + Type typeOfT = typeof(T); + bool hasOptionalTypeOverrides = FastClonerCache.HasActiveTypeBehaviorOverrides; + if (!typeOfT.IsValueType && concreteTypeOfObj == typeOfT && !hasOptionalTypeOverrides) + { + if (TryCloneSafeArrayRoot(obj, concreteTypeOfObj, out T? safeArrayClone)) + return safeArrayClone; + ClonerCache.CacheEntry cacheEntry = ClonerCache.GetCurrent(); + if (cacheEntry.IsSafe) + { + return obj; + } + + if (cacheEntry.Cloner is not null) + { + return cacheEntry.CanUseNoTrackingState ? cacheEntry.Cloner(obj, FastCloneState.GetSimpleState()) : CloneRootWithTrackedState(obj, cacheEntry.Cloner, cacheEntry.Metadata!); + } + } + + if (hasOptionalTypeOverrides) + { + CloneBehavior? behavior = FastClonerCache.GetTypeBehavior(concreteTypeOfObj); + switch (behavior) + { + case CloneBehavior.Ignore: + return default; + case CloneBehavior.Reference: + return obj; + case CloneBehavior.Shallow: + return ShallowClonerGenerator.CloneObject(obj); + } + } + + if (FastClonerSafeTypes.DefaultKnownTypes.TryGetValue(concreteTypeOfObj, out _)) + { + return obj; + } + + switch (obj) + { + case ValueType: + { + if (typeOfT == concreteTypeOfObj) + { + bool hasIgnoredMembers = FastClonerExprGenerator.GetTypeShape(concreteTypeOfObj).ContainsIgnoredMembers; + if (hasIgnoredMembers || !FastClonerSafeTypes.CanReturnSameObject(concreteTypeOfObj)) + { + FastCloneState structState = FastCloneState.Rent(); + try + { + return CloneStructInternal(obj, structState); + } + finally + { + FastCloneState.Return(structState); + } + } + + return obj; + } + + break; + } + } + + if (!typeOfT.IsValueType && concreteTypeOfObj != typeOfT) + return (T? )ClonePolymorphic(obj, concreteTypeOfObj); + return (T? )CloneClassRoot(obj, concreteTypeOfObj); + } + + private static bool TryCloneSafeArrayRoot(T obj, Type runtimeType, out T? cloned) + { + cloned = default; + if (!runtimeType.IsArray || runtimeType.GetArrayRank() != 1) + return false; + Type? elementType = runtimeType.GetElementType(); + if (elementType is null || !FastClonerSafeTypes.CanReturnSameObject(elementType)) + return false; + Array array = (Array)(object)obj!; + cloned = (T)array.Clone(); + return true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static object? ClonePolymorphic(object obj, Type runtimeType) + { + if (!runtimeType.IsValueType && FastClonerSafeTypes.CanReturnSameObject(runtimeType) || runtimeType.IsPrimitive || runtimeType.IsEnum) + return obj; + if (obj is Delegate del) + { + Type? targetType = del.Target?.GetType(); + if (targetType is not null && FastClonerCache.GetOrAddCompilerGeneratedType(targetType, t => t.GetCustomAttribute()is not null)) + return CloneClassRoot(obj, runtimeType); + return obj; + } + + return CloneClassRoot(obj, runtimeType); + } + + private static object? CloneClassRoot(object obj, Type rootType) + { + FastClonerCache.TypeCloneMetadata metadata = GetTypeMetadata(rootType); + Func? cloner = metadata.RecursiveCloner; + // null -> should return same type + if (cloner is null) + { + return obj; + } + + if (metadata.CanSkipReferenceTracking || (metadata is { CyclePolicy: FastClonerCache.CyclePolicy.None, HasBehaviorSensitiveMembers: false } && !rootType.IsValueType)) + return cloner(obj, FastCloneState.GetSimpleState()); + return CloneRootWithTrackedState(obj, cloner, metadata); + } + + private static T CloneRootWithTrackedState(T obj, Func cloner, FastClonerCache.TypeCloneMetadata metadata) + { + FastCloneState state = FastCloneState.Rent(); + state.UseWorkList = metadata.CyclePolicy == FastClonerCache.CyclePolicy.Worklist; + try + { + T result; + if (!state.UseWorkList) + { + int current = state.IncrementDepth(); + if (current >= FastCloner.MaxRecursionDepth) + { + state.DecrementDepth(); + state.UseWorkList = true; + result = cloner(obj, state); + } + else + { + result = cloner(obj, state); + state.DecrementDepth(); + // if UseWorkList was set during recursive cloning, process the worklist + if (!state.UseWorkList) + { + return result; + } + } + } + else + { + result = cloner(obj, state); + } + + Type? lastWorkType = null; + Func? lastClonerTo = null; + while (state.TryPop(out object from, out object to, out Type type)) + { + // boxed value types - MemberwiseClone already created a value copy. + if (type.IsValueType()) + { + continue; + } + + Func clonerTo; + if (type == lastWorkType && lastClonerTo is not null) + { + clonerTo = lastClonerTo; + } + else + { + clonerTo = (Func)FastClonerCache.GetOrAddDeepClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, true)); + lastWorkType = type; + lastClonerTo = clonerTo; + } + + clonerTo(from, to, state); + } + + return result; + } + finally + { + FastCloneState.Return(state); + } + } + + private static bool CalculateCanHaveCycles(Type type) + { + if (type.IsValueType || FastClonerSafeTypes.CanReturnSameObject(type)) + return false; + if (type.IsArray) + { + Type? elementType = type.GetElementType(); + if (elementType is null) + return true; + if (FastClonerSafeTypes.CanReturnSameObject(elementType)) + return false; + if (elementType.IsValueType && !ValueTypeContainsReferenceFields(elementType, [])) + return false; + return true; + } + + Type? payloadType = GetCollectionPayloadType(type); + if (payloadType is not null) + { + if (FastClonerSafeTypes.CanReturnSameObject(payloadType)) + return false; + if (payloadType.IsValueType && !ValueTypeContainsReferenceFieldsCached(payloadType)) + return false; + } + + Type[] fieldTypes = GetCycleFieldTypes(type); + for (int i = 0; i < fieldTypes.Length; i++) + { + Type fieldType = fieldTypes[i]; + if (FastClonerSafeTypes.CanReturnSameObject(fieldType)) + continue; + if (!fieldType.IsValueType) + return true; + if (ValueTypeContainsReferenceFieldsCached(fieldType)) + return true; + } + + return false; + } + + private static bool ValueTypeContainsReferenceFields(Type valueType, HashSet visited) + { + if (!valueType.IsValueType || FastClonerSafeTypes.CanReturnSameObject(valueType)) + return false; + if (!visited.Add(valueType)) + return true; + try + { + Type[] fieldTypes = GetCycleFieldTypes(valueType); + for (int i = 0; i < fieldTypes.Length; i++) + { + Type fieldType = fieldTypes[i]; + if (FastClonerSafeTypes.CanReturnSameObject(fieldType)) + continue; + if (!fieldType.IsValueType) + return true; + if (ValueTypeContainsReferenceFields(fieldType, visited)) + return true; + } + + return false; + } + finally + { + visited.Remove(valueType); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ValueTypeContainsReferenceFieldsCached(Type valueType) + { + return FastClonerCache.GetOrAddValueTypeContainsReferences(valueType, static t => ValueTypeContainsReferenceFields(t, [])); + } + + private static Type[] GetCycleFieldTypes(Type type) + { + return FastClonerExprGenerator.GetTypeShape(type).CycleFieldTypes; + } + + private static Type? GetCollectionPayloadType(Type type) + { + return FastClonerCache.GetOrAddCollectionPayloadType(type, ResolveCollectionPayloadType); + } + + private static Type? ResolveCollectionPayloadType(Type type) + { + if (TryGetGenericInterfaceArgument(type, DictionaryInterfaceDefinition, 1, out Type? dictionaryValueType)) + return dictionaryValueType; + if (TryGetGenericInterfaceArgument(type, EnumerableInterfaceDefinition, 0, out Type? enumerableElementType)) + return enumerableElementType; + return null; + } + + private static bool TryGetGenericInterfaceArgument(Type type, Type genericInterfaceDefinition, int argIndex, out Type? argument) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == genericInterfaceDefinition) + { + Type[] args = type.GetGenericArguments(); + if ((uint)argIndex < (uint)args.Length) + { + argument = args[argIndex]; + return true; + } + } + + Type[] interfaces = type.GetInterfaces(); + for (int i = 0; i < interfaces.Length; i++) + { + Type current = interfaces[i]; + if (!current.IsGenericType || current.GetGenericTypeDefinition() != genericInterfaceDefinition) + continue; + Type[] args = current.GetGenericArguments(); + if ((uint)argIndex < (uint)args.Length) + { + argument = args[argIndex]; + return true; + } + } + + argument = null; + return false; + } + + internal static object? CloneClassInternal(object? obj, FastCloneState state) + { + if (obj is null) + return null; + Type runtimeType = obj.GetType(); + FastClonerCache.TypeCloneMetadata metadata = GetTypeMetadata(runtimeType, state); + if (!FastClonerCache.HasActiveTypeBehaviorOverrides && metadata.IsSafe) + return obj; + return CloneClassInternalTyped(obj, runtimeType, state, metadata); + } + + internal static object? CloneClassInternalNoTracking(object? obj, FastCloneState state) + { + if (obj is null) + return null; + Type objType = obj.GetType(); + FastClonerCache.TypeCloneMetadata metadata = GetTypeMetadata(objType, state); + if (metadata.IsSafe) + return obj; + Func? recursiveCloner = metadata.RecursiveCloner; + return recursiveCloner is null ? obj : recursiveCloner(obj, state); + } + + internal static object? CloneClassInternalTyped(object obj, Type objType, FastCloneState state) + { + FastClonerCache.TypeCloneMetadata metadata = GetTypeMetadata(objType, state); + return CloneClassInternalTyped(obj, objType, state, metadata); + } + + private static object? CloneClassInternalTyped(object obj, Type objType, FastCloneState state, FastClonerCache.TypeCloneMetadata metadata) + { + return CloneClassInternalResolved(obj, state, objType, metadata, metadata.RecursiveCloner); + } + + private static object? CloneClassInternalResolved(object obj, FastCloneState state, Type objType, FastClonerCache.TypeCloneMetadata metadata, Func? recursiveCloner) + { + if (TryGetNonCloningResult(obj, objType, metadata, recursiveCloner, out object? nonCloningResult)) + { + return nonCloningResult; + } + + object? knownRef = state.GetKnownRef(obj); + if (knownRef is not null) + return knownRef; + if (state.UseWorkList) + { + // value types: avoid the worklist because ClonerToExprGenerator.GenerateClonerInternal doesn't support value types + if (objType.IsValueType()) + { + object cloned = recursiveCloner(obj, state); + state.AddKnownRef(obj, cloned); + return cloned; + } + + return CloneClassShallowAndTrack(obj, state); + } + + bool depthIncremented = false; + try + { + depthIncremented = true; + int current = state.IncrementDepth(); + if (current >= FastCloner.MaxRecursionDepth) + { + state.DecrementDepth(); + depthIncremented = false; + state.UseWorkList = true; + // value types: avoid the worklist because ClonerToExprGenerator.GenerateClonerInternal doesn't support value types + if (objType.IsValueType()) + { + object cloned = recursiveCloner(obj, state); + state.AddKnownRef(obj, cloned); + return cloned; + } + + return CloneClassShallowAndTrack(obj, state); + } + + return recursiveCloner(obj, state); + } + finally + { + if (depthIncremented) + state.DecrementDepth(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetNonCloningResult(object obj, Type objType, FastClonerCache.TypeCloneMetadata metadata, Func? recursiveCloner, out object? result) + { + if (!FastClonerCache.HasActiveTypeBehaviorOverrides) + { + if (metadata.IsSafe || recursiveCloner is null) + { + result = obj; + return true; + } + } + else + { + if (!FastClonerCache.HasSafeTypeOverrides) + { + if (FastClonerSafeTypes.DefaultKnownTypes.ContainsKey(objType)) + { + result = obj; + return true; + } + + if (FastClonerCache.IsTypeIgnored(objType)) + { + result = null; + return true; + } + } + else + { + if (FastClonerCache.IsTypeIgnored(objType)) + { + result = null; + return true; + } + + if (FastClonerSafeTypes.DefaultKnownTypes.ContainsKey(objType)) + { + result = obj; + return true; + } + } + + if (recursiveCloner is null) + { + result = obj; + return true; + } + } + + result = null; + return false; + } + + internal static object? CloneClassShallowAndTrack(object? obj, FastCloneState state) + { + if (obj is null) + { + return null; + } + + Type objType = obj.GetType(); + if (FastClonerCache.HasActiveTypeBehaviorOverrides && FastClonerCache.IsTypeIgnored(objType)) + { + return null; + } + + // boxed structs can be mutated and must be deep-cloned + if (FastClonerSafeTypes.CanReturnSameObject(objType) && !objType.IsValueType()) + { + return obj; + } + + object? knownRef = state.GetKnownRef(obj); + if (knownRef is not null) + { + return knownRef; + } + + FastClonerCache.TypeCloneMetadata metadata = GetTypeMetadata(objType, state); + // internal structure of dictionaries, etc. needs to be rebuilt + if (metadata.RequiresSpecializedCloner) + { + Func? specialCloner = metadata.RecursiveCloner; + if (specialCloner is not null) + { + return specialCloner(obj, state); + } + } + + object shallow = ShallowObjectCloner.CloneObject(obj); + state.AddKnownRef(obj, shallow); + if (state.UseWorkList) + { + state.EnqueueProcess(obj, shallow, objType); + } + + return shallow; + } + + internal static T CloneStructInternal(T obj, FastCloneState state) + { + Type typeT = typeof(T); + Type? underlyingTypeT = Nullable.GetUnderlyingType(typeT); + // Check for custom type behaviors + CloneBehavior? behavior = null; + if (FastClonerCache.HasActiveTypeBehaviorOverrides) + { + behavior = FastClonerCache.GetTypeBehavior(typeT); + if (behavior is null && underlyingTypeT is not null) + behavior = FastClonerCache.GetTypeBehavior(underlyingTypeT); + } + + switch (behavior) + { + case CloneBehavior.Ignore: + return default !; + case CloneBehavior.Reference: + case CloneBehavior.Shallow: + return obj; + default: + { + // no loops, no nulls, no inheritance + Func? cloner = GetClonerForValueType(); + // safe object + return cloner is null ? obj : cloner(obj, state); + } + } + } + + // when we can't use code generation, we can use these methods + internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) + { + T[] outArray = FastArrayCopy.Clone(obj); + if (state.TrackReferences) + { + state.EnsureKnownRefCapacity(outArray.Length + 1); + state.AddKnownRef(obj, outArray); + } + + return outArray; + } + + internal static T[]? Clone1DimArrayStructInternal(T[]? obj, FastCloneState state) + { + // not null from called method, but will check it anyway + if (obj == null) + return null; + int l = obj.Length; + Func? cloner = GetClonerForValueType(); + if (cloner is null) + { + T[] clonedArray = FastArrayCopy.Clone(obj); + if (state.TrackReferences) + { + state.EnsureKnownRefCapacity(l + 1); + state.AddKnownRef(obj, clonedArray); + } + + return clonedArray; + } + + T[] outArray = new T[l]; + if (state.TrackReferences) + { + state.EnsureKnownRefCapacity(l + 1); + state.AddKnownRef(obj, outArray); + } + + for (int i = 0; i < l; i++) + outArray[i] = cloner(obj[i], state); + return outArray; + } + + internal static T[]? Clone1DimArrayClassInternal(T[]? obj, FastCloneState state) + { + // not null from called method, but will check it anyway + if (obj == null) + return null; + int l = obj.Length; + T[] outArray = new T[l]; + state.EnsureKnownRefCapacity(l + 1); + if (state.UseWorkList) + state.EnsureWorkQueueCapacity(l); + state.AddKnownRef(obj, outArray); + Type declaredType = typeof(T); + if (!declaredType.IsValueType && FastClonerSafeTypes.CanReturnSameObject(declaredType) && !FastClonerCache.HasActiveTypeBehaviorOverrides) + { + Array.Copy(obj, outArray, l); + return outArray; + } + + bool hasOptionalTypeOverrides = FastClonerCache.HasActiveTypeBehaviorOverrides; + TypeCloneDispatchCache dispatch = default; + Type? lastType = null; + FastClonerCache.TypeCloneMetadata? lastMetadata = null; + Func? lastRecursiveCloner = null; + for (int i = 0; i < l; i++) + { + T? item = obj[i]; + if (item is null) + { + outArray[i] = item; + continue; + } + + Type runtimeType = item.GetType(); + if (!hasOptionalTypeOverrides && ((!runtimeType.IsValueType && FastClonerSafeTypes.CanReturnSameObject(runtimeType)) || runtimeType.IsPrimitive || runtimeType.IsEnum)) + { + outArray[i] = item; + continue; + } + + FastClonerCache.TypeCloneMetadata metadata; + Func? recursiveCloner; + if (runtimeType == lastType && lastMetadata is not null) + { + metadata = lastMetadata; + recursiveCloner = lastRecursiveCloner; + } + else + { + dispatch.Resolve(runtimeType, state, out metadata, out recursiveCloner); + lastType = runtimeType; + lastMetadata = metadata; + lastRecursiveCloner = recursiveCloner; + } + + outArray[i] = (T)CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner)!; + } + + return outArray; + } + + internal static List? CloneListSafeInternal(List? obj, FastCloneState state) + { + if (obj is null) + return null; + int count = obj.Count; + List result = new List(count); + state.AddKnownRef(obj, result); + CollectionsMarshal.SetCount(result, count); + ReadOnlySpan srcSpan = CollectionsMarshal.AsSpan(obj); + Span destSpan = CollectionsMarshal.AsSpan(result); + srcSpan.CopyTo(destSpan); + return result; + } + + internal static List? CloneListStructInternal(List? obj, FastCloneState state) + where T : struct + { + if (obj is null) + return null; + int count = obj.Count; + List result = new List(count); + state.AddKnownRef(obj, result); + Func? cloner = GetClonerForValueType(); + if (cloner is null) + { + CollectionsMarshal.SetCount(result, count); + ReadOnlySpan srcSpan = CollectionsMarshal.AsSpan(obj); + Span destSpan = CollectionsMarshal.AsSpan(result); + srcSpan.CopyTo(destSpan); + return result; + } + + CollectionsMarshal.SetCount(result, count); + Span span = CollectionsMarshal.AsSpan(result); + for (int i = 0; i < count; i++) + span[i] = cloner(obj[i], state); + return result; + } + + internal static List? CloneListClassInternal(List? obj, FastCloneState state) + where T : class + { + if (obj is null) + return null; + int count = obj.Count; + state.EnsureKnownRefCapacity(count + 1); + if (state.UseWorkList) + state.EnsureWorkQueueCapacity(count); + List result = new List(count); + state.AddKnownRef(obj, result); + Type declaredType = typeof(T); + CollectionsMarshal.SetCount(result, count); + ReadOnlySpan srcSpan = CollectionsMarshal.AsSpan(obj); + Span span = CollectionsMarshal.AsSpan(result); + if (declaredType.IsSealed) + { + FastClonerCache.TypeCloneMetadata metadata = GetTypeMetadata(declaredType, state); + Func? recursiveCloner = metadata.RecursiveCloner; + for (int i = 0; i < count; i++) + { + T? item = srcSpan[i]; + span[i] = item is null ? null : (T? )CloneClassInternalResolved(item, state, declaredType, metadata, recursiveCloner); + } + + return result; + } + + TypeCloneDispatchCache dispatch = default; + Type? lastType = null; + FastClonerCache.TypeCloneMetadata? lastMetadata = null; + Func? lastRecursiveCloner = null; + for (int i = 0; i < count; i++) + { + T? item = srcSpan[i]; + if (item is null) + { + span[i] = null; + continue; + } + + Type runtimeType = item.GetType(); + FastClonerCache.TypeCloneMetadata metadata; + Func? recursiveCloner; + if (runtimeType == lastType && lastMetadata is not null) + { + metadata = lastMetadata; + recursiveCloner = lastRecursiveCloner; + } + else + { + dispatch.Resolve(runtimeType, state, out metadata, out recursiveCloner); + lastType = runtimeType; + lastMetadata = metadata; + lastRecursiveCloner = recursiveCloner; + } + + span[i] = (T? )CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner); + } + + return result; + } + + internal static Dictionary? CloneDictionarySafeInternal(Dictionary? obj, FastCloneState state) + where TKey : notnull + { + if (obj is null) + return null; + Dictionary result = new Dictionary(obj.Count, obj.Comparer); + state.AddKnownRef(obj, result); + bool ignoreValues = FastClonerCache.HasActiveTypeBehaviorOverrides && FastClonerCache.IsTypeIgnored(typeof(TValue)); + foreach (KeyValuePair kvp in obj) + { + ref TValue valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + valueRef = ignoreValues ? default ! : kvp.Value; + } + + return result; + } + + internal static Dictionary? CloneDictionaryStructValueInternal(Dictionary? obj, FastCloneState state) + where TKey : notnull where TValue : struct + { + if (obj is null) + return null; + Dictionary result = new Dictionary(obj.Count, obj.Comparer); + state.AddKnownRef(obj, result); + bool ignoreValues = FastClonerCache.HasActiveTypeBehaviorOverrides && FastClonerCache.IsTypeIgnored(typeof(TValue)); + if (ignoreValues) + { + foreach (KeyValuePair kvp in obj) + { + ref TValue valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + valueRef = default; + } + + return result; + } + + Func? cloner = GetClonerForValueType(); + if (cloner is null) + { + foreach (KeyValuePair kvp in obj) + { + ref TValue valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + valueRef = kvp.Value; + } + } + else + { + foreach (KeyValuePair kvp in obj) + { + ref TValue valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + valueRef = cloner(kvp.Value, state); + } + } + + return result; + } + + internal static Dictionary? CloneDictionaryClassValueInternal(Dictionary? obj, FastCloneState state) + where TKey : notnull where TValue : class + { + if (obj is null) + return null; + state.EnsureKnownRefCapacity(obj.Count + 1); + Type declaredType = typeof(TValue); + bool ignoreDeclaredValues = FastClonerCache.HasActiveTypeBehaviorOverrides && FastClonerCache.IsTypeIgnored(declaredType); + if (declaredType == typeof(object)) + return (Dictionary? )(object? )CloneDictionaryObjectValueInternal((Dictionary)(object)obj, state); + Dictionary result = new Dictionary(obj.Count, obj.Comparer); + state.AddKnownRef(obj, result); + if (declaredType.IsSealed) + { + FastClonerCache.TypeCloneMetadata? metadata = ignoreDeclaredValues ? null : GetTypeMetadata(declaredType, state); + Func? recursiveCloner = metadata?.RecursiveCloner; + foreach (KeyValuePair kvp in obj) + { + TValue? cloned; + if (kvp.Value is null || ignoreDeclaredValues) + { + cloned = null; + } + else + { + cloned = (TValue? )CloneClassInternalResolved(kvp.Value, state, declaredType, metadata!, recursiveCloner); + } + + ref TValue? valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + valueRef = cloned; + } + + return result; + } + + Type? lastType = null; + FastClonerCache.TypeCloneMetadata? lastMetadata = null; + Func? lastRecursive = null; + foreach (KeyValuePair kvp in obj) + { + TValue? value = kvp.Value; + if (value is null) + { + ref TValue? nullRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + nullRef = null; + continue; + } + + Type runtimeType = value.GetType(); + if (FastClonerCache.HasActiveTypeBehaviorOverrides && FastClonerCache.IsTypeIgnored(runtimeType)) + { + ref TValue? ignoredRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + ignoredRef = null; + continue; + } + + if (FastClonerSafeTypes.CanReturnSameObject(runtimeType)) + { + ref TValue? valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + valueRef = value; + continue; + } + + FastClonerCache.TypeCloneMetadata metadata; + if (runtimeType == lastType && lastMetadata is not null) + { + metadata = lastMetadata; + } + else + { + metadata = lastMetadata = GetTypeMetadata(runtimeType, state); + lastRecursive = metadata.RecursiveCloner; + lastType = runtimeType; + } + + TValue? cloned = (TValue? )CloneClassInternalResolved(value, state, runtimeType, metadata, lastRecursive); + ref TValue? clonedRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + clonedRef = cloned; + } + + return result; + } + + private static Dictionary CloneDictionaryObjectValueInternal(Dictionary obj, FastCloneState state) + where TKey : notnull + { + Dictionary result = new Dictionary(obj.Count, obj.Comparer); + state.AddKnownRef(obj, result); + TypeCloneDispatchCache dispatch = default; + Type? lastType = null; + FastClonerCache.TypeCloneMetadata? lastMetadata = null; + Func? lastRecursiveCloner = null; + foreach (KeyValuePair kvp in obj) + { + object? value = kvp.Value; + if (value is null) + { + ref object? nullRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + nullRef = null; + continue; + } + + Type runtimeType = value.GetType(); + if (FastClonerSafeTypes.CanReturnSameObject(runtimeType)) + { + ref object? safeRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + safeRef = value; + continue; + } + + FastClonerCache.TypeCloneMetadata metadata; + Func? recursiveCloner; + if (runtimeType == lastType && lastMetadata is not null) + { + metadata = lastMetadata; + recursiveCloner = lastRecursiveCloner; + } + else + { + dispatch.Resolve(runtimeType, state, out metadata, out recursiveCloner); + lastType = runtimeType; + lastMetadata = metadata; + lastRecursiveCloner = recursiveCloner; + } + + object cloned = CloneClassInternalResolved(value, state, runtimeType, metadata, recursiveCloner)!; + ref object? clonedRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); + clonedRef = cloned; + } + + return result; + } + + // relatively frequent case. specially handled + internal static T[, ]? Clone2DimArrayInternal(T[, ]? obj, FastCloneState state) + { + // not null from called method, but will check it anyway + if (obj is null) + { + return null; + } + + // we cannot determine by type multidim arrays (one dimension is possible) + // so, will check for index here + int lb1 = obj.GetLowerBound(0); + int lb2 = obj.GetLowerBound(1); + if (lb1 != 0 || lb2 != 0) + return (T[, ])CloneAbstractArrayInternal(obj, state)!; + int l1 = obj.GetLength(0); + int l2 = obj.GetLength(1); + T[, ] outArray = new T[l1, l2]; + state.AddKnownRef(obj, outArray); + if (FastClonerSafeTypes.CanReturnSameObject(typeof(T))) + { + Array.Copy(obj, outArray, obj.Length); + return outArray; + } + + if (typeof(T).IsValueType()) + { + Func? cloner = GetClonerForValueType(); + if (cloner is null) + { + Array.Copy(obj, outArray, obj.Length); + return outArray; + } + + for (int i = 0; i < l1; i++) + for (int k = 0; k < l2; k++) + outArray[i, k] = cloner(obj[i, k], state); + } + else + { + for (int i = 0; i < l1; i++) + for (int k = 0; k < l2; k++) + outArray[i, k] = (T)CloneClassInternal(obj[i, k], state)!; + } + + return outArray; + } + + internal static Array? CloneAbstractArrayInternal(Array? obj, FastCloneState state) + { + // not null from called method, but will check it anyway + if (obj == null) + return null; + int rank = obj.Rank; + int[] lengths = new int[rank]; + int[] lowerBounds = new int[rank]; + bool hasZeroLength = false; + for (int i = 0; i < rank; i++) + { + int length = obj.GetLength(i); + int lowerBound = obj.GetLowerBound(i); + lengths[i] = length; + lowerBounds[i] = lowerBound; + hasZeroLength |= length == 0; + } + + Type elementType = obj.GetType().GetElementType()!; + Array outArray = Array.CreateInstance(elementType, lengths, lowerBounds); + state.AddKnownRef(obj, outArray); + // we're unable to set any value to this array, so, just return it + if (hasZeroLength) + return outArray; + if (FastClonerSafeTypes.CanReturnSameObject(elementType)) + { + Array.Copy(obj, outArray, obj.Length); + return outArray; + } + + switch (rank) + { + case 1: + { + int lower = lowerBounds[0]; + int upper = lower + lengths[0]; + for (int i = lower; i < upper; i++) + outArray.SetValue(CloneClassInternal(obj.GetValue(i), state), i); + return outArray; + } + + case 2: + { + int lower0 = lowerBounds[0]; + int lower1 = lowerBounds[1]; + int upper0 = lower0 + lengths[0]; + int upper1 = lower1 + lengths[1]; + for (int i = lower0; i < upper0; i++) + for (int k = lower1; k < upper1; k++) + outArray.SetValue(CloneClassInternal(obj.GetValue(i, k), state), i, k); + return outArray; + } + } + + int[] idxes = new int[rank]; + Array.Copy(lowerBounds, idxes, rank); + int ofs = rank - 1; + while (true) + { + outArray.SetValue(CloneClassInternal(obj.GetValue(idxes), state), idxes); + idxes[ofs]++; + if (idxes[ofs] >= lowerBounds[ofs] + lengths[ofs]) + { + do + { + if (ofs == 0) + return outArray; + idxes[ofs] = lowerBounds[ofs]; + ofs--; + idxes[ofs]++; + } + while (idxes[ofs] >= lowerBounds[ofs] + lengths[ofs]); + ofs = rank - 1; + } + } + } + + internal static Func? GetClonerForValueType() => (Func? )FastClonerCache.GetOrAddStructAsObject(typeof(T), t => GenerateCloner(t, false)); + private static object? GenerateClonerRecursive(Type t, bool skipCycleTracking) + { + if (FastClonerSafeTypes.CanReturnSameObject(t) && !t.IsValueType()) + return null; + return FastClonerExprGenerator.GenerateClonerInternal(t, asObject: true, skipCycleTracking: skipCycleTracking, useShallowClassClone: false); + } + + private static object? GenerateCloner(Type t, bool asObject) + { + if (FastClonerSafeTypes.CanReturnSameObject(t) && asObject && !t.IsValueType()) + return null; + bool skipCycleTracking = !asObject && t.IsValueType && !ValueTypeContainsReferenceFieldsCached(t); + return FastClonerExprGenerator.GenerateClonerInternal(t, asObject, skipCycleTracking: skipCycleTracking, useShallowClassClone: false); + } + + public static object? CloneObjectTo(object? objFrom, object? objTo, bool isDeep) + { + if (objTo == null) + return null; + if (objFrom == null) + throw new ArgumentNullException(nameof(objFrom), "Cannot copy null object to another"); + Type type = objFrom.GetType(); + if (!type.IsInstanceOfType(objTo)) + throw new InvalidOperationException("From object should be derived from From object, but From object has type " + objFrom.GetType().FullName + " and to " + objTo.GetType().FullName); + if (objFrom is string) + throw new InvalidOperationException("It is forbidden to clone strings"); + Func? cloner = (Func? )(isDeep ? FastClonerCache.GetOrAddDeepClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, true)) : FastClonerCache.GetOrAddShallowClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, false))); + if (cloner is null) + return objTo; + FastCloneState state = FastCloneState.Rent(); + try + { + object result = cloner(objFrom, objTo, state); + if (!isDeep) + { + return result; + } + + Type? lastWorkType = null; + Func? lastClonerTo = null; + while (state.TryPop(out object from, out object to, out Type workItemType)) + { + Func clonerTo; + if (workItemType == lastWorkType && lastClonerTo is not null) + { + clonerTo = lastClonerTo; + } + else + { + clonerTo = (Func)FastClonerCache.GetOrAddDeepClassTo(workItemType, t => ClonerToExprGenerator.GenerateClonerInternal(t, true)); + lastWorkType = workItemType; + lastClonerTo = clonerTo; + } + + clonerTo(from, to, state); + } + + return result; + } + finally + { + FastCloneState.Return(state); + } + } +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerIgnoreAttribute.cs b/src/Foundatio/FastCloner/Code/FastClonerIgnoreAttribute.cs new file mode 100644 index 000000000..84088a637 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerIgnoreAttribute.cs @@ -0,0 +1,45 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System; + +namespace Foundatio.FastCloner.Code; +/// +/// Marks a member or type as ignored, effectively assigning a default value when cloning. +/// When applied to a type, all usages of that type will be set to default. +/// When applied to a member, that specific member will be set to default. +/// +/// +/// +/// // Type-level: all usages of CancellationToken get default value +/// [FastClonerIgnore] +/// public struct MyCancellationWrapper { } +/// +/// // Member-level +/// public class MyClass +/// { +/// [FastClonerIgnore] +/// public CancellationToken Token { get; set; } +/// } +/// +/// +/// +/// This attribute is a shorthand for [FastClonerBehavior(CloneBehavior.Ignore)]. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] +internal class FastClonerIgnoreAttribute : FastClonerBehaviorAttribute +{ + /// + /// Initializes a new instance of . + /// + /// Whether the member/type should be ignored during cloning. Default is true. + public FastClonerIgnoreAttribute(bool ignored = true) : base(ignored ? CloneBehavior.Ignore : CloneBehavior.Clone) + { + } + + /// + /// Gets whether the member/type should be ignored during cloning. + /// + public bool Ignored => Behavior == CloneBehavior.Ignore; +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerReferenceAttribute.cs b/src/Foundatio/FastCloner/Code/FastClonerReferenceAttribute.cs new file mode 100644 index 000000000..71c712406 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerReferenceAttribute.cs @@ -0,0 +1,40 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System; + +namespace Foundatio.FastCloner.Code; +/// +/// Marks a member or type to preserve the original reference during cloning. +/// When applied to a type, all usages of that type will share the same reference. +/// When applied to a member, that specific member will share the same reference. +/// This is useful for shared services, singletons, or immutable objects that should not be duplicated. +/// +/// +/// +/// // Type-level: all usages of ILogger preserve reference +/// [FastClonerReference] +/// public class LoggerService : ILogger { } +/// +/// // Member-level: only this property preserves reference +/// public class MyClass +/// { +/// [FastClonerReference] +/// public ILogger Logger { get; set; } +/// } +/// +/// +/// +/// This attribute is a shorthand for [FastClonerBehavior(CloneBehavior.Reference)]. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] +internal class FastClonerReferenceAttribute : FastClonerBehaviorAttribute +{ + /// + /// Initializes a new instance of . + /// + public FastClonerReferenceAttribute() : base(CloneBehavior.Reference) + { + } +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerSafeHandleAttribute.cs b/src/Foundatio/FastCloner/Code/FastClonerSafeHandleAttribute.cs new file mode 100644 index 000000000..9c4304261 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerSafeHandleAttribute.cs @@ -0,0 +1,16 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System; + +namespace Foundatio.FastCloner; +/// +/// Apply this attribute to a struct to indicate it should be treated as a "Safe Handle" or identity-preserving type. +/// When applied, FastCloner will NOT attempt to deep-clone readonly fields on this struct, preventing +/// identity breakage for types that rely on specific internal singleton or handle references. +/// +[AttributeUsage(AttributeTargets.Struct)] +internal class FastClonerSafeHandleAttribute : Attribute +{ +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerSafeTypes.cs b/src/Foundatio/FastCloner/Code/FastClonerSafeTypes.cs new file mode 100644 index 000000000..a1ac8cbbf --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerSafeTypes.cs @@ -0,0 +1,274 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Collections.Concurrent; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Foundatio.FastCloner.Code; +/// +/// Safe types are types, which can be copied without real cloning. e.g. simple structs or strings (it is immutable) +/// +internal static class FastClonerSafeTypes +{ + internal static readonly Dictionary DefaultKnownTypes = new Dictionary(64) + { + // Primitives + [typeof(byte)] = true, + [typeof(short)] = true, + [typeof(ushort)] = true, + [typeof(int)] = true, + [typeof(uint)] = true, + [typeof(long)] = true, + [typeof(ulong)] = true, + [typeof(float)] = true, + [typeof(double)] = true, + [typeof(decimal)] = true, + [typeof(string)] = true, + [typeof(char)] = true, + [typeof(bool)] = true, + [typeof(sbyte)] = true, + [typeof(nint)] = true, + [typeof(nuint)] = true, + [typeof(Guid)] = true, + [typeof(Rune)] = true, + // Time-related types + [typeof(TimeSpan)] = true, + [typeof(TimeZoneInfo)] = true, + [typeof(DateTime)] = true, + [typeof(DateTimeOffset)] = true, + [typeof(DateOnly)] = true, + [typeof(TimeOnly)] = true, + // Numeric types + [typeof(Half)] = true, + [typeof(Int128)] = true, + [typeof(UInt128)] = true, + [typeof(Complex)] = true, + // Others + [typeof(DBNull)] = true, + [StringComparer.Ordinal.GetType()] = true, + [StringComparer.OrdinalIgnoreCase.GetType()] = true, + [StringComparer.InvariantCulture.GetType()] = true, + [StringComparer.InvariantCultureIgnoreCase.GetType()] = true, + [typeof(WeakReference)] = true, + [typeof(WeakReference<>)] = true, + [typeof(CancellationTokenSource)] = true, + [typeof(Range)] = true, + [typeof(Index)] = true + }; + private static readonly ConcurrentDictionary knownTypes = []; + static FastClonerSafeTypes() + { + InitializeKnownTypes(); + } + + private static void InitializeKnownTypes() + { + foreach (KeyValuePair x in DefaultKnownTypes) + { + knownTypes.TryAdd(x.Key, x.Value); + } + + List safeTypes = [Type.GetType("System.RuntimeType"), Type.GetType("System.RuntimeTypeHandle")]; + foreach (Type x in safeTypes.OfType()) + { + knownTypes.TryAdd(x, true); + } + } + + private static bool IsSpecialEqualityComparer(string fullName) => fullName switch + { + _ when fullName.StartsWith("System.Collections.Generic.GenericEqualityComparer`") => true, + _ when fullName.StartsWith("System.Collections.Generic.ObjectEqualityComparer`") => true, + _ when fullName.StartsWith("System.Collections.Generic.EnumEqualityComparer`") => true, + _ when fullName.StartsWith("System.Collections.Generic.NullableEqualityComparer`") => true, + "System.Collections.Generic.ByteEqualityComparer" => true, + "System.Collections.Generic.StringEqualityComparer" => true, + _ => false + }; + private static class TypePrefixes + { + public const string SystemReflection = "System.Reflection."; + public const string SystemRuntimeType = "System.RuntimeType"; + public const string MicrosoftExtensions = "Microsoft.Extensions.DependencyInjection."; + } + + private static readonly Assembly propertyInfoAssembly = typeof(PropertyInfo).Assembly; + private static bool IsReflectionType(Type type) + { + if (type == typeof(AssemblyName)) + { + return false; + } + + return type.FullName?.StartsWith(TypePrefixes.SystemReflection)is true && Equals(type.GetTypeInfo().Assembly, typeof(PropertyInfo).GetTypeInfo().Assembly); + } + + private static IEnumerable GetAllTypeFields(Type type) + { + Type? currentType = type; + while (currentType is not null) + { + foreach (FieldInfo field in currentType.GetAllFields()) + { + yield return field; + } + + currentType = currentType.BaseType(); + } + } + + private static bool IsAnonymousType(Type type) + { + return FastClonerCache.GetOrAddAnonymousTypeStatus(type, t => t.IsClass && t is { IsSealed: true, IsNotPublic: true } && t.IsDefined(typeof(CompilerGeneratedAttribute), false) && (t.Name.StartsWith("<>") || t.Name.StartsWith("VB$")) && t.Name.Contains("AnonymousType")); + } + + private static readonly HashSet safeTypeExact = new HashSet(StringComparer.Ordinal) + { + "System.Threading.Tasks.Task", + "Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector" + }; + private static readonly AhoCorasick safeTypePrefixes = new AhoCorasick([TypePrefixes.SystemRuntimeType, TypePrefixes.MicrosoftExtensions, "System.Threading.Tasks.Task`"]); + private static bool IsSafeSystemType(Type type) + { + if (type.IsEnum() || type.IsPointer) + return true; + if (type.IsCOMObject) + return true; + string? fullName = type.FullName; + if (fullName is null) + return true; + if (safeTypeExact.Contains(fullName)) + return true; + if (safeTypePrefixes.ContainsAnyPattern(fullName)) + return true; + if (IsReflectionType(type)) + return true; + if (type.IsSubclassOf(typeof(System.Runtime.ConstrainedExecution.CriticalFinalizerObject))) + return true; + return false; + } + + private static bool CanReturnSameType(Type type, HashSet? processingTypes = null) + { + if (FastClonerCache.IsTypeReference(type)) + { + return true; + } + + if (knownTypes.TryGetValue(type, out bool isSafe)) + { + return isSafe; + } + + if (type.IsGenericType) + { + Type? genericDef = type.GetGenericTypeDefinition(); + if (knownTypes.TryGetValue(genericDef, out bool isGenericSafe)) + { + knownTypes.TryAdd(type, isGenericSafe); + return isGenericSafe; + } + } + + if (typeof(Delegate).IsAssignableFrom(type)) + { + knownTypes.TryAdd(type, true); + return true; + } + + string? fullName = type.FullName; + if (fullName is null || IsSafeSystemType(type) || fullName.Contains("EqualityComparer") && IsSpecialEqualityComparer(fullName)) + { + knownTypes.TryAdd(type, true); + return true; + } + + if (!IsAnonymousType(type) && !type.IsValueType()) + { + knownTypes.TryAdd(type, false); + return false; + } + + processingTypes ??= []; + if (!processingTypes.Add(type)) + { + return true; + } + + foreach (FieldInfo fieldInfo in GetAllTypeFields(type)) + { + Type fieldType = fieldInfo.FieldType; + if (processingTypes.Contains(fieldType)) + { + continue; + } + + if (CanReturnSameType(fieldType, processingTypes)) + { + continue; + } + + knownTypes.TryAdd(type, false); + return false; + } + + knownTypes.TryAdd(type, true); + return true; + } + + public static bool CanReturnSameObject(Type type) => CanReturnSameType(type); + internal static void ClearKnownTypesCache() + { + knownTypes.Clear(); + InitializeKnownTypes(); + } + + /// + /// Determines whether GetHashCode() result won't change after deep cloning (best effort). + /// + internal static bool HasStableHashSemantics(Type type) + { + return FastClonerCache.GetOrAddStableHashSemantics(type, CalculateHasStableHashSemantics); + } + + private static bool CalculateHasStableHashSemantics(Type type) + { + // Primitives are always stable - their hash is based on their value + if (type.IsPrimitive) + return true; + // String is immutable and has value-based hash + if (type == typeof(string)) + return true; + // Enums are always stable - hash is based on underlying value + if (type.IsEnum) + return true; + // Value types: even if they don't override GetHashCode, their fields are copied + // so the hash remains consistent after cloning + if (type.IsValueType) + return true; + // Known safe types from our dictionary are stable + if (DefaultKnownTypes.ContainsKey(type)) + return true; + // Check if the type overrides GetHashCode (not using object.GetHashCode) + // If a type has overridden GetHashCode, it's using value-based hashing + MethodInfo? getHashCodeMethod = type.GetMethod("GetHashCode", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null); + if (getHashCodeMethod is not null && getHashCodeMethod.DeclaringType != typeof(object)) + { + // Type has custom GetHashCode implementation + // This indicates value-based equality semantics + return true; + } + + // Reference types using default GetHashCode use identity-based hash + // These are NOT stable after cloning + return false; + } +} diff --git a/src/Foundatio/FastCloner/Code/FastClonerShallowAttribute.cs b/src/Foundatio/FastCloner/Code/FastClonerShallowAttribute.cs new file mode 100644 index 000000000..2bbbd2088 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FastClonerShallowAttribute.cs @@ -0,0 +1,40 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System; + +namespace Foundatio.FastCloner.Code; +/// +/// Marks a member or type for shallow cloning instead of deep cloning. +/// When applied to a type, all usages perform MemberwiseClone without recursing into members. +/// When applied to a member, that reference is copied directly without deep cloning its contents. +/// This is useful for parent references, shared state, or when deep cloning would cause issues. +/// +/// +/// +/// // Type-level: shallow clone the entire Config type +/// [FastClonerShallow] +/// public class Config { } +/// +/// // Member-level: shallow clone only this member +/// public class Node +/// { +/// [FastClonerShallow] +/// public ParentObject Parent { get; set; } +/// } +/// +/// +/// +/// This attribute is a shorthand for [FastClonerBehavior(CloneBehavior.Shallow)]. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] +internal class FastClonerShallowAttribute : FastClonerBehaviorAttribute +{ + /// + /// Initializes a new instance of . + /// + public FastClonerShallowAttribute() : base(CloneBehavior.Shallow) + { + } +} diff --git a/src/Foundatio/FastCloner/Code/FieldAccessorGenerator.cs b/src/Foundatio/FastCloner/Code/FieldAccessorGenerator.cs new file mode 100644 index 000000000..1d90e3369 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/FieldAccessorGenerator.cs @@ -0,0 +1,38 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Linq.Expressions; +using System.Reflection; +using System; + +namespace Foundatio.FastCloner.Code; +internal static class FieldAccessorGenerator +{ + internal static Action GetFieldSetter(FieldInfo field) + { + return (Action)FastClonerCache.GetOrAddField(field.DeclaringType, field.Name, _ => CreateFieldSetter(field)); + } + + private static Action CreateFieldSetter(FieldInfo field) + { + ParameterExpression targetParam = Expression.Parameter(typeof(object), "target"); + ParameterExpression valueParam = Expression.Parameter(typeof(object), "value"); + UnaryExpression targetCast = Expression.Convert(targetParam, field.DeclaringType); + Expression body; + if (field.IsInitOnly) + { + MethodInfo setValueMethod = typeof(FieldInfo).GetMethod(nameof(FieldInfo.SetValue), [typeof(object), typeof(object)])!; + UnaryExpression valueCast = Expression.Convert(valueParam, typeof(object)); + body = Expression.Call(Expression.Constant(field), setValueMethod, targetCast, valueCast); + } + else + { + UnaryExpression valueCast = Expression.Convert(valueParam, field.FieldType); + body = Expression.Assign(Expression.Field(targetCast, field), valueCast); + } + + Expression> lambda = Expression.Lambda>(body, targetParam, valueParam); + return lambda.Compile(); + } +} diff --git a/src/Foundatio/FastCloner/Code/Polyfill.cs b/src/Foundatio/FastCloner/Code/Polyfill.cs new file mode 100644 index 000000000..f147ff1e7 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/Polyfill.cs @@ -0,0 +1,5 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + + diff --git a/src/Foundatio/FastCloner/Code/ReflectionHelper.cs b/src/Foundatio/FastCloner/Code/ReflectionHelper.cs new file mode 100644 index 000000000..771e929ab --- /dev/null +++ b/src/Foundatio/FastCloner/Code/ReflectionHelper.cs @@ -0,0 +1,25 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Reflection; +using System; + +namespace Foundatio.FastCloner.Code; +internal static class ReflectionHelper +{ + extension (Type t) + { + public bool IsEnum() => t.IsEnum; + public bool IsValueType() => t.IsValueType; + public bool IsClass() => t.IsClass; + public Type? BaseType() => t.BaseType; + public FieldInfo[] GetAllFields() => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + public PropertyInfo[] GetPublicProperties() => t.GetProperties(BindingFlags.Instance | BindingFlags.Public); + public FieldInfo[] GetDeclaredFields() => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); + public ConstructorInfo[] GetPublicConstructors() => t.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + public MethodInfo? GetPrivateMethod(string methodName) => t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); + public MethodInfo? GetPrivateStaticMethod(string methodName) => t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static); + public Type[] GenericArguments() => t.GetGenericArguments(); + } +} diff --git a/src/Foundatio/FastCloner/Code/ShallowClonerGenerator.cs b/src/Foundatio/FastCloner/Code/ShallowClonerGenerator.cs new file mode 100644 index 000000000..44c7d537c --- /dev/null +++ b/src/Foundatio/FastCloner/Code/ShallowClonerGenerator.cs @@ -0,0 +1,23 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System; + +namespace Foundatio.FastCloner.Code; +internal static class ShallowClonerGenerator +{ + public static T? CloneObject(T obj) + { + if (typeof(T).IsValueType) + return obj; + if (obj is null) + return default; + Type runtimeType = obj.GetType(); + if (runtimeType.IsValueType) + return (T)ShallowObjectCloner.DirectCloneObject(obj); + if (FastClonerSafeTypes.CanReturnSameObject(runtimeType)) + return obj; + return (T)ShallowObjectCloner.DirectCloneObject(obj); + } +} diff --git a/src/Foundatio/FastCloner/Code/ShallowObjectCloner.cs b/src/Foundatio/FastCloner/Code/ShallowObjectCloner.cs new file mode 100644 index 000000000..accd0d72d --- /dev/null +++ b/src/Foundatio/FastCloner/Code/ShallowObjectCloner.cs @@ -0,0 +1,17 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Runtime.CompilerServices; + +namespace Foundatio.FastCloner.Code; +/// +/// Internal helper class used to perform shallow object cloning +/// +internal static class ShallowObjectCloner +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object CloneObject(object obj) => DirectCloneObject(obj); + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "MemberwiseClone")] + public static extern object DirectCloneObject(object obj); +} diff --git a/src/Foundatio/FastCloner/Code/StaticMethodInfos.cs b/src/Foundatio/FastCloner/Code/StaticMethodInfos.cs new file mode 100644 index 000000000..bd3fc2bf5 --- /dev/null +++ b/src/Foundatio/FastCloner/Code/StaticMethodInfos.cs @@ -0,0 +1,41 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using System.Collections.Concurrent; +using System.Collections; +using System.Reflection; +using System; + +namespace Foundatio.FastCloner.Code; +internal static class StaticMethodInfos +{ + internal static class DeepCloneStateProperties + { + internal static readonly PropertyInfo UseWorkList = typeof(FastCloneState).GetProperty(nameof(FastCloneState.UseWorkList))!; + } + + internal static class DeepCloneStateMethods + { + internal static readonly MethodInfo AddKnownRef = typeof(FastCloneState).GetMethod(nameof(FastCloneState.AddKnownRef))!; + } + + internal static class DeepClonerGeneratorMethods + { + internal static readonly MethodInfo CloneStructInternal = typeof(FastClonerGenerator).GetMethod(nameof(FastClonerGenerator.CloneStructInternal), BindingFlags.NonPublic | BindingFlags.Static)!; + internal static readonly MethodInfo CloneClassInternal = typeof(FastClonerGenerator).GetMethod(nameof(FastClonerGenerator.CloneClassInternal), BindingFlags.NonPublic | BindingFlags.Static)!; + internal static readonly MethodInfo CloneClassInternalNoTracking = typeof(FastClonerGenerator).GetMethod(nameof(FastClonerGenerator.CloneClassInternalNoTracking), BindingFlags.NonPublic | BindingFlags.Static)!; + internal static readonly MethodInfo CloneClassShallowAndTrack = typeof(FastClonerGenerator).GetMethod(nameof(FastClonerGenerator.CloneClassShallowAndTrack), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly ConcurrentDictionary structCloneMethodCache = new ConcurrentDictionary(); + internal static MethodInfo MakeFieldCloneMethodInfo(Type fieldType) => fieldType.IsValueType ? MakeStructCloneMethodInfo(fieldType) : CloneClassInternal; + internal static MethodInfo MakeStructCloneMethodInfo(Type valueType) => structCloneMethodCache.GetOrAdd(valueType.TypeHandle.Value, _ => CloneStructInternal.MakeGenericMethod(valueType)); + } + + internal static class CommonMethods + { + internal static readonly MethodInfo DirectCloneObject = typeof(ShallowObjectCloner).GetMethod(nameof(ShallowObjectCloner.DirectCloneObject))!; + internal static readonly MethodInfo Dispose = typeof(IDisposable).GetMethod(nameof(IDisposable.Dispose))!; + internal static readonly MethodInfo EnumeratorMoveNext = typeof(IEnumerator).GetMethod(nameof(IEnumerator.MoveNext))!; + internal static readonly MethodInfo ObjectGetType = typeof(object).GetMethod(nameof(GetType))!; + } +} diff --git a/src/Foundatio/FastCloner/FastCloner.cs b/src/Foundatio/FastCloner/FastCloner.cs new file mode 100644 index 000000000..959163629 --- /dev/null +++ b/src/Foundatio/FastCloner/FastCloner.cs @@ -0,0 +1,155 @@ +/* This code was generated by FastCloner.Internalization.Builder, do not edit manually +FastCloner is licensed under the MIT licence. https://github.com/lofcz/FastCloner +*/ + +using Foundatio.FastCloner.Code; +using System; +using System.Collections.Generic; + +namespace Foundatio.FastCloner; +/// +/// Extensions for object cloning +/// +internal static class FastCloner +{ + internal static volatile bool DisableOptionalFeatures; + internal static void SetDisableOptionalFeatures(bool value) + { + if (DisableOptionalFeatures == value) + return; + DisableOptionalFeatures = value; + RefreshCachesAfterBehaviorStateChange(); + } + + /// + /// Cloning objects with nest level above this threshold uses iterative approach instead of recursion. + /// + public static int MaxRecursionDepth { get; set; } = 1_000; + + /// + /// Performs deep (full) copy of object and related graph + /// + public static T? DeepClone(T? obj) => FastClonerGenerator.CloneObject(obj); + /// + /// Performs deep (full) copy of object and related graph to existing object + /// + /// existing filled object + /// Method is valid only for classes, classes should be descendants in reality, not in declaration + public static TTo? DeepCloneTo(TFrom? objFrom, TTo? objTo) + where TTo : class, TFrom => (TTo? )FastClonerGenerator.CloneObjectTo(objFrom, objTo, true); + /// + /// Performs shallow copy of object to existing object + /// + /// existing filled object + /// Method is valid only for classes, classes should be descendants in reality, not in declaration + public static TTo? ShallowCloneTo(TFrom? objFrom, TTo? objTo) + where TTo : class, TFrom => (TTo? )FastClonerGenerator.CloneObjectTo(objFrom, objTo, false); + /// + /// Performs shallow (only new object returned, without cloning of dependencies) copy of object + /// + public static T? ShallowClone(T? obj) => ShallowClonerGenerator.CloneObject(obj); + /// + /// Clears all cached information about classes, structs, types, and other CLR objects. + /// + public static void ClearCache() => FastClonerCache.ClearCache(); + /// + /// Sets the cloning behavior for a type. + /// + /// - Default behavior, performs deep cloning (removes any custom behavior). + /// - Returns the same instance without cloning (for immutable/safe types). + /// - Returns null/default and skips cloning entirely. + /// + /// + /// The type to configure. + /// The cloning behavior to apply. + /// + /// Setting removes any custom behavior (equivalent to ). + /// Note that changing behavior clears the cache, which may impact performance until the cache is repopulated. + /// + public static void SetTypeBehavior(Type type, CloneBehavior behavior) + { + if (behavior == CloneBehavior.Clone) + { + // Clone is the default - remove any custom behavior + if (FastClonerCache.TypeBehaviors.TryRemove(type, out _)) + { + RefreshCachesAfterBehaviorStateChange(); + } + } + else + { + FastClonerCache.TypeBehaviors[type] = behavior; + RefreshCachesAfterBehaviorStateChange(); + } + } + + /// + /// Sets the cloning behavior for a type. + /// + /// The type to configure. + /// The cloning behavior to apply. + public static void SetTypeBehavior(CloneBehavior behavior) => SetTypeBehavior(typeof(T), behavior); + /// + /// Gets the configured cloning behavior for a type, or null if using default behavior. + /// + /// The type to query. + /// The configured behavior, or null if no custom behavior is set. + public static CloneBehavior? GetTypeBehavior(Type type) => FastClonerCache.GetTypeBehavior(type); + /// + /// Gets the configured cloning behavior for a type, or null if using default behavior. + /// + /// The type to query. + /// The configured behavior, or null if no custom behavior is set. + public static CloneBehavior? GetTypeBehavior() => GetTypeBehavior(typeof(T)); + /// + /// Returns all types with custom cloning behaviors configured. + /// + public static Dictionary GetTypeBehaviors() + { + return new Dictionary(FastClonerCache.TypeBehaviors); + } + + /// + /// Clears any custom cloning behavior for a type, returning it to default deep clone behavior. + /// + /// The type to clear. + /// True if a custom behavior was removed, false if none was set. + /// + /// Note that this clears the cache, which may have negative impact on cloning performance until the cache is repopulated. + /// + public static bool ClearTypeBehavior(Type type) + { + bool removed = FastClonerCache.TypeBehaviors.TryRemove(type, out _); + if (removed) + { + RefreshCachesAfterBehaviorStateChange(); + } + + return removed; + } + + /// + /// Clears any custom cloning behavior for a type, returning it to default deep clone behavior. + /// + /// The type to clear. + /// True if a custom behavior was removed, false if none was set. + public static bool ClearTypeBehavior() => ClearTypeBehavior(typeof(T)); + /// + /// Clears all custom type behaviors, returning all types to default deep clone behavior. + /// + /// + /// Note that this clears the cache, which may have negative impact on cloning performance until the cache is repopulated. + /// + public static void ClearAllTypeBehaviors() + { + FastClonerCache.TypeBehaviors.Clear(); + RefreshCachesAfterBehaviorStateChange(); + } + + private static void RefreshCachesAfterBehaviorStateChange() + { + FastClonerSafeTypes.ClearKnownTypesCache(); + FastClonerCache.ClearCache(); + FastClonerCache.RecalculateTypeBehaviorState(); + } +} diff --git a/src/Foundatio/DeepCloner/LICENSE b/src/Foundatio/FastCloner/LICENSE similarity index 97% rename from src/Foundatio/DeepCloner/LICENSE rename to src/Foundatio/FastCloner/LICENSE index 12cc4faf0..d13cc4b26 100644 --- a/src/Foundatio/DeepCloner/LICENSE +++ b/src/Foundatio/FastCloner/LICENSE @@ -1,7 +1,5 @@ The MIT License (MIT) -Copyright (c) 2016 force - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights diff --git a/src/Foundatio/Properties/AssemblyInfo.cs b/src/Foundatio/Properties/AssemblyInfo.cs index 59f9f680d..ad9840441 100644 --- a/src/Foundatio/Properties/AssemblyInfo.cs +++ b/src/Foundatio/Properties/AssemblyInfo.cs @@ -2,3 +2,4 @@ [assembly: InternalsVisibleTo("Foundatio.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a9357232b9bcad78fd310297fdb41bf42816ee2ca9ccdace999889de2badb6f06df2de1d9f2c8cb17b21f5311f11d6bb328d55e0dd9fe8adc5e2dc4610028c1bdacb3355d2e239b81d0bb0ac83e615fc641f8a3ec49e4fad8e305994953d448ef7b38e8c256601e54af19c035b562e3e5e5461c2a93b8dd11936e451b05034a2")] [assembly: InternalsVisibleTo("Foundatio.TestHarness, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a9357232b9bcad78fd310297fdb41bf42816ee2ca9ccdace999889de2badb6f06df2de1d9f2c8cb17b21f5311f11d6bb328d55e0dd9fe8adc5e2dc4610028c1bdacb3355d2e239b81d0bb0ac83e615fc641f8a3ec49e4fad8e305994953d448ef7b38e8c256601e54af19c035b562e3e5e5461c2a93b8dd11936e451b05034a2")] +[assembly: InternalsVisibleTo("Foundatio.Benchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a9357232b9bcad78fd310297fdb41bf42816ee2ca9ccdace999889de2badb6f06df2de1d9f2c8cb17b21f5311f11d6bb328d55e0dd9fe8adc5e2dc4610028c1bdacb3355d2e239b81d0bb0ac83e615fc641f8a3ec49e4fad8e305994953d448ef7b38e8c256601e54af19c035b562e3e5e5461c2a93b8dd11936e451b05034a2")]