From ad426feba486718c0f270d05e20223c7c98a1f57 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 12 Dec 2025 11:30:55 +0100 Subject: [PATCH] Refactor JSON (de)serialization: options, depth, utilities Major overhaul of JSON serialization/deserialization: - Introduce AcJsonSerializerOptions for reference handling and max depth - Centralize type checks, primitive/collection logic in JsonUtilities - Add depth limiting to serializer/deserializer (MaxDepth support) - Make $id/$ref reference handling optional via options - Unify and simplify public API (ToJson, JsonTo, CloneTo, CopyTo, etc.) - Improve primitive, enum, and collection handling and caching - Refactor contract resolver and merge logic to use new utilities - Remove redundant code, centralize string escaping/unescaping - Update all tests and benchmarks to use new API and options - Fix minor bugs and improve error handling and validation This modernizes and unifies the JSON infrastructure for better performance, flexibility, and maintainability. --- AyCode.Core.Tests/JsonExtensionTests.cs | 104 +- AyCode.Core/Extensions/AcJsonDeserializer.cs | 882 +++++--------- AyCode.Core/Extensions/AcJsonSerializer.cs | 826 +++---------- AyCode.Core/Extensions/JsonUtilities.cs | 453 ++++++- .../Extensions/MergeContractResolver.cs | 1070 +++++++---------- .../Extensions/SerializeObjectExtensions.cs | 326 ++--- .../SignalRs/AcSignalRClientBase.cs | 33 +- .../SignalRs/IAcSignalRHubClient.cs | 10 +- .../SignalRs/SignalRRequestModel.cs | 2 +- BenchmarkSuite1/SerializationBenchmarks.cs | 10 +- 10 files changed, 1460 insertions(+), 2256 deletions(-) diff --git a/AyCode.Core.Tests/JsonExtensionTests.cs b/AyCode.Core.Tests/JsonExtensionTests.cs index 9f10fa4..161b08c 100644 --- a/AyCode.Core.Tests/JsonExtensionTests.cs +++ b/AyCode.Core.Tests/JsonExtensionTests.cs @@ -17,8 +17,6 @@ public sealed class JsonExtensionTests TestDataFactory.ResetIdCounter(); } - private static JsonSerializerSettings GetMergeSettings() => SerializeObjectExtensions.Options; - #region Deep Hierarchy Tests (5 Levels) [TestMethod] @@ -56,7 +54,7 @@ public sealed class JsonExtensionTests }}"; // Act - updateJson.JsonTo(order, GetMergeSettings()); + updateJson.JsonTo(order); // Assert: All references preserved Assert.AreSame(originalItem, order.Items[0], "Level 2: Item reference must be preserved"); @@ -101,7 +99,7 @@ public sealed class JsonExtensionTests }}"; // Act - updateJson.JsonTo(order, GetMergeSettings()); + updateJson.JsonTo(order); // Assert Assert.AreEqual(originalItemCount, order.Items.Count, "Items count should be preserved (KEEP logic)"); @@ -120,11 +118,8 @@ public sealed class JsonExtensionTests var sharedTag = TestDataFactory.CreateTag("SharedTag"); var order = TestDataFactory.CreateOrder(itemCount: 2, sharedTag: sharedTag); - var settings = GetMergeSettings(); - settings.Formatting = Formatting.Indented; - // Act - var json = order.ToJson(settings); + var json = order.ToJson(); Console.WriteLine($"Semantic Reference JSON:\n{json}"); // Assert @@ -155,7 +150,7 @@ public sealed class JsonExtensionTests }"; // Act - updateJson.JsonTo(order, GetMergeSettings()); + updateJson.JsonTo(order); // Assert Assert.AreEqual("ORD-UPDATED", order.OrderNumber); @@ -174,11 +169,8 @@ public sealed class JsonExtensionTests var sharedMeta = TestDataFactory.CreateMetadata(withChild: true); var order = TestDataFactory.CreateOrder(itemCount: 2, sharedMetadata: sharedMeta); - var settings = GetMergeSettings(); - settings.Formatting = Formatting.Indented; - // Act - var json = order.ToJson(settings); + var json = order.ToJson(); Console.WriteLine($"Newtonsoft Reference JSON:\n{json}"); // Assert @@ -210,11 +202,8 @@ public sealed class JsonExtensionTests AuditMetadata = rootMeta }; - var settings = GetMergeSettings(); - settings.Formatting = Formatting.Indented; - // Act - var json = order.ToJson(settings); + var json = order.ToJson(); // Assert Assert.IsTrue(json.Contains("Root")); @@ -247,8 +236,7 @@ public sealed class JsonExtensionTests Items = [new TestOrderItem { Id = 10, ProductName = "A", Tag = sharedTag, ItemMetadata = sharedMeta }] }; - var settings = GetMergeSettings(); - var json = order.ToJson(settings); + var json = order.ToJson(); // Assert var refCount = json.Split("\"$ref\"").Length - 1; @@ -279,7 +267,7 @@ public sealed class JsonExtensionTests }}"; // Act - order.DeepPopulateWithMerge(updateJson, GetMergeSettings()); + order.DeepPopulateWithMerge(updateJson); // Assert Assert.AreNotSame(originalRef, order.NoMergeItems); @@ -314,7 +302,7 @@ public sealed class JsonExtensionTests }"; // Act - order.DeepPopulateWithMerge(updateJson, GetMergeSettings()); + order.DeepPopulateWithMerge(updateJson); // Assert Assert.AreEqual(2, order.MetadataList.Count); @@ -350,9 +338,9 @@ public sealed class JsonExtensionTests new { Id = appleId, Name = "Apple", Qty = 7 }, new { Id = Guid.NewGuid(), Name = "Banana", Qty = 4 } } - }.ToJson(GetMergeSettings()); + }.ToJson(); - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); // List reference preserved Assert.AreSame(originalItemsRef, order.Items, "List reference must be preserved"); @@ -377,8 +365,8 @@ public sealed class JsonExtensionTests var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 2, sharedTag: sharedTag, sharedMetadata: sharedMeta); // Act - var json = order.ToJson(GetMergeSettings()); - var deserialized = json.JsonTo(GetMergeSettings()); + var json = order.ToJson(); + var deserialized = json.JsonTo(); // Assert Assert.IsNotNull(deserialized); @@ -394,10 +382,9 @@ public sealed class JsonExtensionTests [TestMethod] public void PrimitiveArray_BooleanTrue_RoundTrips() { - var settings = GetMergeSettings(); - var jsonString = (new[] { true }).ToJson(settings); + var jsonString = (new[] { true }).ToJson(); - var result = jsonString.JsonTo(typeof(bool[]), settings) as bool[]; + var result = jsonString.JsonTo(typeof(bool[])) as bool[]; Assert.IsNotNull(result); Assert.IsTrue(result[0], "Boolean true should deserialize as true!"); @@ -406,7 +393,6 @@ public sealed class JsonExtensionTests [TestMethod] public void PrimitiveArray_AllTypes_RoundTrip() { - var settings = GetMergeSettings(); var testCases = new (Type type, object value)[] { (typeof(bool), true), @@ -424,9 +410,9 @@ public sealed class JsonExtensionTests { var wrapped = Array.CreateInstance(type, 1); wrapped.SetValue(value, 0); - var json = wrapped.ToJson(settings); + var json = wrapped.ToJson(); - var result = json.JsonTo(type.MakeArrayType(), settings) as Array; + var result = json.JsonTo(type.MakeArrayType()) as Array; Assert.IsNotNull(result, $"Failed for {type.Name}"); Assert.AreEqual(value, result.GetValue(0), $"Value mismatch for {type.Name}"); @@ -436,15 +422,14 @@ public sealed class JsonExtensionTests [TestMethod] public void IdMessage_MultipleParameters_SimulateSignalR() { - var settings = GetMergeSettings(); var @params = new (Type t, object v)[] { (typeof(bool), true), (typeof(string), "filter"), (typeof(int), 100) }; foreach (var (type, value) in @params) { var wrapped = Array.CreateInstance(type, 1); wrapped.SetValue(value, 0); - var json = wrapped.ToJson(settings); - var arr = json.JsonTo(type.MakeArrayType(), settings) as Array; + var json = wrapped.ToJson(); + var arr = json.JsonTo(type.MakeArrayType()) as Array; Assert.AreEqual(value, arr?.GetValue(0)); } } @@ -574,7 +559,7 @@ public sealed class JsonExtensionTests }; // Act - Serialize with AyCode - var json = order.ToJson(GetMergeSettings()); + var json = order.ToJson(); // Deserialize with native Newtonsoft var nativeSettings = new JsonSerializerSettings @@ -600,8 +585,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Populate_RefNode_ShouldSetPropertyToReferencedObject() { - // Arrange: Create JSON with $id and $ref - // This simulates a scenario where the same object is referenced multiple times var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -612,15 +595,13 @@ public sealed class JsonExtensionTests var order = new TestOrder { Id = 1, OrderNumber = "OLD" }; // Act - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set"); Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from $ref"); Assert.AreEqual(100, order.PrimaryTag.Id); Assert.AreEqual("SharedTag", order.PrimaryTag.Name); - - // The key assertion: SecondaryTag should be the SAME object as PrimaryTag (via $ref) Assert.AreSame(order.PrimaryTag, order.SecondaryTag, "SecondaryTag should reference the same object as PrimaryTag via $ref"); } @@ -628,7 +609,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Populate_RefNodeInCollection_ShouldSetPropertyToReferencedObject() { - // Arrange: Create JSON with shared reference in collection var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -642,17 +622,13 @@ public sealed class JsonExtensionTests var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List() }; // Act - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set"); Assert.AreEqual(2, order.Tags.Count, "Tags should have 2 items"); - - // First tag should be same as PrimaryTag via $ref Assert.AreSame(order.PrimaryTag, order.Tags[0], "Tags[0] should reference the same object as PrimaryTag via $ref"); - - // Second tag should be different Assert.AreEqual(200, order.Tags[1].Id); Assert.AreNotSame(order.PrimaryTag, order.Tags[1]); } @@ -660,7 +636,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Populate_NestedRefNode_ShouldResolveCorrectly() { - // Arrange: Create JSON with nested $ref var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -680,13 +655,11 @@ public sealed class JsonExtensionTests }; // Act - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); // Assert Assert.IsNotNull(order.Items[0].Tag, "Item's Tag should be set"); Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set from $ref"); - - // PrimaryTag should be same as Items[0].Tag via $ref Assert.AreSame(order.Items[0].Tag, order.PrimaryTag, "PrimaryTag should reference the same object as Items[0].Tag via $ref"); } @@ -694,7 +667,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Populate_ForwardRef_ShouldResolveDeferredReference() { - // Arrange: $ref appears BEFORE $id (forward reference) var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -705,9 +677,9 @@ public sealed class JsonExtensionTests var order = new TestOrder { Id = 1, OrderNumber = "OLD" }; // Act - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); - // Assert - forward reference should be resolved + // Assert Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set"); Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from forward $ref"); Assert.AreSame(order.PrimaryTag, order.SecondaryTag, @@ -717,7 +689,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Populate_MultipleRefsToSameId_AllShouldResolveToSameObject() { - // Arrange: Create JSON with multiple $refs pointing to the same $id var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -733,9 +704,9 @@ public sealed class JsonExtensionTests var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List() }; // Act - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); - // Assert - all refs should point to the same object + // Assert Assert.IsNotNull(order.PrimaryTag); Assert.AreSame(order.PrimaryTag, order.SecondaryTag); Assert.AreEqual(3, order.Tags.Count); @@ -747,7 +718,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Populate_DeeplyNestedRef_ShouldResolveAcrossLevels() { - // Arrange: Create JSON with $id at deep level (Item.Tag), $ref at root level var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -766,7 +736,7 @@ public sealed class JsonExtensionTests }; // Act - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); // Assert var deepTag = order.Items[0].Tag; @@ -779,7 +749,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Populate_RefInNestedObject_ShouldResolveFromParentContext() { - // Arrange: $id at root, $ref in nested child var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -798,7 +767,7 @@ public sealed class JsonExtensionTests }; // Act - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag); @@ -810,7 +779,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Deserialize_RefOnly_ShouldCreateDeferredAndResolve() { - // Arrange: JSON where only $ref exists (forward reference scenario in deserialize) var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -831,7 +799,6 @@ public sealed class JsonExtensionTests [TestMethod] public void Deserialize_MultipleIdRefs_ComplexGraph() { - // Arrange: Complex object graph with multiple $id/$ref pairs var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", @@ -855,23 +822,16 @@ public sealed class JsonExtensionTests // Assert Assert.IsNotNull(order); Assert.AreEqual(3, order.Tags.Count); - - // Verify tag1 references Assert.AreSame(order.PrimaryTag, order.Tags[0]); Assert.AreSame(order.PrimaryTag, order.Tags[2]); - - // Verify tag2 references Assert.AreSame(order.SecondaryTag, order.Tags[1]); Assert.AreSame(order.SecondaryTag, order.Items[0].Tag); - - // Verify they are different Assert.AreNotSame(order.PrimaryTag, order.SecondaryTag); } [TestMethod] public void Populate_RefWithExistingTarget_ShouldOverwriteWithReference() { - // Arrange: Target has existing value, should be overwritten by $ref var json = @"{ ""Id"": 1, ""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""NewTag"" }, @@ -882,13 +842,13 @@ public sealed class JsonExtensionTests var order = new TestOrder { Id = 1, - SecondaryTag = existingTag // Pre-existing value + SecondaryTag = existingTag }; // Act - json.JsonTo(order, GetMergeSettings()); + json.JsonTo(order); - // Assert - SecondaryTag should be overwritten with the $ref reference + // Assert Assert.IsNotNull(order.PrimaryTag); Assert.AreSame(order.PrimaryTag, order.SecondaryTag, "SecondaryTag should be overwritten with $ref reference"); diff --git a/AyCode.Core/Extensions/AcJsonDeserializer.cs b/AyCode.Core/Extensions/AcJsonDeserializer.cs index 3e70a29..a502b45 100644 --- a/AyCode.Core/Extensions/AcJsonDeserializer.cs +++ b/AyCode.Core/Extensions/AcJsonDeserializer.cs @@ -8,6 +8,7 @@ using System.Text.Json; using AyCode.Core.Helpers; using AyCode.Core.Interfaces; using Newtonsoft.Json; +using static AyCode.Core.Extensions.JsonUtilities; namespace AyCode.Core.Extensions; @@ -29,75 +30,23 @@ public class AcJsonDeserializationException : Exception /// /// High-performance custom JSON deserializer optimized for IId<T> reference handling. -/// Throws AcJsonDeserializationException on any parsing or type conversion failure. +/// Supports MaxDepth and UseReferenceHandling options. /// public static class AcJsonDeserializer { private static readonly ConcurrentDictionary TypeMetadataCache = new(); - private static readonly ConcurrentDictionary> ListFactoryCache = new(); - - // Pre-computed type handles for fast comparison - private static readonly Type IntType = typeof(int); - private static readonly Type LongType = typeof(long); - private static readonly Type DoubleType = typeof(double); - private static readonly Type DecimalType = typeof(decimal); - private static readonly Type FloatType = typeof(float); - private static readonly Type StringType = typeof(string); - private static readonly Type DateTimeType = typeof(DateTime); - private static readonly Type GuidType = typeof(Guid); - private static readonly Type BoolType = typeof(bool); + + #region Public API /// - /// Validate JSON string for common issues before deserialization. - /// Throws AcJsonDeserializationException if invalid. + /// Deserialize JSON string to a new object of type T with default options. /// - private static void ValidateJson(string json, Type targetType) - { - if (string.IsNullOrEmpty(json)) - return; - - // Detect double-serialized JSON (JSON string containing escaped JSON) - // Pattern: starts with " and contains escaped quotes \" - if (json.Length > 2 && json[0] == '"' && json[^1] == '"') - { - // Check if this looks like a double-serialized object or array - var inner = json[1..^1]; - if (inner.Contains("\\\"") && (inner.Contains("{") || inner.Contains("["))) - { - throw new AcJsonDeserializationException( - $"Detected double-serialized JSON string. The JSON appears to have been serialized twice. " + - $"Target type: {targetType.Name}. " + - $"JSON starts with: {json[..Math.Min(100, json.Length)]}", - json, targetType); - } - } - - // Detect type mismatch: expecting object but got array or vice versa - var isArrayJson = json.Length > 0 && json[0] == '['; - var isObjectJson = json.Length > 0 && json[0] == '{'; - var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType); - var isDictType = IsDictionaryType(targetType, out _, out _); - - if (isArrayJson && !isCollectionType && !isDictType && targetType != typeof(object)) - { - throw new AcJsonDeserializationException( - $"JSON is an array but target type '{targetType.Name}' is not a collection type.", - json, targetType); - } - - if (isObjectJson && isCollectionType && !isDictType) - { - throw new AcJsonDeserializationException( - $"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", - json, targetType); - } - } + public static T? Deserialize(string json) => Deserialize(json, AcJsonSerializerOptions.Default); /// - /// Deserialize JSON string to a new object of type T. - /// Throws AcJsonDeserializationException on any failure. + /// Deserialize JSON string to a new object of type T with specified options. /// - public static T? Deserialize(string json) + public static T? Deserialize(string json, AcJsonSerializerOptions options) { if (string.IsNullOrEmpty(json) || json == "null") return default; @@ -110,37 +59,34 @@ public static class AcJsonDeserializer if (TryDeserializePrimitive(json, targetType, out var primitiveResult)) return (T?)primitiveResult; - var context = new DeserializationContext(); + var context = new DeserializationContext(options); using var doc = JsonDocument.Parse(json); - var result = ReadValue(doc.RootElement, targetType, context); + var result = ReadValue(doc.RootElement, targetType, context, 0); context.ResolveReferences(); return (T?)result; } - catch (AcJsonDeserializationException) - { - throw; // Re-throw our custom exceptions - } + catch (AcJsonDeserializationException) { throw; } catch (System.Text.Json.JsonException ex) { - throw new AcJsonDeserializationException( - $"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", - json, targetType, ex); + throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", json, targetType, ex); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { - throw new AcJsonDeserializationException( - $"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", - json, targetType, ex); + throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", json, targetType, ex); } } /// - /// Deserialize JSON string to specified type. - /// Throws AcJsonDeserializationException on any failure. + /// Deserialize JSON string to specified type with default options. /// - public static object? Deserialize(string json, Type targetType) + public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default); + + /// + /// Deserialize JSON string to specified type with specified options. + /// + public static object? Deserialize(string json, Type targetType, AcJsonSerializerOptions options) { if (string.IsNullOrEmpty(json) || json == "null") return null; @@ -148,83 +94,83 @@ public static class AcJsonDeserializer { ValidateJson(json, targetType); - // Fast path: check if this is an array/collection type - skip primitive check var isArrayJson = json.Length > 0 && json[0] == '['; var isObjectJson = json.Length > 0 && json[0] == '{'; var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType); var isDictType = IsDictionaryType(targetType, out _, out _); - // Skip primitive check for arrays, collections, and dictionaries if (!isArrayJson && !isCollectionType && !(isObjectJson && isDictType)) { if (TryDeserializePrimitive(json, targetType, out var primitiveResult)) return primitiveResult; } - var context = new DeserializationContext(); + var context = new DeserializationContext(options); using var doc = JsonDocument.Parse(json); - var result = ReadValue(doc.RootElement, targetType, context); + var result = ReadValue(doc.RootElement, targetType, context, 0); context.ResolveReferences(); return result; } - catch (AcJsonDeserializationException) - { - throw; - } + catch (AcJsonDeserializationException) { throw; } catch (System.Text.Json.JsonException ex) { - throw new AcJsonDeserializationException( - $"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", - json, targetType, ex); + throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", json, targetType, ex); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { - throw new AcJsonDeserializationException( - $"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", - json, targetType, ex); + throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", json, targetType, ex); } } /// - /// Populate existing object with JSON data (merge mode). - /// Throws AcJsonDeserializationException on any failure. + /// Populate existing object with JSON data (merge mode) with default options. /// - public static void Populate(string json, T target) where T : class - { - if (target == null) - throw new ArgumentNullException(nameof(target), "Cannot populate null target object"); - - if (string.IsNullOrEmpty(json) || json == "null") return; - Populate(json, target, typeof(T)); - } + public static void Populate(string json, T target) where T : class + => Populate(json, target, AcJsonSerializerOptions.Default); - public static void Populate(string json, object target) + /// + /// Populate existing object with JSON data (merge mode) with specified options. + /// + public static void Populate(string json, T target, AcJsonSerializerOptions options) where T : class { - if (target == null) - throw new ArgumentNullException(nameof(target), "Cannot populate null target object"); - + ArgumentNullException.ThrowIfNull(target); if (string.IsNullOrEmpty(json) || json == "null") return; - Populate(json, target, target.GetType()); + Populate(json, (object)target, target.GetType(), options); } /// - /// Populate existing object with JSON data using runtime type (merge mode). - /// Throws AcJsonDeserializationException on any failure. + /// Populate existing object with JSON data with default options. /// - public static void Populate(string json, object target, Type targetType) + public static void Populate(string json, object target) + => Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default); + + /// + /// Populate existing object with JSON data with specified options. + /// + public static void Populate(string json, object target, AcJsonSerializerOptions options) + => Populate(json, target, target.GetType(), options); + + /// + /// Populate existing object with JSON data with default options. + /// + public static void Populate(string json, object target, Type targetType) + => Populate(json, target, targetType, AcJsonSerializerOptions.Default); + + /// + /// Populate existing object with JSON data with specified options. + /// + public static void Populate(string json, object target, Type targetType, AcJsonSerializerOptions options) { - if (target == null) - throw new ArgumentNullException(nameof(target), "Cannot populate null target object"); - + ArgumentNullException.ThrowIfNull(target); if (string.IsNullOrEmpty(json) || json == "null") return; try { ValidateJson(json, targetType); - var context = new DeserializationContext { IsMergeMode = true }; + var context = new DeserializationContext(options) { IsMergeMode = true }; using var doc = JsonDocument.Parse(json); var rootElement = doc.RootElement; @@ -232,15 +178,10 @@ public static class AcJsonDeserializer if (rootElement.ValueKind == JsonValueKind.Array) { if (target is IList targetList) - { - PopulateList(rootElement, targetList, targetType, context); - } + PopulateList(rootElement, targetList, targetType, context, 0); else - { - throw new AcJsonDeserializationException( - $"Cannot populate non-list target '{targetType.Name}' with JSON array", - json, targetType); - } + throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType); + context.ResolveReferences(); return; } @@ -248,208 +189,208 @@ public static class AcJsonDeserializer if (rootElement.ValueKind == JsonValueKind.Object) { var metadata = GetTypeMetadata(targetType); - PopulateObjectInternalMerge(rootElement, target, metadata, context); + PopulateObjectInternalMerge(rootElement, target, metadata, context, 0); } else - { - throw new AcJsonDeserializationException( - $"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", - json, targetType); - } + throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType); context.ResolveReferences(); } - catch (AcJsonDeserializationException) - { - throw; - } + catch (AcJsonDeserializationException) { throw; } catch (System.Text.Json.JsonException ex) { - throw new AcJsonDeserializationException( - $"Failed to parse JSON for population of type '{targetType.Name}': {ex.Message}", - json, targetType, ex); + throw new AcJsonDeserializationException($"Failed to parse JSON for population of type '{targetType.Name}': {ex.Message}", json, targetType, ex); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { - throw new AcJsonDeserializationException( - $"Failed to convert JSON value during population of type '{targetType.Name}': {ex.Message}", - json, targetType, ex); + throw new AcJsonDeserializationException($"Failed to convert JSON value during population of type '{targetType.Name}': {ex.Message}", json, targetType, ex); } } + #endregion + + #region Validation + + private static void ValidateJson(string json, Type targetType) + { + if (string.IsNullOrEmpty(json)) return; + + if (json.Length > 2 && json[0] == '"' && json[^1] == '"') + { + var inner = json[1..^1]; + if (inner.Contains("\\\"") && (inner.Contains("{") || inner.Contains("["))) + throw new AcJsonDeserializationException( + $"Detected double-serialized JSON string. Target type: {targetType.Name}.", + json, targetType); + } + + var isArrayJson = json.Length > 0 && json[0] == '['; + var isObjectJson = json.Length > 0 && json[0] == '{'; + var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType); + var isDictType = IsDictionaryType(targetType, out _, out _); + + if (isArrayJson && !isCollectionType && !isDictType && targetType != typeof(object)) + throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType); + + if (isObjectJson && isCollectionType && !isDictType) + throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType); + } + + #endregion + #region Core Reading Methods [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadValue(JsonElement element, Type targetType, DeserializationContext context) + private static object? ReadValue(JsonElement element, Type targetType, DeserializationContext context, int depth) { - var valueKind = element.ValueKind; - - return valueKind switch + return element.ValueKind switch { - JsonValueKind.Object => ReadObject(element, targetType, context), - JsonValueKind.Array => ReadArray(element, targetType, context), + JsonValueKind.Object => ReadObject(element, targetType, context, depth), + JsonValueKind.Array => ReadArray(element, targetType, context, depth), JsonValueKind.Null => null, - _ => ReadPrimitive(element, targetType, valueKind) + _ => ReadPrimitive(element, targetType, element.ValueKind) }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadObject(JsonElement element, Type targetType, DeserializationContext context) + private static object? ReadObject(JsonElement element, Type targetType, DeserializationContext context, int depth) { - if (element.TryGetProperty("$ref", out var refElement)) + // Handle $ref even if reference handling is disabled (for compatibility) + if (context.UseReferenceHandling && element.TryGetProperty("$ref", out var refElement)) { var refId = refElement.GetString()!; return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType); } - // Handle Dictionary types + // Check depth limit + if (depth > context.MaxDepth) return null; + if (IsDictionaryType(targetType, out var keyType, out var valueType)) - { - return ReadDictionary(element, targetType, keyType!, valueType!, context); - } + return ReadDictionary(element, keyType!, valueType!, context, depth); var metadata = GetTypeMetadata(targetType); object? instance; if (metadata.CompiledConstructor != null) - { instance = metadata.CompiledConstructor.Invoke(); - } else { - try - { - instance = Activator.CreateInstance(targetType); - } + try { instance = Activator.CreateInstance(targetType); } catch (MissingMethodException ex) { throw new AcJsonDeserializationException( - $"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor. " + - $"Add a parameterless constructor or use a different serialization approach.", + $"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.", null, targetType, ex); } } if (instance == null) return null; - if (element.TryGetProperty("$id", out var idElement)) - { + if (context.UseReferenceHandling && element.TryGetProperty("$id", out var idElement)) context.RegisterObject(idElement.GetString()!, instance); - } - PopulateObjectInternal(element, instance, metadata, context); + PopulateObjectInternal(element, instance, metadata, context, depth); return instance; } - private static void PopulateObjectInternal(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context) + private static void PopulateObjectInternal(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth) { - var propertySetters = metadata.PropertySetters; - foreach (var jsonProp in element.EnumerateObject()) { var propName = jsonProp.Name; - if (propName.Length > 0 && propName[0] == '$') - { - if (propName == "$id" || propName == "$ref") continue; - } + if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref")) + continue; - if (!propertySetters.TryGetValue(propName, out var propInfo)) continue; + if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue; - var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context); + var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, depth + 1); if (value is DeferredReference deferred) - { context.AddPropertyToResolve(target, propInfo, deferred.RefId); - } else - { propInfo.SetValue(target, value); - } } } - private static void PopulateObjectInternalMerge(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context) + private static void PopulateObjectInternalMerge(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth) { - var propertySetters = metadata.PropertySetters; - foreach (var jsonProp in element.EnumerateObject()) { var propName = jsonProp.Name; - if (propName.Length > 0 && propName[0] == '$') - { - if (propName == "$id" || propName == "$ref") continue; - } + if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref")) + continue; - if (!propertySetters.TryGetValue(propName, out var propInfo)) continue; + if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue; var propValue = jsonProp.Value; var propValueKind = propValue.ValueKind; - // Handle IId collection merge - update existing items in place + // Check depth limit for nested objects/collections + if (depth + 1 > context.MaxDepth) + { + // At max depth, only set primitives + if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array) + { + var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind); + propInfo.SetValue(target, primitiveValue); + } + continue; + } + if (propInfo is { IsCollection: true, ElementIsIId: true } && propValueKind == JsonValueKind.Array) { var existingCollection = propInfo.GetValue(target); if (existingCollection != null) { - MergeIIdCollection(propValue, existingCollection, propInfo, context); + MergeIIdCollection(propValue, existingCollection, propInfo, context, depth); continue; } } - // For nested objects, check if it's a $ref first - if so, don't try to update in place if (propValueKind == JsonValueKind.Object) { - // Check if this is a $ref - if so, we need to replace, not merge - if (propValue.TryGetProperty("$ref", out _)) + if (context.UseReferenceHandling && propValue.TryGetProperty("$ref", out _)) { - // This is a reference - resolve it and set - var value = ReadValue(propValue, propInfo.PropertyType, context); + var value = ReadValue(propValue, propInfo.PropertyType, context, depth + 1); if (value is DeferredReference deferred) - { context.AddPropertyToResolve(target, propInfo, deferred.RefId); - } else - { propInfo.SetValue(target, value); - } continue; } - // Not a $ref - try to update existing object in place - if (!propInfo.PropertyType.IsPrimitive && propInfo.PropertyType != typeof(string)) + if (!propInfo.PropertyType.IsPrimitive && propInfo.PropertyType != StringType) { var existingObj = propInfo.GetValue(target); if (existingObj != null) { var nestedMetadata = GetTypeMetadata(propInfo.PropertyType); - PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context); + PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, depth + 1); continue; } } } - var value2 = ReadValue(propValue, propInfo.PropertyType, context); + var value2 = ReadValue(propValue, propInfo.PropertyType, context, depth + 1); if (value2 is DeferredReference deferred2) - { context.AddPropertyToResolve(target, propInfo, deferred2.RefId); - } else - { propInfo.SetValue(target, value2); - } } } - private static void PopulateList(JsonElement arrayElement, IList targetList, Type listType, DeserializationContext context) + private static void PopulateList(JsonElement arrayElement, IList targetList, Type listType, DeserializationContext context, int depth) { + // Check depth limit + if (depth > context.MaxDepth) return; + var elementType = GetCollectionElementType(listType); if (elementType == null) return; - // Use batch update for IAcObservableCollection to suppress per-item notifications var acObservable = targetList as IAcObservableCollection; acObservable?.BeginUpdate(); @@ -459,77 +400,58 @@ public static class AcJsonDeserializer foreach (var item in arrayElement.EnumerateArray()) { - var value = ReadValue(item, elementType, context); + var value = ReadValue(item, elementType, context, depth + 1); if (value != null) - { targetList.Add(value); - } } } - finally - { - acObservable?.EndUpdate(); - } + finally { acObservable?.EndUpdate(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadArray(JsonElement element, Type targetType, DeserializationContext context) + private static object? ReadArray(JsonElement element, Type targetType, DeserializationContext context, int depth) { + // Check depth limit + if (depth > context.MaxDepth) return null; + var elementType = GetCollectionElementType(targetType); if (elementType == null) return null; - // For arrays, we need to use a temporary list if (targetType.IsArray) { var list = GetOrCreateListFactory(elementType)(); foreach (var item in element.EnumerateArray()) - { - list.Add(ReadValue(item, elementType, context)); - } + list.Add(ReadValue(item, elementType, context, depth + 1)); + var array = Array.CreateInstance(elementType, list.Count); list.CopyTo(array, 0); return array; } - // Try to create an instance of the target collection type directly - // This handles ObservableCollection, AcObservableCollection, etc. IList? targetList = null; try { var instance = Activator.CreateInstance(targetType); - if (instance is IList list) - { - targetList = list; - } - } - catch - { - // Fallback to List if we can't create the target type + if (instance is IList list) targetList = list; } + catch { /* Fallback to List */ } - // Fallback to List if target type couldn't be instantiated targetList ??= GetOrCreateListFactory(elementType)(); - // Use batch update for IAcObservableCollection to suppress per-item notifications var acObservable = targetList as IAcObservableCollection; acObservable?.BeginUpdate(); try { foreach (var item in element.EnumerateArray()) - { - targetList.Add(ReadValue(item, elementType, context)); - } - } - finally - { - acObservable?.EndUpdate(); + targetList.Add(ReadValue(item, elementType, context, depth + 1)); } + finally { acObservable?.EndUpdate(); } return targetList; } - private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context) + private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth) { var elementType = propInfo.ElementType!; var idGetter = propInfo.ElementIdGetter!; @@ -538,7 +460,6 @@ public static class AcJsonDeserializer var existingList = (IList)existingCollection; var count = existingList.Count; - // Use batch update for IAcObservableCollection to suppress per-item notifications var acObservable = existingList as IAcObservableCollection; acObservable?.BeginUpdate(); @@ -554,10 +475,8 @@ public static class AcJsonDeserializer if (item != null) { var id = idGetter(item); - if (id != null && !IsDefaultId(id, idType)) - { + if (id != null && !IsDefaultValue(id, idType)) existingById[id] = item; - } } } } @@ -568,29 +487,23 @@ public static class AcJsonDeserializer object? itemId = null; if (jsonItem.TryGetProperty("Id", out var idProp)) - { itemId = ReadPrimitive(idProp, idType, idProp.ValueKind); - } - if (itemId != null && !IsDefaultId(itemId, idType) && existingById != null) + if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null) { if (existingById.TryGetValue(itemId, out var existingItem)) { - // Recursively merge nested objects var itemMetadata = GetTypeMetadata(elementType); - PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context); + PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, depth + 1); continue; } } - var newItem = ReadValue(jsonItem, elementType, context); + var newItem = ReadValue(jsonItem, elementType, context, depth + 1); if (newItem != null) existingList.Add(newItem); } } - finally - { - acObservable?.EndUpdate(); - } + finally { acObservable?.EndUpdate(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -605,12 +518,12 @@ public static class AcJsonDeserializer if (ReferenceEquals(type, DoubleType)) return element.GetDouble(); if (ReferenceEquals(type, DecimalType)) return element.GetDecimal(); if (ReferenceEquals(type, FloatType)) return element.GetSingle(); - if (type == typeof(byte)) return element.GetByte(); - if (type == typeof(short)) return element.GetInt16(); - if (type == typeof(ushort)) return element.GetUInt16(); - if (type == typeof(uint)) return element.GetUInt32(); - if (type == typeof(ulong)) return element.GetUInt64(); - if (type == typeof(sbyte)) return element.GetSByte(); + if (type == ByteType) return element.GetByte(); + if (type == ShortType) return element.GetInt16(); + if (type == UShortType) return element.GetUInt16(); + if (type == UIntType) return element.GetUInt32(); + if (type == ULongType) return element.GetUInt64(); + if (type == SByteType) return element.GetSByte(); if (type.IsEnum) return Enum.ToObject(type, element.GetInt32()); return null; } @@ -620,8 +533,8 @@ public static class AcJsonDeserializer if (ReferenceEquals(type, StringType)) return element.GetString(); if (ReferenceEquals(type, DateTimeType)) return element.GetDateTime(); if (ReferenceEquals(type, GuidType)) return element.GetGuid(); - if (type == typeof(DateTimeOffset)) return element.GetDateTimeOffset(); - if (type == typeof(TimeSpan)) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture); + if (type == DateTimeOffsetType) return element.GetDateTimeOffset(); + if (type == TimeSpanType) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture); if (type.IsEnum) return Enum.Parse(type, element.GetString()!); return null; } @@ -632,150 +545,24 @@ public static class AcJsonDeserializer return null; } - #endregion - - #region Type Helpers - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Type? GetListElementType(Type listType) + private static object ReadDictionary(JsonElement element, Type keyType, Type valueType, DeserializationContext context, int depth) { - if (listType.IsGenericType) - { - var genericDef = listType.GetGenericTypeDefinition(); - if (genericDef == typeof(List<>) || genericDef == typeof(IList<>) || - genericDef == typeof(ICollection<>) || genericDef == typeof(IEnumerable<>)) - { - return listType.GetGenericArguments()[0]; - } - } - - foreach (var iface in listType.GetInterfaces()) - { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>)) - { - return iface.GetGenericArguments()[0]; - } - } - - if (listType.IsArray) - return listType.GetElementType(); - - return null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Type? GetCollectionElementType(Type collectionType) - { - if (collectionType.IsArray) - return collectionType.GetElementType(); - - if (collectionType.IsGenericType) - { - var genericDef = collectionType.GetGenericTypeDefinition(); - if (genericDef == typeof(List<>) || genericDef == typeof(IList<>) || - genericDef == typeof(ICollection<>) || genericDef == typeof(IEnumerable<>)) - { - return collectionType.GetGenericArguments()[0]; - } - } - - foreach (var iface in collectionType.GetInterfaces()) - { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>)) - { - return iface.GetGenericArguments()[0]; - } - } - - return typeof(object); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsDefaultId(object id, Type idType) - { - if (ReferenceEquals(idType, IntType)) return (int)id == 0; - if (ReferenceEquals(idType, LongType)) return (long)id == 0; - if (ReferenceEquals(idType, GuidType)) return (Guid)id == Guid.Empty; - return false; - } - - /// - /// Check if type is a Dictionary type and extract key/value types. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType) - { - keyType = null; - valueType = null; - - if (!type.IsGenericType) return false; - - var genericDef = type.GetGenericTypeDefinition(); - if (genericDef == typeof(Dictionary<,>) || genericDef == typeof(IDictionary<,>)) - { - var args = type.GetGenericArguments(); - keyType = args[0]; - valueType = args[1]; - return true; - } - - // Check interfaces for IDictionary<,> - foreach (var iface in type.GetInterfaces()) - { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IDictionary<,>)) - { - var args = iface.GetGenericArguments(); - keyType = args[0]; - valueType = args[1]; - return true; - } - } - - return false; - } - - /// - /// Read a JSON object as a Dictionary. - /// - private static object ReadDictionary(JsonElement element, Type dictionaryType, Type keyType, Type valueType, DeserializationContext context) - { - var dictType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); + var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType); var dict = (IDictionary)Activator.CreateInstance(dictType)!; - foreach (var prop in element.EnumerateObject().Where(prop => !prop.Name.StartsWith('$'))) + foreach (var prop in element.EnumerateObject()) { - // Parse key - for string keys, use directly; for other types, parse + if (prop.Name.StartsWith('$')) continue; + object key; - if (keyType == StringType) - { - key = prop.Name; - } - else if (keyType == IntType) - { - key = int.Parse(prop.Name, CultureInfo.InvariantCulture); - } - else if (keyType == LongType) - { - key = long.Parse(prop.Name, CultureInfo.InvariantCulture); - } - else if (keyType == GuidType) - { - key = Guid.Parse(prop.Name); - } - else if (keyType.IsEnum) - { - key = Enum.Parse(keyType, prop.Name); - } - else - { - // Fallback: try to convert string to key type - key = Convert.ChangeType(prop.Name, keyType, CultureInfo.InvariantCulture); - } + if (keyType == StringType) key = prop.Name; + else if (keyType == IntType) key = int.Parse(prop.Name, CultureInfo.InvariantCulture); + else if (keyType == LongType) key = long.Parse(prop.Name, CultureInfo.InvariantCulture); + else if (keyType == GuidType) key = Guid.Parse(prop.Name); + else if (keyType.IsEnum) key = Enum.Parse(keyType, prop.Name); + else key = Convert.ChangeType(prop.Name, keyType, CultureInfo.InvariantCulture); - // Parse value - var value = ReadValue(prop.Value, valueType, context); - - dict.Add(key, value); + dict.Add(key, ReadValue(prop.Value, valueType, context, depth + 1)); } return dict; @@ -783,25 +570,80 @@ public static class AcJsonDeserializer #endregion - #region Type Metadata Cache + #region Primitive Deserialization + + private static bool TryDeserializePrimitive(string json, Type targetType, out object? result) + { + var type = Nullable.GetUnderlyingType(targetType) ?? targetType; + + if (ReferenceEquals(type, StringType)) + { + using var doc = JsonDocument.Parse(json); + result = doc.RootElement.GetString(); + return true; + } + + if (ReferenceEquals(type, IntType)) { result = int.Parse(json, CultureInfo.InvariantCulture); return true; } + if (ReferenceEquals(type, LongType)) { result = long.Parse(json, CultureInfo.InvariantCulture); return true; } + if (ReferenceEquals(type, BoolType)) { result = json == "true"; return true; } + if (ReferenceEquals(type, DoubleType)) { result = double.Parse(json, CultureInfo.InvariantCulture); return true; } + if (ReferenceEquals(type, DecimalType)) { result = decimal.Parse(json, CultureInfo.InvariantCulture); return true; } + if (ReferenceEquals(type, FloatType)) { result = float.Parse(json, CultureInfo.InvariantCulture); return true; } + + if (ReferenceEquals(type, DateTimeType)) + { + using var doc = JsonDocument.Parse(json); + result = doc.RootElement.GetDateTime(); + return true; + } + + if (ReferenceEquals(type, GuidType)) + { + using var doc = JsonDocument.Parse(json); + result = doc.RootElement.GetGuid(); + return true; + } + + if (type == DateTimeOffsetType) { using var doc = JsonDocument.Parse(json); result = doc.RootElement.GetDateTimeOffset(); return true; } + if (type == TimeSpanType) { using var doc = JsonDocument.Parse(json); result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture); return true; } + + if (type.IsEnum) + { + if (json.StartsWith('"')) + { + using var doc = JsonDocument.Parse(json); + result = Enum.Parse(type, doc.RootElement.GetString()!); + } + else result = Enum.ToObject(type, int.Parse(json, CultureInfo.InvariantCulture)); + return true; + } + + if (type == ByteType) { result = byte.Parse(json, CultureInfo.InvariantCulture); return true; } + if (type == ShortType) { result = short.Parse(json, CultureInfo.InvariantCulture); return true; } + if (type == UShortType) { result = ushort.Parse(json, CultureInfo.InvariantCulture); return true; } + if (type == UIntType) { result = uint.Parse(json, CultureInfo.InvariantCulture); return true; } + if (type == ULongType) { result = ulong.Parse(json, CultureInfo.InvariantCulture); return true; } + if (type == SByteType) { result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true; } + + if (type == CharType) + { + using var doc = JsonDocument.Parse(json); + var s = doc.RootElement.GetString(); + result = s?.Length > 0 ? s[0] : '\0'; + return true; + } + + result = null; + return false; + } + + #endregion + + #region Type Metadata [MethodImpl(MethodImplOptions.AggressiveInlining)] private static DeserializeTypeMetadata GetTypeMetadata(Type type) - { - return TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Func GetOrCreateListFactory(Type elementType) - { - return ListFactoryCache.GetOrAdd(elementType, static t => - { - var listType = typeof(List<>).MakeGenericType(t); - var newExpr = Expression.New(listType); - var castExpr = Expression.Convert(newExpr, typeof(IList)); - return Expression.Lambda>(castExpr).Compile(); - }); - } + => TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t)); private sealed class DeserializeTypeMetadata { @@ -818,29 +660,19 @@ public static class AcJsonDeserializer CompiledConstructor = Expression.Lambda>(boxed).Compile(); } - // Optimized property filtering - avoid attribute checks when possible var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); var propsList = new List(allProps.Length); foreach (var p in allProps) { - if (!p.CanWrite || !p.CanRead || p.GetIndexParameters().Length != 0) - continue; - - // Check attributes only if IsDefined returns true (faster than GetCustomAttribute) - if (Attribute.IsDefined(p, typeof(JsonIgnoreAttribute))) - continue; - if (Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute))) - continue; - + if (!p.CanWrite || !p.CanRead || p.GetIndexParameters().Length != 0) continue; + if (HasJsonIgnoreAttribute(p)) continue; propsList.Add(p); } PropertySetters = new Dictionary(propsList.Count, StringComparer.OrdinalIgnoreCase); foreach (var prop in propsList) - { PropertySetters[prop.Name] = new PropertySetterInfo(prop, type); - } } } @@ -865,22 +697,18 @@ public static class AcJsonDeserializer ElementType = GetCollectionElementType(PropertyType); IsCollection = ElementType != null && ElementType != typeof(object) && typeof(IEnumerable).IsAssignableFrom(PropertyType) && - PropertyType != typeof(string); + PropertyType != StringType; if (IsCollection && ElementType != null) { - var iidInterface = ElementType.GetInterfaces() - .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IId<>)); - - if (iidInterface != null) + var (isId, idType) = GetIdInfo(ElementType); + if (isId) { ElementIsIId = true; - ElementIdType = iidInterface.GetGenericArguments()[0]; + ElementIdType = idType; var idProp = ElementType.GetProperty("Id"); if (idProp != null) - { ElementIdGetter = CreateCompiledGetter(ElementType, idProp); - } } } } @@ -935,10 +763,21 @@ public static class AcJsonDeserializer private List? _propertiesToResolve; public bool IsMergeMode { get; init; } + public bool UseReferenceHandling { get; } + public byte MaxDepth { get; } + + public DeserializationContext() : this(AcJsonSerializerOptions.Default) { } + + public DeserializationContext(AcJsonSerializerOptions options) + { + UseReferenceHandling = options.UseReferenceHandling; + MaxDepth = options.MaxDepth; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RegisterObject(string id, object obj) { + if (!UseReferenceHandling) return; _idToObject ??= new Dictionary(8, StringComparer.Ordinal); _idToObject[id] = obj; } @@ -946,8 +785,7 @@ public static class AcJsonDeserializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetReferencedObject(string id, out object? obj) { - if (_idToObject != null) - return _idToObject.TryGetValue(id, out obj); + if (_idToObject != null) return _idToObject.TryGetValue(id, out obj); obj = null; return false; } @@ -966,192 +804,10 @@ public static class AcJsonDeserializer foreach (ref readonly var ptr in System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_propertiesToResolve)) { if (_idToObject.TryGetValue(ptr.RefId, out var refObj)) - { ptr.Property.SetValue(ptr.Target, refObj); - } } } } #endregion - - #region Primitive Deserialization - - private static bool TryDeserializePrimitive(string json, Type targetType, out object? result) - { - var type = Nullable.GetUnderlyingType(targetType) ?? targetType; - - if (ReferenceEquals(type, StringType)) - { - using var doc = JsonDocument.Parse(json); - result = doc.RootElement.GetString(); - return true; - } - - if (ReferenceEquals(type, IntType)) - { - result = int.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (ReferenceEquals(type, LongType)) - { - result = long.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (ReferenceEquals(type, BoolType)) - { - result = json == "true"; - return true; - } - - if (ReferenceEquals(type, DoubleType)) - { - result = double.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (ReferenceEquals(type, DecimalType)) - { - result = decimal.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (ReferenceEquals(type, FloatType)) - { - result = float.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (ReferenceEquals(type, DateTimeType)) - { - using var doc = JsonDocument.Parse(json); - result = doc.RootElement.GetDateTime(); - return true; - } - - if (ReferenceEquals(type, GuidType)) - { - using var doc = JsonDocument.Parse(json); - result = doc.RootElement.GetGuid(); - return true; - } - - if (type == typeof(DateTimeOffset)) - { - using var doc = JsonDocument.Parse(json); - result = doc.RootElement.GetDateTimeOffset(); - return true; - } - - if (type == typeof(TimeSpan)) - { - using var doc = JsonDocument.Parse(json); - result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture); - return true; - } - - if (type.IsEnum) - { - if (json.StartsWith('"')) - { - using var doc = JsonDocument.Parse(json); - result = Enum.Parse(type, doc.RootElement.GetString()!); - } - else - { - result = Enum.ToObject(type, int.Parse(json, CultureInfo.InvariantCulture)); - } - return true; - } - - if (type == typeof(byte)) - { - result = byte.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (type == typeof(short)) - { - result = short.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (type == typeof(ushort)) - { - result = ushort.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (type == typeof(uint)) - { - result = uint.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (type == typeof(ulong)) - { - result = ulong.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (type == typeof(sbyte)) - { - result = sbyte.Parse(json, CultureInfo.InvariantCulture); - return true; - } - - if (type == typeof(char)) - { - using var doc = JsonDocument.Parse(json); - var s = doc.RootElement.GetString(); - result = s?.Length > 0 ? s[0] : '\0'; - return true; - } - - result = null; - return false; - } - - /// - /// Check if type is a generic collection type (List, IList, ObservableCollection, etc.) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsGenericCollectionType(Type type) - { - if (!type.IsGenericType) - { - // Check if it implements IList interface (covers ObservableCollection subclasses) - foreach (var iface in type.GetInterfaces()) - { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>)) - return true; - } - return false; - } - - var genericDef = type.GetGenericTypeDefinition(); - - // Check common collection types - if (genericDef == typeof(List<>) || - genericDef == typeof(IList<>) || - genericDef == typeof(ICollection<>) || - genericDef == typeof(IEnumerable<>) || - genericDef == typeof(System.Collections.ObjectModel.ObservableCollection<>) || - genericDef == typeof(System.Collections.ObjectModel.Collection<>)) - { - return true; - } - - // Check if it implements IList interface (covers AcObservableCollection and similar) - foreach (var iface in type.GetInterfaces()) - { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>)) - return true; - } - - return false; - } - #endregion } diff --git a/AyCode.Core/Extensions/AcJsonSerializer.cs b/AyCode.Core/Extensions/AcJsonSerializer.cs index de04680..b72e200 100644 --- a/AyCode.Core/Extensions/AcJsonSerializer.cs +++ b/AyCode.Core/Extensions/AcJsonSerializer.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Text; using AyCode.Core.Interfaces; using Newtonsoft.Json; +using static AyCode.Core.Extensions.JsonUtilities; namespace AyCode.Core.Extensions; @@ -16,354 +17,181 @@ namespace AyCode.Core.Extensions; /// Features: /// - Single-pass serialization with inline $id/$ref emission /// - StringBuilder-based output (no intermediate string allocations) -/// - Compiled expression tree property accessors (no reflection invoke overhead) +/// - Compiled expression tree property accessors /// - Smart reference tracking: only emits $id when object is actually referenced later -/// - Supports: IId<T>, JsonIgnoreAttribute, null skipping, all primitive types -/// - Skips default values: 0, false, empty strings, default enums (but NOT empty collections) +/// - MaxDepth support for controlling serialization depth /// public static class AcJsonSerializer { private static readonly ConcurrentDictionary TypeMetadataCache = new(); /// - /// Serialize object to JSON string with optimized reference handling. - /// Supports primitives, strings, enums, and complex objects. + /// Serialize object to JSON string with default options. /// - public static string Serialize(T value) + public static string Serialize(T value) => Serialize(value, AcJsonSerializerOptions.Default); + + /// + /// Serialize object to JSON string with specified options. + /// + public static string Serialize(T value, AcJsonSerializerOptions options) { if (value == null) return "null"; var type = typeof(T); - // Fast path for primitives - no reference tracking needed if (TrySerializePrimitive(value, type, out var primitiveJson)) return primitiveJson; - var context = new SerializationContext(); + var context = new SerializationContext(options); - // Phase 1: Scan for cross-references (objects that appear multiple times) - ScanReferences(value, context); + if (options.UseReferenceHandling) + ScanReferences(value, context, 0); - // Phase 2: Serialize with $id only for actually referenced objects context.StartWriting(); - WriteValue(value, context); + WriteValue(value, context, 0); return context.GetResult(); } - /// - /// Try to serialize a primitive value directly without context. - /// Returns true if value is primitive, false otherwise. - /// private static bool TrySerializePrimitive(T value, Type type, out string json) { - // Handle nullable underlying type var underlyingType = Nullable.GetUnderlyingType(type) ?? type; - if (underlyingType == typeof(string)) - { - json = SerializeString((string)(object)value!); - return true; - } + if (underlyingType == StringType) { json = SerializeString((string)(object)value!); return true; } + if (underlyingType == IntType) { json = ((int)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == LongType) { json = ((long)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == BoolType) { json = (bool)(object)value! ? "true" : "false"; return true; } - if (underlyingType == typeof(int)) - { - json = ((int)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(long)) - { - json = ((long)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(bool)) - { - json = (bool)(object)value! ? "true" : "false"; - return true; - } - - if (underlyingType == typeof(double)) + if (underlyingType == DoubleType) { var d = (double)(object)value!; json = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture); return true; } - if (underlyingType == typeof(decimal)) - { - json = ((decimal)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } + if (underlyingType == DecimalType) { json = ((decimal)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == typeof(float)) + if (underlyingType == FloatType) { var f = (float)(object)value!; json = float.IsNaN(f) || float.IsInfinity(f) ? "null" : f.ToString("G9", CultureInfo.InvariantCulture); return true; } - if (underlyingType == typeof(DateTime)) - { - json = $"\"{((DateTime)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; - return true; - } - - if (underlyingType == typeof(DateTimeOffset)) - { - json = $"\"{((DateTimeOffset)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; - return true; - } - - if (underlyingType == typeof(Guid)) - { - json = $"\"{((Guid)(object)value!).ToString("D")}\""; - return true; - } - - if (underlyingType == typeof(TimeSpan)) - { - json = $"\"{((TimeSpan)(object)value!).ToString("c", CultureInfo.InvariantCulture)}\""; - return true; - } - - if (underlyingType.IsEnum) - { - json = Convert.ToInt32(value).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(byte)) - { - json = ((byte)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(short)) - { - json = ((short)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(ushort)) - { - json = ((ushort)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(uint)) - { - json = ((uint)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(ulong)) - { - json = ((ulong)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(sbyte)) - { - json = ((sbyte)(object)value!).ToString(CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == typeof(char)) - { - json = SerializeString(value!.ToString()!); - return true; - } + if (underlyingType == DateTimeType) { json = $"\"{((DateTime)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; return true; } + if (underlyingType == DateTimeOffsetType) { json = $"\"{((DateTimeOffset)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; return true; } + if (underlyingType == GuidType) { json = $"\"{((Guid)(object)value!).ToString("D")}\""; return true; } + if (underlyingType == TimeSpanType) { json = $"\"{((TimeSpan)(object)value!).ToString("c", CultureInfo.InvariantCulture)}\""; return true; } + if (underlyingType.IsEnum) { json = Convert.ToInt32(value).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == ByteType) { json = ((byte)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == ShortType) { json = ((short)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == UShortType) { json = ((ushort)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == UIntType) { json = ((uint)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == ULongType) { json = ((ulong)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == SByteType) { json = ((sbyte)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } + if (underlyingType == CharType) { json = SerializeString(value!.ToString()!); return true; } json = ""; return false; } - /// - /// Serialize a string value with proper escaping. - /// private static string SerializeString(string value) { - // Fast path: if no escaping needed - if (!NeedsEscaping(value)) - return $"\"{value}\""; + if (!NeedsEscaping(value)) return $"\"{value}\""; var sb = new StringBuilder(value.Length + 2); sb.Append('"'); - foreach (var c in value) - { - switch (c) - { - case '"': sb.Append("\\\""); break; - case '\\': sb.Append("\\\\"); break; - case '\b': sb.Append("\\b"); break; - case '\f': sb.Append("\\f"); break; - case '\n': sb.Append("\\n"); break; - case '\r': sb.Append("\\r"); break; - case '\t': sb.Append("\\t"); break; - default: - if (c < 32) - { - sb.Append("\\u"); - sb.Append(((int)c).ToString("X4")); - } - else - { - sb.Append(c); - } - break; - } - } + WriteEscapedString(sb, value); sb.Append('"'); return sb.ToString(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool NeedsEscaping(string value) - { - foreach (var c in value) - { - if (c < 32 || c == '"' || c == '\\') - return true; - } - return false; - } + #region Reference Scanning - #region Phase 1: Reference Scanning - - private static void ScanReferences(object? value, SerializationContext context) + private static void ScanReferences(object? value, SerializationContext context, int depth) { if (value == null) return; + if (depth > context.MaxDepth) return; var type = value.GetType(); - // Skip primitives - use cached type check if (IsPrimitiveOrStringFast(type)) return; - // Track object occurrence - inline the check - if (!context.TrackForScanning(value)) - { - // Already seen - mark as needing $id - return; - } + if (!context.TrackForScanning(value)) return; - // Scan collections - check IEnumerable before getting metadata - if (value is IEnumerable enumerable && type != typeof(string)) + if (value is IEnumerable enumerable && type != StringType) { foreach (var item in enumerable) - { - if (item != null) - ScanReferences(item, context); - } + if (item != null) ScanReferences(item, context, depth + 1); return; } - // Scan object properties using cached metadata var metadata = GetTypeMetadata(type); - var properties = metadata.Properties; - var propCount = properties.Length; - - // Unroll small property counts for better performance - for (var i = 0; i < propCount; i++) + foreach (var prop in metadata.Properties) { - var propValue = properties[i].GetValue(value); - if (propValue != null) - ScanReferences(propValue, context); + var propValue = prop.GetValue(value); + if (propValue != null) ScanReferences(propValue, context, depth + 1); } } - // Faster primitive check using type code - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsPrimitiveOrStringFast(Type type) - { - var typeCode = Type.GetTypeCode(type); - return typeCode switch - { - TypeCode.Boolean or TypeCode.Char or TypeCode.SByte or TypeCode.Byte or - TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or - TypeCode.Int64 or TypeCode.UInt64 or TypeCode.Single or TypeCode.Double or - TypeCode.Decimal or TypeCode.DateTime or TypeCode.String => true, - _ => type == typeof(Guid) || type == typeof(TimeSpan) || type == typeof(DateTimeOffset) || type.IsEnum - }; - } - #endregion - #region Phase 2: Serialization + #region Serialization - private static void WriteValue(object? value, SerializationContext context) + private static void WriteValue(object? value, SerializationContext context, int depth) { - if (value == null) + if (value == null) { context.WriteNull(); return; } + + var type = value.GetType(); + + if (TryWritePrimitive(value, type, context)) return; + + // Check depth limit for complex types + if (depth > context.MaxDepth) { context.WriteNull(); return; } - var type = value.GetType(); + if (value is IDictionary dictionary) { WriteDictionary(dictionary, context, depth); return; } + if (value is IEnumerable enumerable && type != StringType) { WriteArray(enumerable, context, depth); return; } - // Primitives - if (TryWritePrimitive(value, type, context)) - return; - - // Dictionaries - must check before IEnumerable since Dictionary implements IEnumerable - if (value is IDictionary dictionary) - { - WriteDictionary(dictionary, context); - return; - } - - // Collections - if (value is IEnumerable enumerable && type != typeof(string)) - { - WriteArray(enumerable, context); - return; - } - - // Objects - WriteObject(value, type, context); + WriteObject(value, type, context, depth); } - private static void WriteObject(object value, Type type, SerializationContext context) + private static void WriteObject(object value, Type type, SerializationContext context, int depth) { - // Check if this is a reference we've already written - if (context.TryGetExistingRef(value, out var refId)) - { - context.WriteRef(refId); - return; + if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId)) + { + context.WriteRef(refId); + return; } context.WriteObjectStart(); var isFirst = true; - // Write $id if this object is referenced elsewhere - if (context.ShouldWriteId(value, out var id)) + if (context.UseReferenceHandling && context.ShouldWriteId(value, out var id)) { context.WritePropertyName("$id", ref isFirst); context.WriteString(id); context.MarkAsWritten(value, id); } - // Write properties var metadata = GetTypeMetadata(type); foreach (var prop in metadata.Properties) { var propValue = prop.GetValue(value); - - // Skip nulls if (propValue == null) continue; - - // Skip default values to reduce JSON size - if (IsDefaultValue(propValue, prop.PropertyType)) continue; + if (IsSerializerDefaultValue(propValue, prop.PropertyType)) continue; context.WritePropertyName(prop.JsonName, ref isFirst); - WriteValue(propValue, context); + WriteValue(propValue, context, depth + 1); } context.WriteObjectEnd(); } - private static void WriteArray(IEnumerable enumerable, SerializationContext context) + private static void WriteArray(IEnumerable enumerable, SerializationContext context, int depth) { context.WriteArrayStart(); var isFirst = true; @@ -372,25 +200,21 @@ public static class AcJsonSerializer { if (!isFirst) context.WriteComma(); isFirst = false; - WriteValue(item, context); + WriteValue(item, context, depth + 1); } context.WriteArrayEnd(); } - /// - /// Write a dictionary as a JSON object with keys as property names. - /// - private static void WriteDictionary(IDictionary dictionary, SerializationContext context) + private static void WriteDictionary(IDictionary dictionary, SerializationContext context, int depth) { context.WriteObjectStart(); var isFirst = true; foreach (DictionaryEntry entry in dictionary) { - var keyString = entry.Key?.ToString() ?? ""; - context.WritePropertyName(keyString, ref isFirst); - WriteValue(entry.Value, context); + context.WritePropertyName(entry.Key?.ToString() ?? "", ref isFirst); + WriteValue(entry.Value, context, depth + 1); } context.WriteObjectEnd(); @@ -398,242 +222,74 @@ public static class AcJsonSerializer private static bool TryWritePrimitive(object value, Type type, SerializationContext context) { - // Handle nullable underlying type var underlyingType = Nullable.GetUnderlyingType(type) ?? type; - if (underlyingType == typeof(string)) - { - context.WriteString((string)value); - return true; - } - - if (underlyingType == typeof(int)) - { - context.WriteInt((int)value); - return true; - } - - if (underlyingType == typeof(long)) - { - context.WriteLong((long)value); - return true; - } - - if (underlyingType == typeof(bool)) - { - context.WriteBool((bool)value); - return true; - } - - if (underlyingType == typeof(double)) - { - context.WriteDouble((double)value); - return true; - } - - if (underlyingType == typeof(decimal)) - { - context.WriteDecimal((decimal)value); - return true; - } - - if (underlyingType == typeof(float)) - { - context.WriteFloat((float)value); - return true; - } - - if (underlyingType == typeof(DateTime)) - { - context.WriteDateTime((DateTime)value); - return true; - } - - if (underlyingType == typeof(DateTimeOffset)) - { - context.WriteDateTimeOffset((DateTimeOffset)value); - return true; - } - - if (underlyingType == typeof(Guid)) - { - context.WriteGuid((Guid)value); - return true; - } - - if (underlyingType == typeof(TimeSpan)) - { - context.WriteTimeSpan((TimeSpan)value); - return true; - } - - if (underlyingType.IsEnum) - { - context.WriteInt(Convert.ToInt32(value)); - return true; - } - - if (underlyingType == typeof(byte)) - { - context.WriteInt((byte)value); - return true; - } - - if (underlyingType == typeof(short)) - { - context.WriteInt((short)value); - return true; - } - - if (underlyingType == typeof(ushort)) - { - context.WriteInt((ushort)value); - return true; - } - - if (underlyingType == typeof(uint)) - { - context.WriteLong((uint)value); - return true; - } - - if (underlyingType == typeof(ulong)) - { - context.WriteULong((ulong)value); - return true; - } - - if (underlyingType == typeof(sbyte)) - { - context.WriteInt((sbyte)value); - return true; - } - - if (underlyingType == typeof(char)) - { - context.WriteString(value.ToString()!); - return true; - } + if (underlyingType == StringType) { context.WriteString((string)value); return true; } + if (underlyingType == IntType) { context.WriteInt((int)value); return true; } + if (underlyingType == LongType) { context.WriteLong((long)value); return true; } + if (underlyingType == BoolType) { context.WriteBool((bool)value); return true; } + if (underlyingType == DoubleType) { context.WriteDouble((double)value); return true; } + if (underlyingType == DecimalType) { context.WriteDecimal((decimal)value); return true; } + if (underlyingType == FloatType) { context.WriteFloat((float)value); return true; } + if (underlyingType == DateTimeType) { context.WriteDateTime((DateTime)value); return true; } + if (underlyingType == DateTimeOffsetType) { context.WriteDateTimeOffset((DateTimeOffset)value); return true; } + if (underlyingType == GuidType) { context.WriteGuid((Guid)value); return true; } + if (underlyingType == TimeSpanType) { context.WriteTimeSpan((TimeSpan)value); return true; } + if (underlyingType.IsEnum) { context.WriteInt(Convert.ToInt32(value)); return true; } + if (underlyingType == ByteType) { context.WriteInt((byte)value); return true; } + if (underlyingType == ShortType) { context.WriteInt((short)value); return true; } + if (underlyingType == UShortType) { context.WriteInt((ushort)value); return true; } + if (underlyingType == UIntType) { context.WriteLong((uint)value); return true; } + if (underlyingType == ULongType) { context.WriteULong((ulong)value); return true; } + if (underlyingType == SByteType) { context.WriteInt((sbyte)value); return true; } + if (underlyingType == CharType) { context.WriteString(value.ToString()!); return true; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsPrimitiveOrString(Type type) - { - var t = Nullable.GetUnderlyingType(type) ?? type; - return IsPrimitiveOrStringFast(t); - } - - /// - /// Check if a value is the default value for its type (0, false, empty string, default enum). - /// NOTE: Empty collections are NOT skipped - they serialize as [] to preserve the distinction from null. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsDefaultValue(object value, Type propertyType) + private static bool IsSerializerDefaultValue(object value, Type propertyType) { var type = Nullable.GetUnderlyingType(propertyType) ?? propertyType; - // Check numeric types for 0 - if (type == typeof(int)) return (int)value == 0; - if (type == typeof(long)) return (long)value == 0L; - if (type == typeof(double)) return (double)value == 0.0; - if (type == typeof(decimal)) return (decimal)value == 0m; - if (type == typeof(float)) return (float)value == 0f; - if (type == typeof(byte)) return (byte)value == 0; - if (type == typeof(short)) return (short)value == 0; - if (type == typeof(ushort)) return (ushort)value == 0; - if (type == typeof(uint)) return (uint)value == 0; - if (type == typeof(ulong)) return (ulong)value == 0; - if (type == typeof(sbyte)) return (sbyte)value == 0; - - // Check bool for false - if (type == typeof(bool)) return (bool)value == false; - - // Check string for empty - if (type == typeof(string)) return string.IsNullOrEmpty((string)value); - - // Check enum for default (0) + if (type == IntType) return (int)value == 0; + if (type == LongType) return (long)value == 0L; + if (type == DoubleType) return (double)value == 0.0; + if (type == DecimalType) return (decimal)value == 0m; + if (type == FloatType) return (float)value == 0f; + if (type == ByteType) return (byte)value == 0; + if (type == ShortType) return (short)value == 0; + if (type == UShortType) return (ushort)value == 0; + if (type == UIntType) return (uint)value == 0; + if (type == ULongType) return (ulong)value == 0; + if (type == SByteType) return (sbyte)value == 0; + if (type == BoolType) return (bool)value == false; + if (type == StringType) return string.IsNullOrEmpty((string)value); if (type.IsEnum) return Convert.ToInt32(value) == 0; - - // DO NOT skip empty collections - they should serialize as [] - // This preserves the distinction between null and empty - - // Check Guid for empty - if (type == typeof(Guid)) return (Guid)value == Guid.Empty; - - // Check DateTime for default (MinValue) - // Note: We don't skip DateTime.MinValue as it may be intentional + if (type == GuidType) return (Guid)value == Guid.Empty; return false; } #endregion - #region Type Metadata Cache + #region Type Metadata private static TypeMetadata GetTypeMetadata(Type type) - { - return TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t)); - } + => TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t)); private sealed class TypeMetadata { public PropertyAccessor[] Properties { get; } - public bool IsIId { get; } - public Type? IdType { get; } - public Func? IdGetter { get; } public TypeMetadata(Type type) { - var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.GetIndexParameters().Length == 0 && - p.GetCustomAttribute() == null && - p.GetCustomAttribute() == null) + !HasJsonIgnoreAttribute(p)) .Select(p => new PropertyAccessor(p)) .ToArray(); - - Properties = props; - - // Check if type implements IId - var iidInterface = type.GetInterfaces() - .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IId<>)); - - if (iidInterface != null) - { - IsIId = true; - IdType = iidInterface.GetGenericArguments()[0]; - var idProp = type.GetProperty("Id"); - if (idProp != null) - { - // Use compiled expression for Id getter too - IdGetter = CreateCompiledGetter(type, idProp); - } - } - } - - /// - /// Creates a compiled getter delegate using expression trees. - /// This is ~10-50x faster than MethodInfo.Invoke. - /// - private static Func CreateCompiledGetter(Type declaringType, PropertyInfo prop) - { - // Parameter: object obj - var objParam = Expression.Parameter(typeof(object), "obj"); - - // Cast to actual type: (DeclaringType)obj - var castExpr = Expression.Convert(objParam, declaringType); - - // Access property: ((DeclaringType)obj).PropertyName - var propAccess = Expression.Property(castExpr, prop); - - // Box value types: (object)value - var boxed = Expression.Convert(propAccess, typeof(object)); - - // Compile: obj => (object)((DeclaringType)obj).PropertyName - var lambda = Expression.Lambda>(boxed, objParam); - return lambda.Compile(); } } @@ -647,32 +303,16 @@ public static class AcJsonSerializer { JsonName = prop.Name; PropertyType = prop.PropertyType; - - // Use compiled expression tree instead of MethodInfo.Invoke _getter = CreateCompiledGetter(prop.DeclaringType!, prop); } - /// - /// Creates a compiled getter delegate using expression trees. - /// This is ~10-50x faster than MethodInfo.Invoke. - /// private static Func CreateCompiledGetter(Type declaringType, PropertyInfo prop) { - // Parameter: object obj var objParam = Expression.Parameter(typeof(object), "obj"); - - // Cast to actual type: (DeclaringType)obj var castExpr = Expression.Convert(objParam, declaringType); - - // Access property: ((DeclaringType)obj).PropertyName var propAccess = Expression.Property(castExpr, prop); - - // Box value types: (object)value var boxed = Expression.Convert(propAccess, typeof(object)); - - // Compile: obj => (object)((DeclaringType)obj).PropertyName - var lambda = Expression.Lambda>(boxed, objParam); - return lambda.Compile(); + return Expression.Lambda>(boxed, objParam).Compile(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -685,50 +325,46 @@ public static class AcJsonSerializer private sealed class SerializationContext { - private readonly StringBuilder _sb; - private readonly Dictionary _scanOccurrences; - private readonly Dictionary _writtenRefs; - private readonly HashSet _multiReferenced; - private int _nextId; + private readonly StringBuilder _sb = new(4096); + private readonly Dictionary? _scanOccurrences; + private readonly Dictionary? _writtenRefs; + private readonly HashSet? _multiReferenced; + private int _nextId = 1; - // Use ArrayPool for number buffer to reduce allocations private static readonly ArrayPool CharPool = ArrayPool.Shared; - private readonly char[] _numberBuffer; + private readonly char[] _numberBuffer = CharPool.Rent(64); - public SerializationContext() + public bool UseReferenceHandling { get; } + public byte MaxDepth { get; } + + public SerializationContext(AcJsonSerializerOptions options) { - _sb = new StringBuilder(4096); - _scanOccurrences = new Dictionary(64, ReferenceEqualityComparer.Instance); - _writtenRefs = new Dictionary(32, ReferenceEqualityComparer.Instance); - _multiReferenced = new HashSet(32, ReferenceEqualityComparer.Instance); - _numberBuffer = CharPool.Rent(64); - _nextId = 1; + UseReferenceHandling = options.UseReferenceHandling; + MaxDepth = options.MaxDepth; + + if (UseReferenceHandling) + { + _scanOccurrences = new Dictionary(64, ReferenceEqualityComparer.Instance); + _writtenRefs = new Dictionary(32, ReferenceEqualityComparer.Instance); + _multiReferenced = new HashSet(32, ReferenceEqualityComparer.Instance); + } } - /// - /// Track object during scan phase. Returns false if already seen (multi-referenced). - /// public bool TrackForScanning(object obj) { + if (_scanOccurrences == null) return true; + ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); - if (exists) - { - count++; - _multiReferenced.Add(obj); - return false; - } + if (exists) { count++; _multiReferenced!.Add(obj); return false; } count = 1; return true; } public void StartWriting() { } - /// - /// Check if this object needs a $id (is referenced elsewhere). - /// public bool ShouldWriteId(object obj, out string id) { - if (_multiReferenced.Contains(obj) && !_writtenRefs.ContainsKey(obj)) + if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj)) { id = _nextId++.ToString(); return true; @@ -737,194 +373,94 @@ public static class AcJsonSerializer return false; } - public void MarkAsWritten(object obj, string id) - { - _writtenRefs[obj] = id; - } - + public void MarkAsWritten(object obj, string id) => _writtenRefs![obj] = id; + public bool TryGetExistingRef(object obj, out string refId) { - return _writtenRefs.TryGetValue(obj, out refId!); + if (_writtenRefs != null) return _writtenRefs.TryGetValue(obj, out refId!); + refId = ""; + return false; } + + public void WriteRef(string refId) { _sb.Append("{\"$ref\":\""); _sb.Append(refId); _sb.Append("\"}"); } - public void WriteRef(string refId) - { - _sb.Append("{\"$ref\":\""); - _sb.Append(refId); - _sb.Append("\"}"); - } + public string GetResult() { CharPool.Return(_numberBuffer); return _sb.ToString(); } - public string GetResult() - { - var result = _sb.ToString(); - CharPool.Return(_numberBuffer); - return result; - } - - // Write methods - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteNull() => _sb.Append("null"); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteObjectStart() => _sb.Append('{'); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteObjectEnd() => _sb.Append('}'); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteArrayStart() => _sb.Append('['); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteArrayEnd() => _sb.Append(']'); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteComma() => _sb.Append(','); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteNull() => _sb.Append("null"); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteObjectStart() => _sb.Append('{'); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteObjectEnd() => _sb.Append('}'); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayStart() => _sb.Append('['); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayEnd() => _sb.Append(']'); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteComma() => _sb.Append(','); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WritePropertyName(string name, ref bool isFirst) { if (!isFirst) _sb.Append(','); isFirst = false; - _sb.Append('"'); - _sb.Append(name); - _sb.Append("\":"); + _sb.Append('"'); _sb.Append(name); _sb.Append("\":"); } public void WriteString(string value) { _sb.Append('"'); - // Fast path: if no escaping needed, append directly - if (!NeedsEscaping(value)) - { - _sb.Append(value); - } - else - { - WriteEscapedString(value); - } + if (!NeedsEscaping(value)) _sb.Append(value); + else WriteEscapedString(_sb, value); _sb.Append('"'); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool NeedsEscaping(string value) - { - foreach (var c in value) - { - if (c < 32 || c == '"' || c == '\\') - return true; - } - return false; - } - - private void WriteEscapedString(string value) - { - foreach (var c in value) - { - switch (c) - { - case '"': _sb.Append("\\\""); break; - case '\\': _sb.Append("\\\\"); break; - case '\b': _sb.Append("\\b"); break; - case '\f': _sb.Append("\\f"); break; - case '\n': _sb.Append("\\n"); break; - case '\r': _sb.Append("\\r"); break; - case '\t': _sb.Append("\\t"); break; - default: - if (c < 32) - { - _sb.Append("\\u"); - _sb.Append(((int)c).ToString("X4")); - } - else - { - _sb.Append(c); - } - break; - } - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteInt(int value) { - if (value.TryFormat(_numberBuffer, out var written)) - _sb.Append(_numberBuffer, 0, written); - else - _sb.Append(value); + if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written); + else _sb.Append(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteLong(long value) { - if (value.TryFormat(_numberBuffer, out var written)) - _sb.Append(_numberBuffer, 0, written); - else - _sb.Append(value); + if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written); + else _sb.Append(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteULong(ulong value) { - if (value.TryFormat(_numberBuffer, out var written)) - _sb.Append(_numberBuffer, 0, written); - else - _sb.Append(value); + if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written); + else _sb.Append(value); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteBool(bool value) => _sb.Append(value ? "true" : "false"); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteBool(bool value) => _sb.Append(value ? "true" : "false"); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteDouble(double value) { - if (double.IsNaN(value) || double.IsInfinity(value)) - { - _sb.Append("null"); - } - else if (value.TryFormat(_numberBuffer, out var written, "G17", CultureInfo.InvariantCulture)) - { - _sb.Append(_numberBuffer, 0, written); - } - else - { - _sb.Append(value.ToString("G17", CultureInfo.InvariantCulture)); - } + if (double.IsNaN(value) || double.IsInfinity(value)) _sb.Append("null"); + else if (value.TryFormat(_numberBuffer, out var written, "G17", CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written); + else _sb.Append(value.ToString("G17", CultureInfo.InvariantCulture)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteFloat(float value) { - if (float.IsNaN(value) || float.IsInfinity(value)) - { - _sb.Append("null"); - } - else if (value.TryFormat(_numberBuffer, out var written, "G9", CultureInfo.InvariantCulture)) - { - _sb.Append(_numberBuffer, 0, written); - } - else - { - _sb.Append(value.ToString("G9", CultureInfo.InvariantCulture)); - } + if (float.IsNaN(value) || float.IsInfinity(value)) _sb.Append("null"); + else if (value.TryFormat(_numberBuffer, out var written, "G9", CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written); + else _sb.Append(value.ToString("G9", CultureInfo.InvariantCulture)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteDecimal(decimal value) { - if (value.TryFormat(_numberBuffer, out var written, provider: CultureInfo.InvariantCulture)) - _sb.Append(_numberBuffer, 0, written); - else - _sb.Append(value.ToString(CultureInfo.InvariantCulture)); + if (value.TryFormat(_numberBuffer, out var written, provider: CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written); + else _sb.Append(value.ToString(CultureInfo.InvariantCulture)); } public void WriteDateTime(DateTime value) { _sb.Append('"'); - Span buffer = stackalloc char[33]; // ISO 8601 max length - if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) - _sb.Append(buffer[..written]); - else - _sb.Append(value.ToString("O", CultureInfo.InvariantCulture)); + Span buffer = stackalloc char[33]; + if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]); + else _sb.Append(value.ToString("O", CultureInfo.InvariantCulture)); _sb.Append('"'); } @@ -932,10 +468,8 @@ public static class AcJsonSerializer { _sb.Append('"'); Span buffer = stackalloc char[33]; - if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) - _sb.Append(buffer[..written]); - else - _sb.Append(value.ToString("O", CultureInfo.InvariantCulture)); + if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]); + else _sb.Append(value.ToString("O", CultureInfo.InvariantCulture)); _sb.Append('"'); } @@ -943,10 +477,8 @@ public static class AcJsonSerializer { _sb.Append('"'); Span buffer = stackalloc char[36]; - if (value.TryFormat(buffer, out var written, "D")) - _sb.Append(buffer[..written]); - else - _sb.Append(value.ToString("D")); + if (value.TryFormat(buffer, out var written, "D")) _sb.Append(buffer[..written]); + else _sb.Append(value.ToString("D")); _sb.Append('"'); } @@ -954,13 +486,21 @@ public static class AcJsonSerializer { _sb.Append('"'); Span buffer = stackalloc char[26]; - if (value.TryFormat(buffer, out var written, "c", CultureInfo.InvariantCulture)) - _sb.Append(buffer[..written]); - else - _sb.Append(value.ToString("c", CultureInfo.InvariantCulture)); + if (value.TryFormat(buffer, out var written, "c", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]); + else _sb.Append(value.ToString("c", CultureInfo.InvariantCulture)); _sb.Append('"'); } } #endregion } + +/// +/// Reference equality comparer for object identity comparison. +/// +internal sealed class ReferenceEqualityComparer : IEqualityComparer +{ + public static readonly ReferenceEqualityComparer Instance = new(); + public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); + public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); +} diff --git a/AyCode.Core/Extensions/JsonUtilities.cs b/AyCode.Core/Extensions/JsonUtilities.cs index 6f8a986..c0772f9 100644 --- a/AyCode.Core/Extensions/JsonUtilities.cs +++ b/AyCode.Core/Extensions/JsonUtilities.cs @@ -1,10 +1,126 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Globalization; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; +using AyCode.Core.Interfaces; +using Newtonsoft.Json; namespace AyCode.Core.Extensions; -internal static class JsonUtilities +/// +/// Options for AcJsonSerializer and AcJsonDeserializer. +/// +public sealed class AcJsonSerializerOptions { + /// + /// Default options instance with reference handling enabled and max depth. + /// + public static readonly AcJsonSerializerOptions Default = new(); + + /// + /// Options for shallow serialization (root level only, no references). + /// + public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false }; + + /// + /// Whether to use $id/$ref reference handling for circular references. + /// Default: true + /// + public bool UseReferenceHandling { get; init; } = true; + + /// + /// Maximum depth for serialization/deserialization. + /// 0 = root level only (primitives of root object) + /// 1 = root + first level of nested objects/collections + /// byte.MaxValue (255) = effectively unlimited + /// Default: byte.MaxValue + /// + public byte MaxDepth { get; init; } = byte.MaxValue; + + /// + /// Creates options with specified max depth. + /// + public static AcJsonSerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth }; + + /// + /// Creates options without reference handling. + /// + public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false }; +} + +/// +/// Central utilities for JSON serialization/deserialization. +/// Contains shared type caches, primitive type checks, and string utilities. +/// +public static class JsonUtilities +{ + #region Pre-computed Type Handles + + public static readonly Type IntType = typeof(int); + public static readonly Type LongType = typeof(long); + public static readonly Type DoubleType = typeof(double); + public static readonly Type DecimalType = typeof(decimal); + public static readonly Type FloatType = typeof(float); + public static readonly Type StringType = typeof(string); + public static readonly Type DateTimeType = typeof(DateTime); + public static readonly Type GuidType = typeof(Guid); + public static readonly Type BoolType = typeof(bool); + public static readonly Type DateTimeOffsetType = typeof(DateTimeOffset); + public static readonly Type TimeSpanType = typeof(TimeSpan); + public static readonly Type ByteType = typeof(byte); + public static readonly Type ShortType = typeof(short); + public static readonly Type UShortType = typeof(ushort); + public static readonly Type UIntType = typeof(uint); + public static readonly Type ULongType = typeof(ulong); + public static readonly Type SByteType = typeof(sbyte); + public static readonly Type CharType = typeof(char); + + #endregion + + #region Cached Generic Type Definitions + + internal static readonly Type IEnumerableGenericType = typeof(IEnumerable<>); + internal static readonly Type IIdGenericType = typeof(IId<>); + internal static readonly Type NullableGenericType = typeof(Nullable<>); + internal static readonly Type IListGenericType = typeof(IList<>); + internal static readonly Type ListGenericType = typeof(List<>); + internal static readonly Type DictionaryGenericType = typeof(Dictionary<,>); + internal static readonly Type IDictionaryGenericType = typeof(IDictionary<,>); + internal static readonly Type ObservableCollectionType = typeof(System.Collections.ObjectModel.ObservableCollection<>); + internal static readonly Type CollectionType = typeof(System.Collections.ObjectModel.Collection<>); + + #endregion + + #region Primitive Type Set + + private static readonly FrozenSet PrimitiveTypes = new HashSet + { + typeof(string), typeof(decimal), typeof(DateTime), + typeof(DateTimeOffset), typeof(Guid), typeof(TimeSpan), + typeof(bool), typeof(byte), typeof(sbyte), typeof(short), + typeof(ushort), typeof(int), typeof(uint), typeof(long), + typeof(ulong), typeof(float), typeof(double), typeof(char) + }.ToFrozenSet(); + + #endregion + + #region Type Caches + + private static readonly ConcurrentDictionary IdInfoCache = new(); + private static readonly ConcurrentDictionary CollectionElementCache = new(); + private static readonly ConcurrentDictionary IsPrimitiveCache = new(); + private static readonly ConcurrentDictionary IsCollectionCache = new(); + private static readonly ConcurrentDictionary IsPrimitiveCollectionCache = new(); + private static readonly ConcurrentDictionary JsonIgnoreCache = new(); + private static readonly ConcurrentDictionary> ListFactoryCache = new(); + + #endregion + + #region String Utilities + /// /// Unwraps a JSON string that may be double-quoted (e.g., "\"...\"" -> ...). /// Optimized to avoid Regex.Unescape allocation when no escape sequences exist. @@ -18,23 +134,14 @@ internal static class JsonUtilities if (span.Length < 2 || span[0] != '"' || span[^1] != '"') return json; - // Extract inner content (without outer quotes) var inner = span[1..^1]; - // Fast path: check if any escape sequences exist if (!inner.Contains('\\')) - { - // No escapes - just return substring (single allocation) return json.Substring(1, json.Length - 2); - } - // Slow path: unescape the string return UnescapeJsonString(inner); } - /// - /// Manual JSON string unescaping - avoids Regex.Unescape overhead. - /// private static string UnescapeJsonString(ReadOnlySpan input) { var sb = new StringBuilder(input.Length); @@ -51,54 +158,24 @@ internal static class JsonUtilities var next = input[i + 1]; switch (next) { - case '"': - sb.Append('"'); - i++; - break; - case '\\': - sb.Append('\\'); - i++; - break; - case '/': - sb.Append('/'); - i++; - break; - case 'b': - sb.Append('\b'); - i++; - break; - case 'f': - sb.Append('\f'); - i++; - break; - case 'n': - sb.Append('\n'); - i++; - break; - case 'r': - sb.Append('\r'); - i++; - break; - case 't': - sb.Append('\t'); - i++; - break; + case '"': sb.Append('"'); i++; break; + case '\\': sb.Append('\\'); i++; break; + case '/': sb.Append('/'); i++; break; + case 'b': sb.Append('\b'); i++; break; + case 'f': sb.Append('\f'); i++; break; + case 'n': sb.Append('\n'); i++; break; + case 'r': sb.Append('\r'); i++; break; + case 't': sb.Append('\t'); i++; break; case 'u' when i + 5 < input.Length: - // Unicode escape: \uXXXX var hex = input.Slice(i + 2, 4); if (TryParseHex(hex, out var unicode)) { sb.Append((char)unicode); i += 5; } - else - { - sb.Append(c); - } - break; - default: - sb.Append(c); + else sb.Append(c); break; + default: sb.Append(c); break; } } @@ -112,15 +189,275 @@ internal static class JsonUtilities foreach (var c in hex) { value <<= 4; - if (c >= '0' && c <= '9') - value |= c - '0'; - else if (c >= 'a' && c <= 'f') - value |= c - 'a' + 10; - else if (c >= 'A' && c <= 'F') - value |= c - 'A' + 10; - else - return false; + if (c >= '0' && c <= '9') value |= c - '0'; + else if (c >= 'a' && c <= 'f') value |= c - 'a' + 10; + else if (c >= 'A' && c <= 'F') value |= c - 'A' + 10; + else return false; } return true; } + + /// + /// Checks if a string needs JSON escaping. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool NeedsEscaping(string value) + { + foreach (var c in value) + { + if (c < 32 || c == '"' || c == '\\') + return true; + } + return false; + } + + /// + /// Escapes a string for JSON output. + /// + public static void WriteEscapedString(StringBuilder sb, string value) + { + foreach (var c in value) + { + switch (c) + { + case '"': sb.Append("\\\""); break; + case '\\': sb.Append("\\\\"); break; + case '\b': sb.Append("\\b"); break; + case '\f': sb.Append("\\f"); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + default: + if (c < 32) + { + sb.Append("\\u"); + sb.Append(((int)c).ToString("X4")); + } + else sb.Append(c); + break; + } + } + } + + #endregion + + #region Type Checking Methods + + /// + /// Fast primitive check using type code. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPrimitiveOrString(Type type) + { + return IsPrimitiveCache.GetOrAdd(type, static t => + { + if (t.IsPrimitive || PrimitiveTypes.Contains(t)) return true; + if (t.IsGenericType && t.GetGenericTypeDefinition() == NullableGenericType) + return IsPrimitiveOrString(t.GetGenericArguments()[0]); + if (t.IsEnum) return true; + return false; + }); + } + + /// + /// Faster primitive check using TypeCode for hot paths. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPrimitiveOrStringFast(Type type) + { + var typeCode = Type.GetTypeCode(type); + return typeCode switch + { + TypeCode.Boolean or TypeCode.Char or TypeCode.SByte or TypeCode.Byte or + TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or + TypeCode.Int64 or TypeCode.UInt64 or TypeCode.Single or TypeCode.Double or + TypeCode.Decimal or TypeCode.DateTime or TypeCode.String => true, + _ => type == GuidType || type == TimeSpanType || type == DateTimeOffsetType || type.IsEnum + }; + } + + /// + /// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsGenericCollectionType(Type type) + { + return IsCollectionCache.GetOrAdd(type, static t => + { + if (t == StringType || t.IsPrimitive) return false; + if (t.IsArray) return true; + + if (t.IsGenericType) + { + var genericDef = t.GetGenericTypeDefinition(); + if (genericDef == ListGenericType || + genericDef == IListGenericType || + genericDef == typeof(ICollection<>) || + genericDef == IEnumerableGenericType || + genericDef == ObservableCollectionType || + genericDef == CollectionType) + return true; + } + + foreach (var iface in t.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType) + return true; + } + + return typeof(IEnumerable).IsAssignableFrom(t); + }); + } + + /// + /// Checks if type is a dictionary type. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType) + { + keyType = null; + valueType = null; + + if (!type.IsGenericType) return false; + + var genericDef = type.GetGenericTypeDefinition(); + if (genericDef == DictionaryGenericType || genericDef == IDictionaryGenericType) + { + var args = type.GetGenericArguments(); + keyType = args[0]; + valueType = args[1]; + return true; + } + + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IDictionaryGenericType) + { + var args = iface.GetGenericArguments(); + keyType = args[0]; + valueType = args[1]; + return true; + } + } + + return false; + } + + /// + /// Gets the element type of a collection. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Type? GetCollectionElementType(Type collectionType) + { + return CollectionElementCache.GetOrAdd(collectionType, static type => + { + if (type.IsArray) + return type.GetElementType(); + + if (type.IsGenericType) + { + var genericDef = type.GetGenericTypeDefinition(); + if (genericDef == ListGenericType || genericDef == IListGenericType || + genericDef == typeof(ICollection<>) || genericDef == IEnumerableGenericType) + return type.GetGenericArguments()[0]; + } + + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType) + return iface.GetGenericArguments()[0]; + } + + return typeof(object); + }); + } + + /// + /// Gets IId info for a type. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (bool IsId, Type? IdType) GetIdInfo(Type type) + { + return IdInfoCache.GetOrAdd(type, static t => + { + foreach (var iface in t.GetInterfaces()) + { + if (!iface.IsGenericType) continue; + if (iface.GetGenericTypeDefinition() != IIdGenericType) continue; + var idType = iface.GetGenericArguments()[0]; + return (idType.IsValueType, idType); + } + return (false, null); + }); + } + + /// + /// Checks if property has JsonIgnore attribute. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasJsonIgnoreAttribute(PropertyInfo prop) + { + return JsonIgnoreCache.GetOrAdd(prop, static p => + Attribute.IsDefined(p, typeof(JsonIgnoreAttribute)) || + Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute))); + } + + /// + /// Checks if collection contains primitive elements. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPrimitiveElementCollection(Type type) + { + return IsPrimitiveCollectionCache.GetOrAdd(type, static t => + { + if (t == StringType) return false; + + Type? elementType = null; + if (t.IsArray) + elementType = t.GetElementType(); + else if (t.IsGenericType && typeof(IEnumerable).IsAssignableFrom(t)) + { + var genericArgs = t.GetGenericArguments(); + if (genericArgs.Length == 1) elementType = genericArgs[0]; + } + + if (elementType == null) return false; + return IsPrimitiveOrString(elementType) || elementType.IsEnum; + }); + } + + /// + /// Gets or creates a list factory for a given element type. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Func GetOrCreateListFactory(Type elementType) + { + return ListFactoryCache.GetOrAdd(elementType, static t => + { + var listType = ListGenericType.MakeGenericType(t); + var newExpr = System.Linq.Expressions.Expression.New(listType); + var castExpr = System.Linq.Expressions.Expression.Convert(newExpr, typeof(IList)); + return System.Linq.Expressions.Expression.Lambda>(castExpr).Compile(); + }); + } + + /// + /// Checks if value is the default value for its type. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDefaultValue(object id, Type idType) + { + if (ReferenceEquals(idType, IntType)) return (int)id == 0; + if (ReferenceEquals(idType, LongType)) return (long)id == 0; + if (ReferenceEquals(idType, GuidType)) return (Guid)id == Guid.Empty; + if (ReferenceEquals(idType, ShortType)) return (short)id == 0; + if (ReferenceEquals(idType, ByteType)) return (byte)id == 0; + if (ReferenceEquals(idType, UIntType)) return (uint)id == 0; + if (ReferenceEquals(idType, ULongType)) return (ulong)id == 0; + if (ReferenceEquals(idType, UShortType)) return (ushort)id == 0; + if (ReferenceEquals(idType, SByteType)) return (sbyte)id == 0; + return false; + } + + #endregion } diff --git a/AyCode.Core/Extensions/MergeContractResolver.cs b/AyCode.Core/Extensions/MergeContractResolver.cs index 1c9b5ff..601a620 100644 --- a/AyCode.Core/Extensions/MergeContractResolver.cs +++ b/AyCode.Core/Extensions/MergeContractResolver.cs @@ -1,657 +1,459 @@ using System.Collections; using System.Collections.Concurrent; -using System.Collections.Frozen; using System.Reflection; using System.Runtime.CompilerServices; using AyCode.Core.Interfaces; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; +using static AyCode.Core.Extensions.JsonUtilities; -namespace AyCode.Core.Extensions +namespace AyCode.Core.Extensions; + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public sealed class JsonNoMergeCollectionAttribute : Attribute { } + +/// +/// Cached property metadata for faster JSON processing. +/// +public sealed class CachedPropertyInfo { - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public sealed class JsonNoMergeCollectionAttribute : Attribute { } + public PropertyInfo Property { get; } + public string Name { get; } + public Type PropertyType { get; } + public bool IsIId { get; } + public Type? IdType { get; } + public bool IsIIdCollection { get; } + public Type? CollectionElementType { get; } + public Type? CollectionElementIdType { get; } + public bool ShouldSkip { get; } - /// - /// Cached property metadata for faster JSON processing. - /// - public sealed class CachedPropertyInfo + public CachedPropertyInfo(PropertyInfo prop) { - public PropertyInfo Property { get; } - public string Name { get; } - public Type PropertyType { get; } - public bool IsIId { get; } - public Type? IdType { get; } - public bool IsIIdCollection { get; } - public Type? CollectionElementType { get; } - public Type? CollectionElementIdType { get; } - public bool ShouldSkip { get; } - public bool CanRead { get; } - public bool HasIndexParameters { get; } - - public CachedPropertyInfo(PropertyInfo prop) - { - Property = prop; - Name = prop.Name; - PropertyType = prop.PropertyType; - CanRead = prop.CanRead; - HasIndexParameters = prop.GetIndexParameters().Length > 0; - - // Pre-compute skip condition - ShouldSkip = !CanRead || HasIndexParameters || TypeCache.HasJsonIgnoreAttribute(prop); - - if (!ShouldSkip) - { - var (isId, idType) = TypeCache.GetIdInfo(PropertyType); - IsIId = isId; - IdType = idType; - - if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != typeof(string)) - { - CollectionElementType = TypeCache.GetElementType(PropertyType); - if (CollectionElementType != null) - { - var (elemIsId, elemIdType) = TypeCache.GetIdInfo(CollectionElementType); - IsIIdCollection = elemIsId; - CollectionElementIdType = elemIdType; - } - } - } - } - } - - /// - /// Static type metadata cache - thread-safe because shared across all serialization operations - /// 🔑 OPTIMIZATION: Uses FrozenDictionary for hot-path lookups after warmup - /// - public static class TypeCache - { - private static readonly ConcurrentDictionary _idCache = new(); - private static readonly ConcurrentDictionary _collectionElemCache = new(); - private static readonly ConcurrentDictionary _typeNameCache = new(); - private static readonly ConcurrentDictionary _cachedPropertyInfoCache = new(); - private static readonly ConcurrentDictionary _jsonIgnoreCache = new(); - private static readonly ConcurrentDictionary _isPrimitiveCache = new(); - private static readonly ConcurrentDictionary _isPrimitiveElementCollectionCache = new(); - private static readonly ConcurrentDictionary _isCollectionTypeCache = new(); + Property = prop; + Name = prop.Name; + PropertyType = prop.PropertyType; - // 🔑 Type ID cache for long-based semantic IDs - private static readonly ConcurrentDictionary _typeIdCache = new(); - private static int _typeIdCounter; - - // 🔑 OPTIMIZATION: Pre-computed primitive types set - private static readonly FrozenSet PrimitiveTypes = new HashSet - { - typeof(string), typeof(decimal), typeof(DateTime), - typeof(DateTimeOffset), typeof(Guid), typeof(TimeSpan), - typeof(bool), typeof(byte), typeof(sbyte), typeof(short), - typeof(ushort), typeof(int), typeof(uint), typeof(long), - typeof(ulong), typeof(float), typeof(double), typeof(char) - }.ToFrozenSet(); - - // 🔑 OPTIMIZATION: Cache generic type definitions to avoid repeated GetGenericTypeDefinition calls - private static readonly Type IEnumerableGenericType = typeof(IEnumerable<>); - private static readonly Type IIdGenericType = typeof(IId<>); - private static readonly Type NullableGenericType = typeof(Nullable<>); - - /// - /// Gets a unique integer ID for a type (thread-safe, consistent within app lifetime) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetTypeId(Type t) - { - return _typeIdCache.GetOrAdd(t, _ => Interlocked.Increment(ref _typeIdCounter)); - } - - /// - /// Creates a long-based semantic ID from type ID and object ID. - /// Format: [16 bits typeId][48 bits objectId] - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long CreateSemanticId(int typeId, long objectId) - { - return ((long)typeId << 48) | (objectId & 0xFFFFFFFFFFFF); - } - - /// - /// Extracts the type ID from a semantic ID - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ExtractTypeId(long semanticId) => (int)(semanticId >> 48); - - /// - /// Extracts the object ID from a semantic ID - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long ExtractObjectId(long semanticId) => semanticId & 0xFFFFFFFFFFFF; - - /// - /// Converts any ID value to a long for semantic ID creation. - /// Supports: int, long, Guid, short, byte, uint, ulong, ushort. - /// Throws for unsupported types to prevent cross-process hash inconsistencies. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long IdToLong(object idValue) - { - return idValue switch - { - int i => i, - long l => l, - Guid g => GuidToLong(g), - short s => s, - byte b => b, - uint ui => ui, - ulong ul => (long)ul, - ushort us => us, - sbyte sb => sb, - _ => throw new NotSupportedException( - $"ID type '{idValue.GetType().Name}' is not supported for semantic ID generation. " + - $"Supported types: int, long, Guid, short, byte, uint, ulong, ushort, sbyte. " + - $"Using GetHashCode() would cause inconsistent IDs between client and server.") - }; - } - - /// - /// 🔑 OPTIMIZATION: Generic ID to long conversion without boxing for common types - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long IdToLong(TId id) where TId : struct - { - if (typeof(TId) == typeof(int)) return Unsafe.As(ref id); - if (typeof(TId) == typeof(long)) return Unsafe.As(ref id); - if (typeof(TId) == typeof(Guid)) return GuidToLong(Unsafe.As(ref id)); - if (typeof(TId) == typeof(short)) return Unsafe.As(ref id); - if (typeof(TId) == typeof(byte)) return Unsafe.As(ref id); - if (typeof(TId) == typeof(uint)) return Unsafe.As(ref id); - if (typeof(TId) == typeof(ulong)) return (long)Unsafe.As(ref id); - if (typeof(TId) == typeof(ushort)) return Unsafe.As(ref id); - if (typeof(TId) == typeof(sbyte)) return Unsafe.As(ref id); - - // Fallback with boxing for unknown types - return IdToLong((object)id); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long GuidToLong(Guid guid) - { - Span bytes = stackalloc byte[16]; - guid.TryWriteBytes(bytes); - var high = BitConverter.ToInt64(bytes); - var low = BitConverter.ToInt64(bytes.Slice(8)); - return high ^ low; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetTypeName(Type t) - { - return _typeNameCache.GetOrAdd(t, static type => type.Name); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (bool IsId, Type? IdType) GetIdInfo(Type t) - { - return _idCache.GetOrAdd(t, static type => - { - var interfaces = type.GetInterfaces(); - for (var i = 0; i < interfaces.Length; i++) - { - var iface = interfaces[i]; - if (!iface.IsGenericType) continue; - if (iface.GetGenericTypeDefinition() != IIdGenericType) continue; - var idType = iface.GetGenericArguments()[0]; - return (idType.IsValueType, idType); - } - return (false, null); - }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Type? GetElementType(Type t) - { - return _collectionElemCache.GetOrAdd(t, static type => - { - if (type.IsArray) return type.GetElementType(); - - if (type.IsGenericType && type.GetGenericTypeDefinition() == IEnumerableGenericType) - return type.GetGenericArguments()[0]; - - var interfaces = type.GetInterfaces(); - for (var i = 0; i < interfaces.Length; i++) - { - var iface = interfaces[i]; - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IEnumerableGenericType) - return iface.GetGenericArguments()[0]; - } - return null; - }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static CachedPropertyInfo[] GetCachedProperties(Type t) - { - return _cachedPropertyInfoCache.GetOrAdd(t, static type => - { - var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - var cached = new CachedPropertyInfo[props.Length]; - for (var i = 0; i < props.Length; i++) - cached[i] = new CachedPropertyInfo(props[i]); - return cached; - }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasJsonIgnoreAttribute(PropertyInfo prop) - { - return _jsonIgnoreCache.GetOrAdd(prop, static p => - p.GetCustomAttribute() != null || - p.GetCustomAttribute() != null); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsPrimitive(Type t) - { - return _isPrimitiveCache.GetOrAdd(t, static type => - { - if (type.IsPrimitive || PrimitiveTypes.Contains(type)) return true; - if (type.IsGenericType && type.GetGenericTypeDefinition() == NullableGenericType) - return IsPrimitive(type.GetGenericArguments()[0]); - return false; - }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsPrimitiveElementCollection(Type type) - { - return _isPrimitiveElementCollectionCache.GetOrAdd(type, static t => - { - if (t == typeof(string)) return false; - - Type? elementType = null; - if (t.IsArray) - elementType = t.GetElementType(); - else if (t.IsGenericType && typeof(IEnumerable).IsAssignableFrom(t)) - { - var genericArgs = t.GetGenericArguments(); - if (genericArgs.Length == 1) elementType = genericArgs[0]; - } - - if (elementType == null) return false; - return IsPrimitive(elementType) || elementType.IsEnum; - }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsCollectionType(Type type) - { - return _isCollectionTypeCache.GetOrAdd(type, static t => - { - if (t == typeof(string) || t.IsPrimitive) return false; - return t.IsArray || typeof(IEnumerable).IsAssignableFrom(t); - }); - } - } - - /// - /// Converter for IId collections that supports merging by ID (update existing, add new, keep unmentioned) - /// 🔑 OPTIMIZATION: Uses pre-computed type ID and EqualityComparer - /// - public class IdAwareCollectionMergeConverter : JsonConverter - where TItem : class, IId, new() where TId : struct - { - private static readonly int CachedTypeId = TypeCache.GetTypeId(typeof(TItem)); - private static readonly EqualityComparer IdComparer = EqualityComparer.Default; - - public override bool CanRead => true; - public override bool CanWrite => false; - public override bool CanConvert(Type objectType) => - typeof(ICollection).IsAssignableFrom(objectType) || typeof(IEnumerable).IsAssignableFrom(objectType); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static long GetSemanticId(TId id) => TypeCache.CreateSemanticId(CachedTypeId, TypeCache.IdToLong(id)); - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return existingValue; - - if (existingValue is not IList targetList) - { - var jsonArrayFallback = JArray.Load(reader); - return jsonArrayFallback.ToObject(objectType, serializer); - } - - var isFixedSize = targetList.IsFixedSize; - var jsonArray = JArray.Load(reader); - var jsonCount = jsonArray.Count; - var existingCount = targetList.Count; - - // 🔑 OPTIMIZATION: Pre-sized dictionary with exact capacity - var existingItemsMap = new Dictionary(existingCount); - for (var index = 0; index < existingCount; index++) - { - if (targetList[index] is TItem item && !IdComparer.Equals(item.Id, default)) - existingItemsMap[GetSemanticId(item.Id)] = item; - } - - // 🔑 OPTIMIZATION: Pre-sized collections - var estimatedCapacity = jsonCount + existingCount; - var finalItems = new List(estimatedCapacity); - var processedIds = new HashSet(jsonCount); - - for (var i = 0; i < jsonCount; i++) - { - var itemToken = jsonArray[i]; - TItem? itemResult = null; - - if (itemToken is JObject jObj) - { - var incomingId = IdExtractor.GetIdFromJToken(jObj); - var hasId = !IdComparer.Equals(incomingId, default); - var semanticId = hasId ? GetSemanticId(incomingId) : 0L; - - TItem? existingItem = hasId && existingItemsMap.TryGetValue(semanticId, out var found) ? found : null; - - if (existingItem != null) - { - using var subReader = jObj.CreateReader(); - serializer.Populate(subReader, existingItem); - itemResult = existingItem; - } - else - { - itemResult = jObj.ToObject(serializer); - } - } - else - { - itemResult = itemToken.ToObject(serializer); - } - - if (itemResult == null) continue; - - var currentId = itemResult.Id; - var isIdentifiable = !IdComparer.Equals(currentId, default); - - if (isIdentifiable) - { - if (processedIds.Add(GetSemanticId(currentId))) - finalItems.Add(itemResult); - } - else - { - finalItems.Add(itemResult); - } - } - - // KEEP logic - add items that weren't in the JSON - foreach (var kvp in existingItemsMap) - { - if (processedIds.Add(kvp.Key)) - finalItems.Add(kvp.Value); - } - - if (isFixedSize) - { - var resultArray = new TItem[finalItems.Count]; - finalItems.CopyTo(resultArray); - return resultArray; - } - - targetList.Clear(); - if (targetList is List typedList) - { - typedList.EnsureCapacity(finalItems.Count); - typedList.AddRange(finalItems); - } - else - { - for (var i = 0; i < finalItems.Count; i++) - targetList.Add(finalItems[i]); - } - - return targetList; - } - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - throw new InvalidOperationException("IdAwareCollectionMergeConverter is read-only."); - } - } - - /// - /// 🔑 OPTIMIZATION: Static class with inlined ID extraction methods - /// - public static class IdExtractor - { - private static readonly string IdPropertyName = nameof(IId.Id); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TId GetIdFromJToken(JObject obj) where TId : struct - { - var idPropToken = obj.GetValue(IdPropertyName, StringComparison.OrdinalIgnoreCase); - if (idPropToken == null || idPropToken.Type == JTokenType.Null) return default; - - // 🔑 OPTIMIZATION: Fast path for common types - JIT eliminates dead branches - if (typeof(TId) == typeof(int)) - { - if (idPropToken.Type == JTokenType.Integer) - return Unsafe.As(ref Unsafe.AsRef((int)idPropToken)); - return (TId)(object)idPropToken.Value(); - } - - if (typeof(TId) == typeof(long)) - { - if (idPropToken.Type == JTokenType.Integer) - return Unsafe.As(ref Unsafe.AsRef((long)idPropToken)); - return (TId)(object)idPropToken.Value(); - } - - if (typeof(TId) == typeof(Guid)) - { - var stringValue = idPropToken.Type == JTokenType.String - ? (string?)idPropToken - : idPropToken.Value(); - - if (string.IsNullOrEmpty(stringValue)) return default; - - if (Guid.TryParse(stringValue, out var guidValue)) - return Unsafe.As(ref guidValue); - return default; - } - - try { return idPropToken.Value(); } - catch { return default; } - } - } - - public class UnifiedMergeContractResolver : DefaultContractResolver - { - private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> CollectionConverterCache = new(); - private static readonly ConcurrentDictionary NoMergeAttributeCache = new(); - private static readonly ConcurrentDictionary PropertyConfigCache = new(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasNoMergeAttribute(MemberInfo member) - { - return NoMergeAttributeCache.GetOrAdd(member, static m => - m.GetCustomAttribute() != null); - } - - protected override JsonArrayContract CreateArrayContract(Type objectType) - { - var contract = base.CreateArrayContract(objectType); - if (TypeCache.IsPrimitiveElementCollection(objectType)) - { - contract.ItemIsReference = false; - contract.IsReference = false; - } - return contract; - } - - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - var property = base.CreateProperty(member, memberSerialization); - var t = property.PropertyType; - if (t == null) return property; - - var config = GetOrCreatePropertyConfig(member, t); - - if (config.IsPrimitiveElementCollection) - { - property.ItemIsReference = false; - property.IsReference = false; - return property; - } - - if (config.IsCollection && (!config.IsIdCollection || config.IsExcludedFromMerge)) - { - property.ObjectCreationHandling = ObjectCreationHandling.Replace; - return property; - } - - if (config.IsIdCollection && config.IdType != null && config.ElementType != null && !config.IsPrimitiveElement) - { - property.Converter = CollectionConverterCache.GetOrAdd((config.ElementType, config.IdType), static k => - (JsonConverter)Activator.CreateInstance( - typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!); - property.ObjectCreationHandling = ObjectCreationHandling.Reuse; - return property; - } - - return property; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static CachedPropertyConfig GetOrCreatePropertyConfig(MemberInfo member, Type propertyType) - { - return PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType)); - } - - private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType) - { - var config = new CachedPropertyConfig - { - IsPrimitiveElementCollection = TypeCache.IsPrimitiveElementCollection(propertyType), - IsExcludedFromMerge = HasNoMergeAttribute(member), - IsCollection = TypeCache.IsCollectionType(propertyType) - }; - - if (config.IsCollection) - { - config.ElementType = TypeCache.GetElementType(propertyType); - if (config.ElementType != null) - { - var (hasId, elemIdType) = TypeCache.GetIdInfo(config.ElementType); - if (hasId && elemIdType != null) - { - config.IsIdCollection = true; - config.IdType = elemIdType; - } - config.IsPrimitiveElement = TypeCache.IsPrimitive(config.ElementType); - } - } - - return config; - } - - private sealed class CachedPropertyConfig - { - public bool IsPrimitiveElementCollection { get; set; } - public bool IsExcludedFromMerge { get; set; } - public bool IsCollection { get; set; } - public bool IsIdCollection { get; set; } - public Type? ElementType { get; set; } - public Type? IdType { get; set; } - public bool IsPrimitiveElement { get; set; } - } - } - - public static class JsonPopulateExtensions - { - private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> RootConverterCache = new(); - private static readonly UnifiedMergeContractResolver SharedContractResolver = new(); + ShouldSkip = !prop.CanRead || + prop.GetIndexParameters().Length > 0 || + HasJsonIgnoreAttribute(prop); - // Cached serializer for merge operations - private static readonly JsonSerializer CachedMergeSerializer = CreateMergeSerializer(); - - private static JsonSerializer CreateMergeSerializer() + if (!ShouldSkip) { - var settings = new JsonSerializerSettings - { - ContractResolver = SharedContractResolver, - PreserveReferencesHandling = PreserveReferencesHandling.Objects, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - NullValueHandling = NullValueHandling.Ignore, - MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead, - }; - return JsonSerializer.Create(settings); - } - - public static void DeepPopulateWithMerge(this T target, string json, JsonSerializerSettings? settings = null) where T : notnull - { - ArgumentNullException.ThrowIfNull(target); - ArgumentNullException.ThrowIfNull(json); - - // Use centralized unwrap helper - json = JsonUtilities.UnwrapJsonString(json); + var (isId, idType) = GetIdInfo(PropertyType); + IsIId = isId; + IdType = idType; - // Create a local resolver indicating this serializer is used for a MERGE operation - var resolver = new HybridReferenceResolver(isForMerge: true, estimatedObjectCount: 32); - - JsonSerializer serializer; - if (settings == null) + if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != StringType) { - // Fast path: reuse cached serializer - serializer = CachedMergeSerializer; - } - else - { - settings.ContractResolver ??= SharedContractResolver; - var serializerSettings = new JsonSerializerSettings(settings) + CollectionElementType = GetCollectionElementType(PropertyType); + if (CollectionElementType != null) { - ReferenceResolverProvider = () => resolver - }; - serializer = JsonSerializer.Create(serializerSettings); - } - - // Temporarily set the reference resolver for cached serializer - var originalResolver = serializer.ReferenceResolver; - serializer.ReferenceResolver = resolver; - - try - { - if (target is IList targetList) - { - var type = target.GetType(); - var elemType = TypeCache.GetElementType(type); - - if (elemType != null) - { - var (isId, idType) = TypeCache.GetIdInfo(elemType); - if (isId && idType != null) - { - var converterInstance = RootConverterCache.GetOrAdd((elemType, idType), static k => - (JsonConverter)Activator.CreateInstance( - typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!); - - // Use JToken for collection merge (needed for complex logic) - var token = JToken.Parse(json); - using var reader = token.CreateReader(); - converterInstance.ReadJson(reader, target.GetType(), target, serializer); - return; - } - } + var (elemIsId, elemIdType) = GetIdInfo(CollectionElementType); + IsIIdCollection = elemIsId; + CollectionElementIdType = elemIdType; } - - // For non-collection targets, use direct JsonTextReader for better performance - using var stringReader = new StringReader(json); - using var jsonReader = new JsonTextReader(stringReader); - serializer.Populate(jsonReader, target); - } - finally - { - serializer.ReferenceResolver = originalResolver; } } } } + +/// +/// Static type metadata cache for semantic ID generation. +/// +public static class TypeCache +{ + private static readonly ConcurrentDictionary TypeNameCache = new(); + private static readonly ConcurrentDictionary CachedPropertyInfoCache = new(); + private static readonly ConcurrentDictionary TypeIdCache = new(); + private static int _typeIdCounter; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetTypeId(Type t) => TypeIdCache.GetOrAdd(t, _ => Interlocked.Increment(ref _typeIdCounter)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long CreateSemanticId(int typeId, long objectId) => ((long)typeId << 48) | (objectId & 0xFFFFFFFFFFFF); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetTypeName(Type t) => TypeNameCache.GetOrAdd(t, static type => type.Name); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CachedPropertyInfo[] GetCachedProperties(Type t) + { + return CachedPropertyInfoCache.GetOrAdd(t, static type => + { + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + var cached = new CachedPropertyInfo[props.Length]; + for (var i = 0; i < props.Length; i++) + cached[i] = new CachedPropertyInfo(props[i]); + return cached; + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IdToLong(object idValue) + { + return idValue switch + { + int i => i, + long l => l, + Guid g => GuidToLong(g), + short s => s, + byte b => b, + uint ui => ui, + ulong ul => (long)ul, + ushort us => us, + sbyte sb => sb, + _ => throw new NotSupportedException($"ID type '{idValue.GetType().Name}' is not supported for semantic ID generation.") + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IdToLong(TId id) where TId : struct + { + if (typeof(TId) == typeof(int)) return Unsafe.As(ref id); + if (typeof(TId) == typeof(long)) return Unsafe.As(ref id); + if (typeof(TId) == typeof(Guid)) return GuidToLong(Unsafe.As(ref id)); + if (typeof(TId) == typeof(short)) return Unsafe.As(ref id); + if (typeof(TId) == typeof(byte)) return Unsafe.As(ref id); + if (typeof(TId) == typeof(uint)) return Unsafe.As(ref id); + if (typeof(TId) == typeof(ulong)) return (long)Unsafe.As(ref id); + if (typeof(TId) == typeof(ushort)) return Unsafe.As(ref id); + if (typeof(TId) == typeof(sbyte)) return Unsafe.As(ref id); + return IdToLong((object)id); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long GuidToLong(Guid guid) + { + Span bytes = stackalloc byte[16]; + guid.TryWriteBytes(bytes); + return BitConverter.ToInt64(bytes) ^ BitConverter.ToInt64(bytes.Slice(8)); + } +} + +/// +/// Converter for IId collections that supports merging by ID. +/// +public class IdAwareCollectionMergeConverter : JsonConverter + where TItem : class, IId, new() where TId : struct +{ + private static readonly int CachedTypeId = TypeCache.GetTypeId(typeof(TItem)); + private static readonly EqualityComparer IdComparer = EqualityComparer.Default; + + public override bool CanRead => true; + public override bool CanWrite => false; + public override bool CanConvert(Type objectType) => + typeof(ICollection).IsAssignableFrom(objectType) || typeof(IEnumerable).IsAssignableFrom(objectType); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long GetSemanticId(TId id) => TypeCache.CreateSemanticId(CachedTypeId, TypeCache.IdToLong(id)); + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return existingValue; + + if (existingValue is not IList targetList) + { + var jsonArrayFallback = JArray.Load(reader); + return jsonArrayFallback.ToObject(objectType, serializer); + } + + var isFixedSize = targetList.IsFixedSize; + var jsonArray = JArray.Load(reader); + var jsonCount = jsonArray.Count; + var existingCount = targetList.Count; + + var existingItemsMap = new Dictionary(existingCount); + for (var index = 0; index < existingCount; index++) + { + if (targetList[index] is TItem item && !IdComparer.Equals(item.Id, default)) + existingItemsMap[GetSemanticId(item.Id)] = item; + } + + var finalItems = new List(jsonCount + existingCount); + var processedIds = new HashSet(jsonCount); + + for (var i = 0; i < jsonCount; i++) + { + var itemToken = jsonArray[i]; + TItem? itemResult = null; + + if (itemToken is JObject jObj) + { + var incomingId = IdExtractor.GetIdFromJToken(jObj); + var hasId = !IdComparer.Equals(incomingId, default); + var semanticId = hasId ? GetSemanticId(incomingId) : 0L; + + TItem? existingItem = hasId && existingItemsMap.TryGetValue(semanticId, out var found) ? found : null; + + if (existingItem != null) + { + using var subReader = jObj.CreateReader(); + serializer.Populate(subReader, existingItem); + itemResult = existingItem; + } + else + itemResult = jObj.ToObject(serializer); + } + else + itemResult = itemToken.ToObject(serializer); + + if (itemResult == null) continue; + + var currentId = itemResult.Id; + if (!IdComparer.Equals(currentId, default)) + { + if (processedIds.Add(GetSemanticId(currentId))) + finalItems.Add(itemResult); + } + else + finalItems.Add(itemResult); + } + + foreach (var kvp in existingItemsMap) + { + if (processedIds.Add(kvp.Key)) + finalItems.Add(kvp.Value); + } + + if (isFixedSize) + { + var resultArray = new TItem[finalItems.Count]; + finalItems.CopyTo(resultArray); + return resultArray; + } + + targetList.Clear(); + if (targetList is List typedList) + { + typedList.EnsureCapacity(finalItems.Count); + typedList.AddRange(finalItems); + } + else + { + for (var i = 0; i < finalItems.Count; i++) + targetList.Add(finalItems[i]); + } + + return targetList; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => + throw new InvalidOperationException("IdAwareCollectionMergeConverter is read-only."); +} + +/// +/// Static class with inlined ID extraction methods. +/// +public static class IdExtractor +{ + private static readonly string IdPropertyName = nameof(IId.Id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TId GetIdFromJToken(JObject obj) where TId : struct + { + var idPropToken = obj.GetValue(IdPropertyName, StringComparison.OrdinalIgnoreCase); + if (idPropToken == null || idPropToken.Type == JTokenType.Null) return default; + + if (typeof(TId) == typeof(int)) + { + if (idPropToken.Type == JTokenType.Integer) + return Unsafe.As(ref Unsafe.AsRef((int)idPropToken)); + return (TId)(object)idPropToken.Value(); + } + + if (typeof(TId) == typeof(long)) + { + if (idPropToken.Type == JTokenType.Integer) + return Unsafe.As(ref Unsafe.AsRef((long)idPropToken)); + return (TId)(object)idPropToken.Value(); + } + + if (typeof(TId) == typeof(Guid)) + { + var stringValue = idPropToken.Type == JTokenType.String + ? (string?)idPropToken + : idPropToken.Value(); + + if (string.IsNullOrEmpty(stringValue)) return default; + + if (Guid.TryParse(stringValue, out var guidValue)) + return Unsafe.As(ref guidValue); + return default; + } + + try { return idPropToken.Value(); } + catch { return default; } + } +} + +public class UnifiedMergeContractResolver : DefaultContractResolver +{ + private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> CollectionConverterCache = new(); + private static readonly ConcurrentDictionary NoMergeAttributeCache = new(); + private static readonly ConcurrentDictionary PropertyConfigCache = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HasNoMergeAttribute(MemberInfo member) => + NoMergeAttributeCache.GetOrAdd(member, static m => m.GetCustomAttribute() != null); + + protected override JsonArrayContract CreateArrayContract(Type objectType) + { + var contract = base.CreateArrayContract(objectType); + if (IsPrimitiveElementCollection(objectType)) + { + contract.ItemIsReference = false; + contract.IsReference = false; + } + return contract; + } + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + var t = property.PropertyType; + if (t == null) return property; + + var config = GetOrCreatePropertyConfig(member, t); + + if (config.IsPrimitiveElementCollection) + { + property.ItemIsReference = false; + property.IsReference = false; + return property; + } + + if (config.IsCollection && (!config.IsIdCollection || config.IsExcludedFromMerge)) + { + property.ObjectCreationHandling = ObjectCreationHandling.Replace; + return property; + } + + if (config.IsIdCollection && config.IdType != null && config.ElementType != null && !config.IsPrimitiveElement) + { + property.Converter = CollectionConverterCache.GetOrAdd((config.ElementType, config.IdType), static k => + (JsonConverter)Activator.CreateInstance( + typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!); + property.ObjectCreationHandling = ObjectCreationHandling.Reuse; + return property; + } + + return property; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static CachedPropertyConfig GetOrCreatePropertyConfig(MemberInfo member, Type propertyType) + => PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType)); + + private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType) + { + var config = new CachedPropertyConfig + { + IsPrimitiveElementCollection = IsPrimitiveElementCollection(propertyType), + IsExcludedFromMerge = HasNoMergeAttribute(member), + IsCollection = IsGenericCollectionType(propertyType) + }; + + if (config.IsCollection) + { + config.ElementType = GetCollectionElementType(propertyType); + if (config.ElementType != null) + { + var (hasId, elemIdType) = GetIdInfo(config.ElementType); + if (hasId && elemIdType != null) + { + config.IsIdCollection = true; + config.IdType = elemIdType; + } + config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType); + } + } + + return config; + } + + private sealed class CachedPropertyConfig + { + public bool IsPrimitiveElementCollection { get; set; } + public bool IsExcludedFromMerge { get; set; } + public bool IsCollection { get; set; } + public bool IsIdCollection { get; set; } + public Type? ElementType { get; set; } + public Type? IdType { get; set; } + public bool IsPrimitiveElement { get; set; } + } +} + +public static class JsonPopulateExtensions +{ + private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> RootConverterCache = new(); + private static readonly UnifiedMergeContractResolver SharedContractResolver = new(); + private static readonly JsonSerializer CachedMergeSerializer = CreateMergeSerializer(); + + private static JsonSerializer CreateMergeSerializer() + { + return JsonSerializer.Create(new JsonSerializerSettings + { + ContractResolver = SharedContractResolver, + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead, + }); + } + + public static void DeepPopulateWithMerge(this T target, string json, JsonSerializerSettings? settings = null) where T : notnull + { + ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(json); + + json = UnwrapJsonString(json); + var resolver = new HybridReferenceResolver(isForMerge: true, estimatedObjectCount: 32); + + JsonSerializer serializer; + if (settings == null) + serializer = CachedMergeSerializer; + else + { + settings.ContractResolver ??= SharedContractResolver; + serializer = JsonSerializer.Create(new JsonSerializerSettings(settings) { ReferenceResolverProvider = () => resolver }); + } + + var originalResolver = serializer.ReferenceResolver; + serializer.ReferenceResolver = resolver; + + try + { + if (target is IList targetList) + { + var type = target.GetType(); + var elemType = GetCollectionElementType(type); + + if (elemType != null) + { + var (isId, idType) = GetIdInfo(elemType); + if (isId && idType != null) + { + var converterInstance = RootConverterCache.GetOrAdd((elemType, idType), static k => + (JsonConverter)Activator.CreateInstance( + typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!); + + var token = JToken.Parse(json); + using var reader = token.CreateReader(); + converterInstance.ReadJson(reader, target.GetType(), target, serializer); + return; + } + } + } + + using var stringReader = new StringReader(json); + using var jsonReader = new JsonTextReader(stringReader); + serializer.Populate(jsonReader, target); + } + finally + { + serializer.ReferenceResolver = originalResolver; + } + } +} diff --git a/AyCode.Core/Extensions/SerializeObjectExtensions.cs b/AyCode.Core/Extensions/SerializeObjectExtensions.cs index b1481f2..a4aaaa8 100644 --- a/AyCode.Core/Extensions/SerializeObjectExtensions.cs +++ b/AyCode.Core/Extensions/SerializeObjectExtensions.cs @@ -1,14 +1,13 @@ -using AyCode.Core.Interfaces; -using MessagePack; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using System.Buffers; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; +using System.Collections.Concurrent; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; +using AyCode.Core.Interfaces; +using MessagePack; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using static AyCode.Core.Extensions.JsonUtilities; namespace AyCode.Core.Extensions; @@ -36,8 +35,7 @@ internal static class Base62 value /= 62; } - if (isNegative) - buffer[--index] = '-'; + if (isNegative) buffer[--index] = '-'; return new string(buffer[index..]); } @@ -53,7 +51,7 @@ public class HybridReferenceResolver : IReferenceResolver internal HashSet? _referencedIds; private int _nextNumericId = 1; - private static readonly ConcurrentDictionary> _idGetterCache = new(); + private static readonly ConcurrentDictionary> IdGetterCache = new(); public bool IsForMerge { get; } private readonly int _estimatedObjectCount; @@ -86,13 +84,12 @@ public class HybridReferenceResolver : IReferenceResolver var objectToId = GetObjectToId(); if (objectToId.TryGetValue(value, out var existingId)) { - if (!IsForMerge) - ReferencedIds.Add(existingId); + if (!IsForMerge) ReferencedIds.Add(existingId); return existingId; } var type = value.GetType(); - var (isId, idType) = TypeCache.GetIdInfo(type); + var (isId, idType) = GetIdInfo(type); string newRef; if (isId && idType != null) @@ -108,14 +105,10 @@ public class HybridReferenceResolver : IReferenceResolver newRef = Base62.Encode(semanticId); } else - { newRef = Base62.Encode(-_nextNumericId++); - } } else - { newRef = Base62.Encode(-_nextNumericId++); - } GetIdToObject()[newRef] = value; objectToId[value] = newRef; @@ -130,7 +123,7 @@ public class HybridReferenceResolver : IReferenceResolver [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Func GetOrCreateIdGetter(Type type) => - _idGetterCache.GetOrAdd(type, static t => + IdGetterCache.GetOrAdd(type, static t => { var prop = t.GetProperty("Id"); if (prop == null) return static _ => null; @@ -138,44 +131,18 @@ public class HybridReferenceResolver : IReferenceResolver if (getMethod == null) return static _ => null; return obj => getMethod.Invoke(obj, null); }); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsDefaultValue(object value, Type type) - { - if (type == typeof(int)) return (int)value == 0; - if (type == typeof(long)) return (long)value == 0L; - if (type == typeof(Guid)) return (Guid)value == Guid.Empty; - if (type == typeof(short)) return (short)value == 0; - if (type == typeof(byte)) return (byte)value == 0; - if (type == typeof(uint)) return (uint)value == 0; - if (type == typeof(ulong)) return (ulong)value == 0; - if (type == typeof(ushort)) return (ushort)value == 0; - if (type == typeof(sbyte)) return (sbyte)value == 0; - return false; - } -} - -internal sealed class ReferenceEqualityComparer : IEqualityComparer -{ - public static readonly ReferenceEqualityComparer Instance = new(); - public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); } internal static class JsonReferencePostProcessor { private const string IdMarker = "\"$id\""; - private const string RefMarker = "\"$ref\""; public static string RemoveUnreferencedIds(string json, HashSet? referencedIds) { - if (!json.Contains(IdMarker)) - return json; - - if (referencedIds == null || referencedIds.Count == 0) - return RemoveAllIdsSpan(json); - - return RemoveUnreferencedIdsSpan(json, referencedIds); + if (!json.Contains(IdMarker)) return json; + return referencedIds == null || referencedIds.Count == 0 + ? RemoveAllIdsSpan(json) + : RemoveUnreferencedIdsSpan(json, referencedIds); } private static string RemoveAllIdsSpan(string json) @@ -189,16 +156,14 @@ internal static class JsonReferencePostProcessor var idIndex = json.IndexOf(IdMarker, searchStart, StringComparison.Ordinal); if (idIndex < 0) break; - if (idIndex > lastCopyEnd) - sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd); + if (idIndex > lastCopyEnd) sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd); var endIndex = SkipIdEntry(json, idIndex); lastCopyEnd = endIndex; searchStart = endIndex; } - if (lastCopyEnd < json.Length) - sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd); + if (lastCopyEnd < json.Length) sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd); return sb.Length == json.Length ? json : sb.ToString(); } @@ -225,9 +190,7 @@ internal static class JsonReferencePostProcessor { valueStart++; valueEnd = valueStart; - while (valueEnd < json.Length && json[valueEnd] != '"') - valueEnd++; - + while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++; idValue = json.Substring(valueStart, valueEnd - valueStart); valueEnd++; } @@ -236,24 +199,17 @@ internal static class JsonReferencePostProcessor valueEnd++; if (idValue != null && referencedIds.Contains(idValue)) - { searchStart = valueEnd; - } else { - if (idIndex > lastCopyEnd) - sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd); - + if (idIndex > lastCopyEnd) sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd); lastCopyEnd = valueEnd; searchStart = valueEnd; } } - if (lastCopyEnd == 0) - return json; - - if (lastCopyEnd < json.Length) - sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd); + if (lastCopyEnd == 0) return json; + if (lastCopyEnd < json.Length) sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd); return sb.ToString(); } @@ -262,32 +218,29 @@ internal static class JsonReferencePostProcessor private static int SkipIdEntry(string json, int idIndex) { var pos = idIndex + IdMarker.Length; - while (pos < json.Length && (json[pos] == ' ' || json[pos] == ':')) - pos++; + while (pos < json.Length && (json[pos] == ' ' || json[pos] == ':')) pos++; if (pos < json.Length && json[pos] == '"') { pos++; - while (pos < json.Length && json[pos] != '"') - pos++; - if (pos < json.Length) - pos++; + while (pos < json.Length && json[pos] != '"') pos++; + if (pos < json.Length) pos++; } - while (pos < json.Length && (json[pos] == ' ' || json[pos] == ',')) - pos++; + while (pos < json.Length && (json[pos] == ' ' || json[pos] == ',')) pos++; return pos; } public static HashSet CollectReferencedIds(string json) { + const string refMarker = "\"$ref\""; var result = new HashSet(StringComparer.Ordinal); var searchStart = 0; while (searchStart < json.Length) { - var refIndex = json.IndexOf(RefMarker, searchStart, StringComparison.Ordinal); + var refIndex = json.IndexOf(refMarker, searchStart, StringComparison.Ordinal); if (refIndex < 0) break; - var valueStart = refIndex + RefMarker.Length; + var valueStart = refIndex + refMarker.Length; while (valueStart < json.Length && (json[valueStart] == ' ' || json[valueStart] == ':')) valueStart++; @@ -295,18 +248,12 @@ internal static class JsonReferencePostProcessor { valueStart++; var valueEnd = valueStart; - while (valueEnd < json.Length && json[valueEnd] != '"') - valueEnd++; - - if (valueEnd > valueStart) - result.Add(json.Substring(valueStart, valueEnd - valueStart)); - + while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++; + if (valueEnd > valueStart) result.Add(json.Substring(valueStart, valueEnd - valueStart)); searchStart = valueEnd + 1; } else - { searchStart = valueStart; - } } return result; @@ -332,20 +279,12 @@ internal sealed class PooledStringWriter : StringWriter protected override void Dispose(bool disposing) { - if (!_disposed) - { - _disposed = true; - StringBuilderPool.Return(_pooledBuilder); - } + if (!_disposed) { _disposed = true; StringBuilderPool.Return(_pooledBuilder); } base.Dispose(disposing); } } -internal interface ObjectPool where T : class -{ - T Get(); - void Return(T obj); -} +internal interface ObjectPool where T : class { T Get(); void Return(T obj); } internal sealed class DefaultObjectPool : ObjectPool where T : class { @@ -385,7 +324,6 @@ public static class SerializeObjectExtensions { private static readonly UnifiedMergeContractResolver SharedContractResolver = new(); private static readonly Dictionary EmptyContextDict = new(); - private static readonly JsonSerializer CachedSerializer = CreateCachedSerializer(); public static JsonSerializerSettings Options => new() { @@ -399,143 +337,113 @@ public static class SerializeObjectExtensions Formatting = Formatting.None, }; - private static JsonSerializer CreateCachedSerializer() => JsonSerializer.Create(new JsonSerializerSettings - { - ContractResolver = SharedContractResolver, - PreserveReferencesHandling = PreserveReferencesHandling.Objects, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - NullValueHandling = NullValueHandling.Ignore, - MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead, - Formatting = Formatting.None, - }); + /// + /// Serialize object to JSON string with default options. + /// + public static string ToJson(this T source) + => AcJsonSerializer.Serialize(source); /// - /// Serialize object to JSON string using high-performance AcJsonSerializer. - /// Uses optimized reference handling with $id/$ref for shared objects. - /// Skips default values (0, false, empty strings, empty collections) to reduce JSON size. + /// Serialize object to JSON string with specified options. /// - public static string ToJson(this T source, JsonSerializerSettings? options = null) - { - // If custom options are provided, use Newtonsoft for full compatibility - //if (options != null) - //{ - // return JsonConvert.SerializeObject(source, options); - //} - - // Use our high-performance custom serializer - return AcJsonSerializer.Serialize(source); - - // ======================================================================== - // OLD IMPLEMENTATION - Newtonsoft with HybridReferenceResolver - // Uncomment below and comment out the AcJsonSerializer.Serialize line above to rollback - // ======================================================================== - // var resolver = new HybridReferenceResolver(estimatedObjectCount: 256); - // var serializer = CachedSerializer; - // - // string json; - // using (var sw = PooledStringWriter.Rent()) - // { - // var originalResolver = serializer.ReferenceResolver; - // serializer.ReferenceResolver = resolver; - // try - // { - // serializer.Serialize(sw, source); - // json = sw.ToString(); - // } - // finally - // { - // serializer.ReferenceResolver = originalResolver; - // } - // } - // - // // Skip post-processing if no $id in output - // if (!json.Contains("\"$id\"")) - // return json; - // - // // If we tracked references, use them - // if (resolver._referencedIds?.Count > 0) - // return JsonReferencePostProcessor.RemoveUnreferencedIds(json, resolver._referencedIds); - // - // // No references and no $ref - remove all $id - // if (!json.Contains("\"$ref\"")) - // return JsonReferencePostProcessor.RemoveUnreferencedIds(json, null); - // - // // Fallback: scan JSON for $ref values - // var referenced = JsonReferencePostProcessor.CollectReferencedIds(json); - // return JsonReferencePostProcessor.RemoveUnreferencedIds(json, referenced); - } + public static string ToJson(this T source, AcJsonSerializerOptions options) + => AcJsonSerializer.Serialize(source, options); - public static string ToJson(this IQueryable source, JsonSerializerSettings? options = null) where T : class, IAcSerializableToJson - //=> options != null ? JsonConvert.SerializeObject(source, options) : AcJsonSerializer.Serialize(source); - => ((object)source).ToJson(options); + public static string ToJson(this IQueryable source) where T : class, IAcSerializableToJson + => AcJsonSerializer.Serialize(source); + + public static string ToJson(this IQueryable source, AcJsonSerializerOptions options) where T : class, IAcSerializableToJson + => AcJsonSerializer.Serialize(source, options); - public static string ToJson(this IEnumerable source, JsonSerializerSettings? options = null) where T : class, IAcSerializableToJson - //=> options != null ? JsonConvert.SerializeObject(source, options) : AcJsonSerializer.Serialize(source); - => ((object)source).ToJson(options); + public static string ToJson(this IEnumerable source) where T : class, IAcSerializableToJson + => AcJsonSerializer.Serialize(source); - public static T? JsonTo(this string json, JsonSerializerSettings? options = null) + public static string ToJson(this IEnumerable source, AcJsonSerializerOptions options) where T : class, IAcSerializableToJson + => AcJsonSerializer.Serialize(source, options); + + /// + /// Deserialize JSON to object with default options. + /// + public static T? JsonTo(this string json) { - json = JsonUtilities.UnwrapJsonString(json); + json = UnwrapJsonString(json); return AcJsonDeserializer.Deserialize(json); - - // Use our high-performance custom deserializer - // AcJsonDeserializer now supports primitives, enums, and complex types - //if (options == null) - //{ - // try - // { - // return AcJsonDeserializer.Deserialize(json); - // } - // catch - // { - // // Fallback to Newtonsoft if custom deserializer fails - // } - //} - - //return JsonConvert.DeserializeObject(json, options ?? Options); } - public static object? JsonTo(this string json, Type toType, JsonSerializerSettings? options = null) + /// + /// Deserialize JSON to object with specified options. + /// + public static T? JsonTo(this string json, AcJsonSerializerOptions options) { - json = JsonUtilities.UnwrapJsonString(json); + json = UnwrapJsonString(json); + return AcJsonDeserializer.Deserialize(json, options); + } + + /// + /// Deserialize JSON to specified type with default options. + /// + public static object? JsonTo(this string json, Type toType) + { + json = UnwrapJsonString(json); return AcJsonDeserializer.Deserialize(json, toType); - - //// Use our high-performance custom deserializer - //// AcJsonDeserializer now supports primitives, enums, and complex types - //if (options == null) - //{ - // try - // { - // return AcJsonDeserializer.Deserialize(json, toType); - // } - // catch - // { - // // Fallback to Newtonsoft if custom deserializer fails - // } - //} - - //return JsonConvert.DeserializeObject(json, toType, options ?? Options); } - public static void JsonTo(this string json, object target, JsonSerializerSettings? options = null) + /// + /// Deserialize JSON to specified type with specified options. + /// + public static object? JsonTo(this string json, Type toType, AcJsonSerializerOptions options) { - json = JsonUtilities.UnwrapJsonString(json); - - // Use runtime type instead of compile-time type for Populate + json = UnwrapJsonString(json); + return AcJsonDeserializer.Deserialize(json, toType, options); + } + + /// + /// Populate existing object from JSON with default options. + /// + public static void JsonTo(this string json, object target) + { + json = UnwrapJsonString(json); AcJsonDeserializer.Populate(json, target); } - [return: NotNullIfNotNull(nameof(src))] - public static TDestination? CloneTo(this object? src, JsonSerializerSettings? options = null) where TDestination : class + /// + /// Populate existing object from JSON with specified options. + /// + public static void JsonTo(this string json, object target, AcJsonSerializerOptions options) + { + json = UnwrapJsonString(json); + AcJsonDeserializer.Populate(json, target, options); + } + + /// + /// Clone object via JSON serialization with default options. + /// + public static TDestination? CloneTo(this object? src) where TDestination : class + => src?.ToJson().JsonTo(); + + /// + /// Clone object via JSON serialization with specified options. + /// + public static TDestination? CloneTo(this object? src, AcJsonSerializerOptions options) where TDestination : class => src?.ToJson(options).JsonTo(options); - public static void CopyTo(this object? src, object target, JsonSerializerSettings? options = null) + /// + /// Copy object properties to target via JSON with default options. + /// + public static void CopyTo(this object? src, object target) + => src?.ToJson().JsonTo(target); + + /// + /// Copy object properties to target via JSON with specified options. + /// + public static void CopyTo(this object? src, object target, AcJsonSerializerOptions options) => src?.ToJson(options).JsonTo(target, options); - //public static byte[] ToMessagePack(this object message) => MessagePackSerializer.Serialize(message); - public static byte[] ToMessagePack(this object message, MessagePackSerializerOptions options) => MessagePackSerializer.Serialize(message, options); - //public static T MessagePackTo(this byte[] message) => MessagePackSerializer.Deserialize(message); - public static T MessagePackTo(this byte[] message, MessagePackSerializerOptions options) => MessagePackSerializer.Deserialize(message, options); + public static byte[] ToMessagePack(this object message, MessagePackSerializerOptions options) + => MessagePackSerializer.Serialize(message, options); + + public static T MessagePackTo(this byte[] message, MessagePackSerializerOptions options) + => MessagePackSerializer.Deserialize(message, options); } public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContractResolver diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index 3a7abd5..ecef3de 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -110,13 +110,13 @@ namespace AyCode.Services.SignalRs /// /// Gets the current connection state. Override in tests. /// - protected virtual HubConnectionState GetConnectionState() + protected virtual HubConnectionState GetConnectionState() => HubConnection?.State ?? HubConnectionState.Disconnected; /// /// Checks if the connection is connected. Override in tests. /// - protected virtual bool IsConnected() + protected virtual bool IsConnected() => GetConnectionState() == HubConnectionState.Connected; /// @@ -162,13 +162,13 @@ namespace AyCode.Services.SignalRs /// /// Gets the pending requests dictionary for testing. /// - protected ConcurrentDictionary GetPendingRequests() + protected ConcurrentDictionary GetPendingRequests() => _responseByRequestId; /// /// Clears all pending requests. /// - protected void ClearPendingRequests() + protected void ClearPendingRequests() => _responseByRequestId.Clear(); /// @@ -214,7 +214,7 @@ namespace AyCode.Services.SignalRs Logger.DebugConditional($"Client SendMessageToServerAsync sending; {nameof(requestId)}: {requestId}; ConnectionState: {GetConnectionState()}; {ConstHelper.NameByValue(TagsName, messageTag)}"); await StartConnection(); - + var msgp = message?.ToMessagePack(ContractlessStandardResolver.Options); if (!IsConnected()) @@ -273,13 +273,13 @@ namespace AyCode.Services.SignalRs private static ISignalRMessage CreatePostMessage(TPostData postData) { var type = typeof(TPostData); - + // Primitives, strings, enums, and value types should use IdMessage format if (IsPrimitiveOrStringOrEnum(type)) { return new SignalPostJsonDataMessage(new IdMessage(postData!)); } - + // Complex objects use direct serialization return new SignalPostJsonDataMessage(postData); } @@ -291,9 +291,9 @@ namespace AyCode.Services.SignalRs /// private static bool IsPrimitiveOrStringOrEnum(Type type) { - return type == typeof(string) || - type.IsEnum || - type.IsValueType || + return type == typeof(string) || + type.IsEnum || + type.IsValueType || type == typeof(DateTime); } @@ -340,8 +340,9 @@ namespace AyCode.Services.SignalRs if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, TransportSendTimeout, MsDelay, MsFirstDelay) && _responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage responseMessage) { + startTime = obj.RequestDateTime; SignalRRequestModelPool.Return(obj); - + if (responseMessage.Status == SignalResponseStatus.Error || responseMessage.ResponseData == null) { var errorText = $"Client SendMessageToServerAsync response error; await; tag: {messageTag}; Status: {responseMessage.Status}; ConnectionState: {GetConnectionState()}; requestId: {requestId}"; @@ -355,14 +356,16 @@ namespace AyCode.Services.SignalRs //return default; } - return responseMessage.ResponseData.JsonTo(); + var responseData = responseMessage.ResponseData.JsonTo(); + Logger.Info($"Client deserialized response json. Total: {(DateTime.UtcNow.Subtract(startTime)).TotalMilliseconds} ms! requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]"); + return responseData; } Logger.Error($"Client timeout after: {(DateTime.Now - startTime).TotalSeconds} sec! ConnectionState: {GetConnectionState()}; requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]"); } catch (Exception ex) { - Logger.Error($"SendMessageToServerAsync; requestId: {requestId}; ConnectionState: {GetConnectionState()}; {ex.Message}; {ConstHelper.NameByValue(TagsName, messageTag)}", ex); + Logger.Error($"Client SendMessageToServerAsync; requestId: {requestId}; ConnectionState: {GetConnectionState()}; {ex.Message}; {ConstHelper.NameByValue(TagsName, messageTag)}", ex); } if (_responseByRequestId.TryRemove(requestId, out var removedModel)) @@ -375,7 +378,7 @@ namespace AyCode.Services.SignalRs public virtual Task SendMessageToServerAsync(int messageTag, Func, Task> responseCallback) => SendMessageToServerAsync(messageTag, null, responseCallback); - public virtual Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, Func, Task> responseCallback) + public virtual Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, Func, Task> responseCallback) { if (messageTag == 0) Logger.Error($"SendMessageToServerAsync; messageTag == 0"); @@ -444,7 +447,7 @@ namespace AyCode.Services.SignalRs { SignalRRequestModelPool.Return(removedModel); } - + // Request-response hibás eset - ne hívjuk meg a MessageReceived-et return Task.CompletedTask; } diff --git a/AyCode.Services/SignalRs/IAcSignalRHubClient.cs b/AyCode.Services/SignalRs/IAcSignalRHubClient.cs index ac800d1..81ba062 100644 --- a/AyCode.Services/SignalRs/IAcSignalRHubClient.cs +++ b/AyCode.Services/SignalRs/IAcSignalRHubClient.cs @@ -215,17 +215,19 @@ public sealed class SignalResponseMessage : ISignalResponseMessag { if (!_isDeserialized) { - _responseData = ResponseDataJson != null - ? ResponseDataJson.JsonTo() - : default; _isDeserialized = true; + + _responseData = ResponseDataJson != null + ? ResponseDataJson.JsonTo() + : default; } + return _responseData; } set { - _responseData = value; _isDeserialized = true; + _responseData = value; ResponseDataJson = value?.ToJson(); } } diff --git a/AyCode.Services/SignalRs/SignalRRequestModel.cs b/AyCode.Services/SignalRs/SignalRRequestModel.cs index 47005b2..32a31ca 100644 --- a/AyCode.Services/SignalRs/SignalRRequestModel.cs +++ b/AyCode.Services/SignalRs/SignalRRequestModel.cs @@ -27,7 +27,7 @@ public class SignalRRequestModel : IResettable /// public bool TryReset() { - RequestDateTime = DateTime.UtcNow; + RequestDateTime = default; ResponseDateTime = default; ResponseByRequestId = null; return true; diff --git a/BenchmarkSuite1/SerializationBenchmarks.cs b/BenchmarkSuite1/SerializationBenchmarks.cs index ede6575..97a9006 100644 --- a/BenchmarkSuite1/SerializationBenchmarks.cs +++ b/BenchmarkSuite1/SerializationBenchmarks.cs @@ -25,7 +25,6 @@ public class SerializationBenchmarks // Settings private JsonSerializerSettings _newtonsoftNoRefSettings = null!; - private JsonSerializerSettings _ayCodeSettings = null!; [GlobalSetup] public void Setup() @@ -39,9 +38,6 @@ public class SerializationBenchmarks Formatting = Formatting.None }; - // AyCode WITH reference handling - _ayCodeSettings = SerializeObjectExtensions.Options; - // Create benchmark data using shared factory // ~1500 objects: 5 items × 4 pallets × 3 measurements × 5 points = 300 points + containers _testOrder = TestDataFactory.CreateBenchmarkOrder( @@ -52,7 +48,7 @@ public class SerializationBenchmarks // Pre-serialize for deserialization benchmarks _newtonsoftJson = JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings); - _ayCodeJson = _testOrder.ToJson(_ayCodeSettings); + _ayCodeJson = _testOrder.ToJson(); // Create target for populate benchmarks _populateTarget = new TestOrder(); @@ -77,7 +73,7 @@ public class SerializationBenchmarks [Benchmark(Description = "AyCode (with refs)")] [BenchmarkCategory("Serialize")] public string Serialize_AyCode_WithRefs() - => _testOrder.ToJson(_ayCodeSettings); + => _testOrder.ToJson(); [Benchmark(Description = "AcJsonSerializer (custom)")] [BenchmarkCategory("Serialize")] @@ -96,7 +92,7 @@ public class SerializationBenchmarks [Benchmark(Description = "AyCode (with refs)")] [BenchmarkCategory("Deserialize")] public TestOrder? Deserialize_AyCode_WithRefs() - => _ayCodeJson.JsonTo(_ayCodeSettings); + => _ayCodeJson.JsonTo(); [Benchmark(Description = "AcJsonDeserializer (custom)")] [BenchmarkCategory("Deserialize")]