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.
This commit is contained in:
parent
8e7869b3da
commit
ad426feba4
|
|
@ -17,8 +17,6 @@ public sealed class JsonExtensionTests
|
||||||
TestDataFactory.ResetIdCounter();
|
TestDataFactory.ResetIdCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonSerializerSettings GetMergeSettings() => SerializeObjectExtensions.Options;
|
|
||||||
|
|
||||||
#region Deep Hierarchy Tests (5 Levels)
|
#region Deep Hierarchy Tests (5 Levels)
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
|
@ -56,7 +54,7 @@ public sealed class JsonExtensionTests
|
||||||
}}";
|
}}";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
updateJson.JsonTo(order, GetMergeSettings());
|
updateJson.JsonTo(order);
|
||||||
|
|
||||||
// Assert: All references preserved
|
// Assert: All references preserved
|
||||||
Assert.AreSame(originalItem, order.Items[0], "Level 2: Item reference must be preserved");
|
Assert.AreSame(originalItem, order.Items[0], "Level 2: Item reference must be preserved");
|
||||||
|
|
@ -101,7 +99,7 @@ public sealed class JsonExtensionTests
|
||||||
}}";
|
}}";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
updateJson.JsonTo(order, GetMergeSettings());
|
updateJson.JsonTo(order);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.AreEqual(originalItemCount, order.Items.Count, "Items count should be preserved (KEEP logic)");
|
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 sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
var order = TestDataFactory.CreateOrder(itemCount: 2, sharedTag: sharedTag);
|
var order = TestDataFactory.CreateOrder(itemCount: 2, sharedTag: sharedTag);
|
||||||
|
|
||||||
var settings = GetMergeSettings();
|
|
||||||
settings.Formatting = Formatting.Indented;
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var json = order.ToJson(settings);
|
var json = order.ToJson();
|
||||||
Console.WriteLine($"Semantic Reference JSON:\n{json}");
|
Console.WriteLine($"Semantic Reference JSON:\n{json}");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -155,7 +150,7 @@ public sealed class JsonExtensionTests
|
||||||
}";
|
}";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
updateJson.JsonTo(order, GetMergeSettings());
|
updateJson.JsonTo(order);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.AreEqual("ORD-UPDATED", order.OrderNumber);
|
Assert.AreEqual("ORD-UPDATED", order.OrderNumber);
|
||||||
|
|
@ -174,11 +169,8 @@ public sealed class JsonExtensionTests
|
||||||
var sharedMeta = TestDataFactory.CreateMetadata(withChild: true);
|
var sharedMeta = TestDataFactory.CreateMetadata(withChild: true);
|
||||||
var order = TestDataFactory.CreateOrder(itemCount: 2, sharedMetadata: sharedMeta);
|
var order = TestDataFactory.CreateOrder(itemCount: 2, sharedMetadata: sharedMeta);
|
||||||
|
|
||||||
var settings = GetMergeSettings();
|
|
||||||
settings.Formatting = Formatting.Indented;
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var json = order.ToJson(settings);
|
var json = order.ToJson();
|
||||||
Console.WriteLine($"Newtonsoft Reference JSON:\n{json}");
|
Console.WriteLine($"Newtonsoft Reference JSON:\n{json}");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -210,11 +202,8 @@ public sealed class JsonExtensionTests
|
||||||
AuditMetadata = rootMeta
|
AuditMetadata = rootMeta
|
||||||
};
|
};
|
||||||
|
|
||||||
var settings = GetMergeSettings();
|
|
||||||
settings.Formatting = Formatting.Indented;
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var json = order.ToJson(settings);
|
var json = order.ToJson();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsTrue(json.Contains("Root"));
|
Assert.IsTrue(json.Contains("Root"));
|
||||||
|
|
@ -247,8 +236,7 @@ public sealed class JsonExtensionTests
|
||||||
Items = [new TestOrderItem { Id = 10, ProductName = "A", Tag = sharedTag, ItemMetadata = sharedMeta }]
|
Items = [new TestOrderItem { Id = 10, ProductName = "A", Tag = sharedTag, ItemMetadata = sharedMeta }]
|
||||||
};
|
};
|
||||||
|
|
||||||
var settings = GetMergeSettings();
|
var json = order.ToJson();
|
||||||
var json = order.ToJson(settings);
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var refCount = json.Split("\"$ref\"").Length - 1;
|
var refCount = json.Split("\"$ref\"").Length - 1;
|
||||||
|
|
@ -279,7 +267,7 @@ public sealed class JsonExtensionTests
|
||||||
}}";
|
}}";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
order.DeepPopulateWithMerge(updateJson, GetMergeSettings());
|
order.DeepPopulateWithMerge(updateJson);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.AreNotSame(originalRef, order.NoMergeItems);
|
Assert.AreNotSame(originalRef, order.NoMergeItems);
|
||||||
|
|
@ -314,7 +302,7 @@ public sealed class JsonExtensionTests
|
||||||
}";
|
}";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
order.DeepPopulateWithMerge(updateJson, GetMergeSettings());
|
order.DeepPopulateWithMerge(updateJson);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.AreEqual(2, order.MetadataList.Count);
|
Assert.AreEqual(2, order.MetadataList.Count);
|
||||||
|
|
@ -350,9 +338,9 @@ public sealed class JsonExtensionTests
|
||||||
new { Id = appleId, Name = "Apple", Qty = 7 },
|
new { Id = appleId, Name = "Apple", Qty = 7 },
|
||||||
new { Id = Guid.NewGuid(), Name = "Banana", Qty = 4 }
|
new { Id = Guid.NewGuid(), Name = "Banana", Qty = 4 }
|
||||||
}
|
}
|
||||||
}.ToJson(GetMergeSettings());
|
}.ToJson();
|
||||||
|
|
||||||
json.JsonTo(order, GetMergeSettings());
|
json.JsonTo(order);
|
||||||
|
|
||||||
// List reference preserved
|
// List reference preserved
|
||||||
Assert.AreSame(originalItemsRef, order.Items, "List reference must be 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);
|
var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 2, sharedTag: sharedTag, sharedMetadata: sharedMeta);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var json = order.ToJson(GetMergeSettings());
|
var json = order.ToJson();
|
||||||
var deserialized = json.JsonTo<TestOrder>(GetMergeSettings());
|
var deserialized = json.JsonTo<TestOrder>();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(deserialized);
|
Assert.IsNotNull(deserialized);
|
||||||
|
|
@ -394,10 +382,9 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void PrimitiveArray_BooleanTrue_RoundTrips()
|
public void PrimitiveArray_BooleanTrue_RoundTrips()
|
||||||
{
|
{
|
||||||
var settings = GetMergeSettings();
|
var jsonString = (new[] { true }).ToJson();
|
||||||
var jsonString = (new[] { true }).ToJson(settings);
|
|
||||||
|
|
||||||
var result = jsonString.JsonTo(typeof(bool[]), settings) as bool[];
|
var result = jsonString.JsonTo(typeof(bool[])) as bool[];
|
||||||
|
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
Assert.IsTrue(result[0], "Boolean true should deserialize as true!");
|
Assert.IsTrue(result[0], "Boolean true should deserialize as true!");
|
||||||
|
|
@ -406,7 +393,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void PrimitiveArray_AllTypes_RoundTrip()
|
public void PrimitiveArray_AllTypes_RoundTrip()
|
||||||
{
|
{
|
||||||
var settings = GetMergeSettings();
|
|
||||||
var testCases = new (Type type, object value)[]
|
var testCases = new (Type type, object value)[]
|
||||||
{
|
{
|
||||||
(typeof(bool), true),
|
(typeof(bool), true),
|
||||||
|
|
@ -424,9 +410,9 @@ public sealed class JsonExtensionTests
|
||||||
{
|
{
|
||||||
var wrapped = Array.CreateInstance(type, 1);
|
var wrapped = Array.CreateInstance(type, 1);
|
||||||
wrapped.SetValue(value, 0);
|
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.IsNotNull(result, $"Failed for {type.Name}");
|
||||||
Assert.AreEqual(value, result.GetValue(0), $"Value mismatch for {type.Name}");
|
Assert.AreEqual(value, result.GetValue(0), $"Value mismatch for {type.Name}");
|
||||||
|
|
@ -436,15 +422,14 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void IdMessage_MultipleParameters_SimulateSignalR()
|
public void IdMessage_MultipleParameters_SimulateSignalR()
|
||||||
{
|
{
|
||||||
var settings = GetMergeSettings();
|
|
||||||
var @params = new (Type t, object v)[] { (typeof(bool), true), (typeof(string), "filter"), (typeof(int), 100) };
|
var @params = new (Type t, object v)[] { (typeof(bool), true), (typeof(string), "filter"), (typeof(int), 100) };
|
||||||
|
|
||||||
foreach (var (type, value) in @params)
|
foreach (var (type, value) in @params)
|
||||||
{
|
{
|
||||||
var wrapped = Array.CreateInstance(type, 1);
|
var wrapped = Array.CreateInstance(type, 1);
|
||||||
wrapped.SetValue(value, 0);
|
wrapped.SetValue(value, 0);
|
||||||
var json = wrapped.ToJson(settings);
|
var json = wrapped.ToJson();
|
||||||
var arr = json.JsonTo(type.MakeArrayType(), settings) as Array;
|
var arr = json.JsonTo(type.MakeArrayType()) as Array;
|
||||||
Assert.AreEqual(value, arr?.GetValue(0));
|
Assert.AreEqual(value, arr?.GetValue(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -574,7 +559,7 @@ public sealed class JsonExtensionTests
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act - Serialize with AyCode
|
// Act - Serialize with AyCode
|
||||||
var json = order.ToJson(GetMergeSettings());
|
var json = order.ToJson();
|
||||||
|
|
||||||
// Deserialize with native Newtonsoft
|
// Deserialize with native Newtonsoft
|
||||||
var nativeSettings = new JsonSerializerSettings
|
var nativeSettings = new JsonSerializerSettings
|
||||||
|
|
@ -600,8 +585,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Populate_RefNode_ShouldSetPropertyToReferencedObject()
|
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 = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -612,15 +595,13 @@ public sealed class JsonExtensionTests
|
||||||
var order = new TestOrder { Id = 1, OrderNumber = "OLD" };
|
var order = new TestOrder { Id = 1, OrderNumber = "OLD" };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
json.JsonTo(order, GetMergeSettings());
|
json.JsonTo(order);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
|
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
|
||||||
Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from $ref");
|
Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from $ref");
|
||||||
Assert.AreEqual(100, order.PrimaryTag.Id);
|
Assert.AreEqual(100, order.PrimaryTag.Id);
|
||||||
Assert.AreEqual("SharedTag", order.PrimaryTag.Name);
|
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,
|
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
|
||||||
"SecondaryTag should reference the same object as PrimaryTag via $ref");
|
"SecondaryTag should reference the same object as PrimaryTag via $ref");
|
||||||
}
|
}
|
||||||
|
|
@ -628,7 +609,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Populate_RefNodeInCollection_ShouldSetPropertyToReferencedObject()
|
public void Populate_RefNodeInCollection_ShouldSetPropertyToReferencedObject()
|
||||||
{
|
{
|
||||||
// Arrange: Create JSON with shared reference in collection
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -642,17 +622,13 @@ public sealed class JsonExtensionTests
|
||||||
var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List<SharedTag>() };
|
var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List<SharedTag>() };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
json.JsonTo(order, GetMergeSettings());
|
json.JsonTo(order);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
|
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
|
||||||
Assert.AreEqual(2, order.Tags.Count, "Tags should have 2 items");
|
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],
|
Assert.AreSame(order.PrimaryTag, order.Tags[0],
|
||||||
"Tags[0] should reference the same object as PrimaryTag via $ref");
|
"Tags[0] should reference the same object as PrimaryTag via $ref");
|
||||||
|
|
||||||
// Second tag should be different
|
|
||||||
Assert.AreEqual(200, order.Tags[1].Id);
|
Assert.AreEqual(200, order.Tags[1].Id);
|
||||||
Assert.AreNotSame(order.PrimaryTag, order.Tags[1]);
|
Assert.AreNotSame(order.PrimaryTag, order.Tags[1]);
|
||||||
}
|
}
|
||||||
|
|
@ -660,7 +636,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Populate_NestedRefNode_ShouldResolveCorrectly()
|
public void Populate_NestedRefNode_ShouldResolveCorrectly()
|
||||||
{
|
{
|
||||||
// Arrange: Create JSON with nested $ref
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -680,13 +655,11 @@ public sealed class JsonExtensionTests
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
json.JsonTo(order, GetMergeSettings());
|
json.JsonTo(order);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(order.Items[0].Tag, "Item's Tag should be set");
|
Assert.IsNotNull(order.Items[0].Tag, "Item's Tag should be set");
|
||||||
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set from $ref");
|
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,
|
Assert.AreSame(order.Items[0].Tag, order.PrimaryTag,
|
||||||
"PrimaryTag should reference the same object as Items[0].Tag via $ref");
|
"PrimaryTag should reference the same object as Items[0].Tag via $ref");
|
||||||
}
|
}
|
||||||
|
|
@ -694,7 +667,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Populate_ForwardRef_ShouldResolveDeferredReference()
|
public void Populate_ForwardRef_ShouldResolveDeferredReference()
|
||||||
{
|
{
|
||||||
// Arrange: $ref appears BEFORE $id (forward reference)
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -705,9 +677,9 @@ public sealed class JsonExtensionTests
|
||||||
var order = new TestOrder { Id = 1, OrderNumber = "OLD" };
|
var order = new TestOrder { Id = 1, OrderNumber = "OLD" };
|
||||||
|
|
||||||
// Act
|
// 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.PrimaryTag, "PrimaryTag should be set");
|
||||||
Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from forward $ref");
|
Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from forward $ref");
|
||||||
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
|
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
|
||||||
|
|
@ -717,7 +689,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Populate_MultipleRefsToSameId_AllShouldResolveToSameObject()
|
public void Populate_MultipleRefsToSameId_AllShouldResolveToSameObject()
|
||||||
{
|
{
|
||||||
// Arrange: Create JSON with multiple $refs pointing to the same $id
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -733,9 +704,9 @@ public sealed class JsonExtensionTests
|
||||||
var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List<SharedTag>() };
|
var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List<SharedTag>() };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
json.JsonTo(order, GetMergeSettings());
|
json.JsonTo(order);
|
||||||
|
|
||||||
// Assert - all refs should point to the same object
|
// Assert
|
||||||
Assert.IsNotNull(order.PrimaryTag);
|
Assert.IsNotNull(order.PrimaryTag);
|
||||||
Assert.AreSame(order.PrimaryTag, order.SecondaryTag);
|
Assert.AreSame(order.PrimaryTag, order.SecondaryTag);
|
||||||
Assert.AreEqual(3, order.Tags.Count);
|
Assert.AreEqual(3, order.Tags.Count);
|
||||||
|
|
@ -747,7 +718,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Populate_DeeplyNestedRef_ShouldResolveAcrossLevels()
|
public void Populate_DeeplyNestedRef_ShouldResolveAcrossLevels()
|
||||||
{
|
{
|
||||||
// Arrange: Create JSON with $id at deep level (Item.Tag), $ref at root level
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -766,7 +736,7 @@ public sealed class JsonExtensionTests
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
json.JsonTo(order, GetMergeSettings());
|
json.JsonTo(order);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var deepTag = order.Items[0].Tag;
|
var deepTag = order.Items[0].Tag;
|
||||||
|
|
@ -779,7 +749,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Populate_RefInNestedObject_ShouldResolveFromParentContext()
|
public void Populate_RefInNestedObject_ShouldResolveFromParentContext()
|
||||||
{
|
{
|
||||||
// Arrange: $id at root, $ref in nested child
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -798,7 +767,7 @@ public sealed class JsonExtensionTests
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
json.JsonTo(order, GetMergeSettings());
|
json.JsonTo(order);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(order.PrimaryTag);
|
Assert.IsNotNull(order.PrimaryTag);
|
||||||
|
|
@ -810,7 +779,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Deserialize_RefOnly_ShouldCreateDeferredAndResolve()
|
public void Deserialize_RefOnly_ShouldCreateDeferredAndResolve()
|
||||||
{
|
{
|
||||||
// Arrange: JSON where only $ref exists (forward reference scenario in deserialize)
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -831,7 +799,6 @@ public sealed class JsonExtensionTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Deserialize_MultipleIdRefs_ComplexGraph()
|
public void Deserialize_MultipleIdRefs_ComplexGraph()
|
||||||
{
|
{
|
||||||
// Arrange: Complex object graph with multiple $id/$ref pairs
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""OrderNumber"": ""ORD-001"",
|
""OrderNumber"": ""ORD-001"",
|
||||||
|
|
@ -855,23 +822,16 @@ public sealed class JsonExtensionTests
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(order);
|
Assert.IsNotNull(order);
|
||||||
Assert.AreEqual(3, order.Tags.Count);
|
Assert.AreEqual(3, order.Tags.Count);
|
||||||
|
|
||||||
// Verify tag1 references
|
|
||||||
Assert.AreSame(order.PrimaryTag, order.Tags[0]);
|
Assert.AreSame(order.PrimaryTag, order.Tags[0]);
|
||||||
Assert.AreSame(order.PrimaryTag, order.Tags[2]);
|
Assert.AreSame(order.PrimaryTag, order.Tags[2]);
|
||||||
|
|
||||||
// Verify tag2 references
|
|
||||||
Assert.AreSame(order.SecondaryTag, order.Tags[1]);
|
Assert.AreSame(order.SecondaryTag, order.Tags[1]);
|
||||||
Assert.AreSame(order.SecondaryTag, order.Items[0].Tag);
|
Assert.AreSame(order.SecondaryTag, order.Items[0].Tag);
|
||||||
|
|
||||||
// Verify they are different
|
|
||||||
Assert.AreNotSame(order.PrimaryTag, order.SecondaryTag);
|
Assert.AreNotSame(order.PrimaryTag, order.SecondaryTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Populate_RefWithExistingTarget_ShouldOverwriteWithReference()
|
public void Populate_RefWithExistingTarget_ShouldOverwriteWithReference()
|
||||||
{
|
{
|
||||||
// Arrange: Target has existing value, should be overwritten by $ref
|
|
||||||
var json = @"{
|
var json = @"{
|
||||||
""Id"": 1,
|
""Id"": 1,
|
||||||
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""NewTag"" },
|
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""NewTag"" },
|
||||||
|
|
@ -882,13 +842,13 @@ public sealed class JsonExtensionTests
|
||||||
var order = new TestOrder
|
var order = new TestOrder
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
SecondaryTag = existingTag // Pre-existing value
|
SecondaryTag = existingTag
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
json.JsonTo(order, GetMergeSettings());
|
json.JsonTo(order);
|
||||||
|
|
||||||
// Assert - SecondaryTag should be overwritten with the $ref reference
|
// Assert
|
||||||
Assert.IsNotNull(order.PrimaryTag);
|
Assert.IsNotNull(order.PrimaryTag);
|
||||||
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
|
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
|
||||||
"SecondaryTag should be overwritten with $ref reference");
|
"SecondaryTag should be overwritten with $ref reference");
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using AyCode.Core.Interfaces;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions;
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
||||||
internal static class JsonUtilities
|
/// <summary>
|
||||||
|
/// Options for AcJsonSerializer and AcJsonDeserializer.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AcJsonSerializerOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default options instance with reference handling enabled and max depth.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly AcJsonSerializerOptions Default = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options for shallow serialization (root level only, no references).
|
||||||
|
/// </summary>
|
||||||
|
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to use $id/$ref reference handling for circular references.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool UseReferenceHandling { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
public byte MaxDepth { get; init; } = byte.MaxValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates options with specified max depth.
|
||||||
|
/// </summary>
|
||||||
|
public static AcJsonSerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates options without reference handling.
|
||||||
|
/// </summary>
|
||||||
|
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Central utilities for JSON serialization/deserialization.
|
||||||
|
/// Contains shared type caches, primitive type checks, and string utilities.
|
||||||
|
/// </summary>
|
||||||
|
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<Type> PrimitiveTypes = new HashSet<Type>
|
||||||
|
{
|
||||||
|
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<Type, (bool IsId, Type? IdType)> IdInfoCache = new();
|
||||||
|
private static readonly ConcurrentDictionary<Type, Type?> CollectionElementCache = new();
|
||||||
|
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new();
|
||||||
|
private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new();
|
||||||
|
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCollectionCache = new();
|
||||||
|
private static readonly ConcurrentDictionary<PropertyInfo, bool> JsonIgnoreCache = new();
|
||||||
|
private static readonly ConcurrentDictionary<Type, Func<IList>> ListFactoryCache = new();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region String Utilities
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unwraps a JSON string that may be double-quoted (e.g., "\"...\"" -> ...).
|
/// Unwraps a JSON string that may be double-quoted (e.g., "\"...\"" -> ...).
|
||||||
/// Optimized to avoid Regex.Unescape allocation when no escape sequences exist.
|
/// 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] != '"')
|
if (span.Length < 2 || span[0] != '"' || span[^1] != '"')
|
||||||
return json;
|
return json;
|
||||||
|
|
||||||
// Extract inner content (without outer quotes)
|
|
||||||
var inner = span[1..^1];
|
var inner = span[1..^1];
|
||||||
|
|
||||||
// Fast path: check if any escape sequences exist
|
|
||||||
if (!inner.Contains('\\'))
|
if (!inner.Contains('\\'))
|
||||||
{
|
|
||||||
// No escapes - just return substring (single allocation)
|
|
||||||
return json.Substring(1, json.Length - 2);
|
return json.Substring(1, json.Length - 2);
|
||||||
}
|
|
||||||
|
|
||||||
// Slow path: unescape the string
|
|
||||||
return UnescapeJsonString(inner);
|
return UnescapeJsonString(inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manual JSON string unescaping - avoids Regex.Unescape overhead.
|
|
||||||
/// </summary>
|
|
||||||
private static string UnescapeJsonString(ReadOnlySpan<char> input)
|
private static string UnescapeJsonString(ReadOnlySpan<char> input)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(input.Length);
|
var sb = new StringBuilder(input.Length);
|
||||||
|
|
@ -51,54 +158,24 @@ internal static class JsonUtilities
|
||||||
var next = input[i + 1];
|
var next = input[i + 1];
|
||||||
switch (next)
|
switch (next)
|
||||||
{
|
{
|
||||||
case '"':
|
case '"': sb.Append('"'); i++; break;
|
||||||
sb.Append('"');
|
case '\\': sb.Append('\\'); i++; break;
|
||||||
i++;
|
case '/': sb.Append('/'); i++; break;
|
||||||
break;
|
case 'b': sb.Append('\b'); i++; break;
|
||||||
case '\\':
|
case 'f': sb.Append('\f'); i++; break;
|
||||||
sb.Append('\\');
|
case 'n': sb.Append('\n'); i++; break;
|
||||||
i++;
|
case 'r': sb.Append('\r'); i++; break;
|
||||||
break;
|
case 't': sb.Append('\t'); 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:
|
case 'u' when i + 5 < input.Length:
|
||||||
// Unicode escape: \uXXXX
|
|
||||||
var hex = input.Slice(i + 2, 4);
|
var hex = input.Slice(i + 2, 4);
|
||||||
if (TryParseHex(hex, out var unicode))
|
if (TryParseHex(hex, out var unicode))
|
||||||
{
|
{
|
||||||
sb.Append((char)unicode);
|
sb.Append((char)unicode);
|
||||||
i += 5;
|
i += 5;
|
||||||
}
|
}
|
||||||
else
|
else sb.Append(c);
|
||||||
{
|
|
||||||
sb.Append(c);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
sb.Append(c);
|
|
||||||
break;
|
break;
|
||||||
|
default: sb.Append(c); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,15 +189,275 @@ internal static class JsonUtilities
|
||||||
foreach (var c in hex)
|
foreach (var c in hex)
|
||||||
{
|
{
|
||||||
value <<= 4;
|
value <<= 4;
|
||||||
if (c >= '0' && c <= '9')
|
if (c >= '0' && c <= '9') value |= c - '0';
|
||||||
value |= c - '0';
|
else if (c >= 'a' && c <= 'f') value |= c - 'a' + 10;
|
||||||
else if (c >= 'a' && c <= 'f')
|
else if (c >= 'A' && c <= 'F') value |= c - 'A' + 10;
|
||||||
value |= c - 'a' + 10;
|
else return false;
|
||||||
else if (c >= 'A' && c <= 'F')
|
|
||||||
value |= c - 'A' + 10;
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a string needs JSON escaping.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool NeedsEscaping(string value)
|
||||||
|
{
|
||||||
|
foreach (var c in value)
|
||||||
|
{
|
||||||
|
if (c < 32 || c == '"' || c == '\\')
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Escapes a string for JSON output.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fast primitive check using type code.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Faster primitive check using TypeCode for hot paths.
|
||||||
|
/// </summary>
|
||||||
|
[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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if type is a dictionary type.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the element type of a collection.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets IId info for a type.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if property has JsonIgnore attribute.
|
||||||
|
/// </summary>
|
||||||
|
[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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if collection contains primitive elements.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or creates a list factory for a given element type.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static Func<IList> 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<Func<IList>>(castExpr).Compile();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if value is the default value for its type.
|
||||||
|
/// </summary>
|
||||||
|
[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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Frozen;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
using static AyCode.Core.Extensions.JsonUtilities;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||||
public sealed class JsonNoMergeCollectionAttribute : Attribute { }
|
public sealed class JsonNoMergeCollectionAttribute : Attribute { }
|
||||||
|
|
||||||
|
|
@ -27,32 +27,29 @@ namespace AyCode.Core.Extensions
|
||||||
public Type? CollectionElementType { get; }
|
public Type? CollectionElementType { get; }
|
||||||
public Type? CollectionElementIdType { get; }
|
public Type? CollectionElementIdType { get; }
|
||||||
public bool ShouldSkip { get; }
|
public bool ShouldSkip { get; }
|
||||||
public bool CanRead { get; }
|
|
||||||
public bool HasIndexParameters { get; }
|
|
||||||
|
|
||||||
public CachedPropertyInfo(PropertyInfo prop)
|
public CachedPropertyInfo(PropertyInfo prop)
|
||||||
{
|
{
|
||||||
Property = prop;
|
Property = prop;
|
||||||
Name = prop.Name;
|
Name = prop.Name;
|
||||||
PropertyType = prop.PropertyType;
|
PropertyType = prop.PropertyType;
|
||||||
CanRead = prop.CanRead;
|
|
||||||
HasIndexParameters = prop.GetIndexParameters().Length > 0;
|
|
||||||
|
|
||||||
// Pre-compute skip condition
|
ShouldSkip = !prop.CanRead ||
|
||||||
ShouldSkip = !CanRead || HasIndexParameters || TypeCache.HasJsonIgnoreAttribute(prop);
|
prop.GetIndexParameters().Length > 0 ||
|
||||||
|
HasJsonIgnoreAttribute(prop);
|
||||||
|
|
||||||
if (!ShouldSkip)
|
if (!ShouldSkip)
|
||||||
{
|
{
|
||||||
var (isId, idType) = TypeCache.GetIdInfo(PropertyType);
|
var (isId, idType) = GetIdInfo(PropertyType);
|
||||||
IsIId = isId;
|
IsIId = isId;
|
||||||
IdType = idType;
|
IdType = idType;
|
||||||
|
|
||||||
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != typeof(string))
|
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != StringType)
|
||||||
{
|
{
|
||||||
CollectionElementType = TypeCache.GetElementType(PropertyType);
|
CollectionElementType = GetCollectionElementType(PropertyType);
|
||||||
if (CollectionElementType != null)
|
if (CollectionElementType != null)
|
||||||
{
|
{
|
||||||
var (elemIsId, elemIdType) = TypeCache.GetIdInfo(CollectionElementType);
|
var (elemIsId, elemIdType) = GetIdInfo(CollectionElementType);
|
||||||
IsIIdCollection = elemIsId;
|
IsIIdCollection = elemIsId;
|
||||||
CollectionElementIdType = elemIdType;
|
CollectionElementIdType = elemIdType;
|
||||||
}
|
}
|
||||||
|
|
@ -62,75 +59,37 @@ namespace AyCode.Core.Extensions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Static type metadata cache - thread-safe because shared across all serialization operations
|
/// Static type metadata cache for semantic ID generation.
|
||||||
/// 🔑 OPTIMIZATION: Uses FrozenDictionary for hot-path lookups after warmup
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class TypeCache
|
public static class TypeCache
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, (bool IsId, Type? IdType)> _idCache = new();
|
private static readonly ConcurrentDictionary<Type, string> TypeNameCache = new();
|
||||||
private static readonly ConcurrentDictionary<Type, Type?> _collectionElemCache = new();
|
private static readonly ConcurrentDictionary<Type, CachedPropertyInfo[]> CachedPropertyInfoCache = new();
|
||||||
private static readonly ConcurrentDictionary<Type, string> _typeNameCache = new();
|
private static readonly ConcurrentDictionary<Type, int> TypeIdCache = new();
|
||||||
private static readonly ConcurrentDictionary<Type, CachedPropertyInfo[]> _cachedPropertyInfoCache = new();
|
|
||||||
private static readonly ConcurrentDictionary<PropertyInfo, bool> _jsonIgnoreCache = new();
|
|
||||||
private static readonly ConcurrentDictionary<Type, bool> _isPrimitiveCache = new();
|
|
||||||
private static readonly ConcurrentDictionary<Type, bool> _isPrimitiveElementCollectionCache = new();
|
|
||||||
private static readonly ConcurrentDictionary<Type, bool> _isCollectionTypeCache = new();
|
|
||||||
|
|
||||||
// 🔑 Type ID cache for long-based semantic IDs
|
|
||||||
private static readonly ConcurrentDictionary<Type, int> _typeIdCache = new();
|
|
||||||
private static int _typeIdCounter;
|
private static int _typeIdCounter;
|
||||||
|
|
||||||
// 🔑 OPTIMIZATION: Pre-computed primitive types set
|
|
||||||
private static readonly FrozenSet<Type> PrimitiveTypes = new HashSet<Type>
|
|
||||||
{
|
|
||||||
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<>);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a unique integer ID for a type (thread-safe, consistent within app lifetime)
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static int GetTypeId(Type t)
|
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 _typeIdCache.GetOrAdd(t, _ => Interlocked.Increment(ref _typeIdCounter));
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a long-based semantic ID from type ID and object ID.
|
|
||||||
/// Format: [16 bits typeId][48 bits objectId]
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static long CreateSemanticId(int typeId, long objectId)
|
|
||||||
{
|
|
||||||
return ((long)typeId << 48) | (objectId & 0xFFFFFFFFFFFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts the type ID from a semantic ID
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static int ExtractTypeId(long semanticId) => (int)(semanticId >> 48);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts the object ID from a semantic ID
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static long ExtractObjectId(long semanticId) => semanticId & 0xFFFFFFFFFFFF;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static long IdToLong(object idValue)
|
public static long IdToLong(object idValue)
|
||||||
{
|
{
|
||||||
|
|
@ -145,16 +104,10 @@ namespace AyCode.Core.Extensions
|
||||||
ulong ul => (long)ul,
|
ulong ul => (long)ul,
|
||||||
ushort us => us,
|
ushort us => us,
|
||||||
sbyte sb => sb,
|
sbyte sb => sb,
|
||||||
_ => throw new NotSupportedException(
|
_ => throw new NotSupportedException($"ID type '{idValue.GetType().Name}' is not supported for semantic ID generation.")
|
||||||
$"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.")
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 🔑 OPTIMIZATION: Generic ID to long conversion without boxing for common types
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static long IdToLong<TId>(TId id) where TId : struct
|
public static long IdToLong<TId>(TId id) where TId : struct
|
||||||
{
|
{
|
||||||
|
|
@ -167,8 +120,6 @@ namespace AyCode.Core.Extensions
|
||||||
if (typeof(TId) == typeof(ulong)) return (long)Unsafe.As<TId, ulong>(ref id);
|
if (typeof(TId) == typeof(ulong)) return (long)Unsafe.As<TId, ulong>(ref id);
|
||||||
if (typeof(TId) == typeof(ushort)) return Unsafe.As<TId, ushort>(ref id);
|
if (typeof(TId) == typeof(ushort)) return Unsafe.As<TId, ushort>(ref id);
|
||||||
if (typeof(TId) == typeof(sbyte)) return Unsafe.As<TId, sbyte>(ref id);
|
if (typeof(TId) == typeof(sbyte)) return Unsafe.As<TId, sbyte>(ref id);
|
||||||
|
|
||||||
// Fallback with boxing for unknown types
|
|
||||||
return IdToLong((object)id);
|
return IdToLong((object)id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,124 +128,12 @@ namespace AyCode.Core.Extensions
|
||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[16];
|
Span<byte> bytes = stackalloc byte[16];
|
||||||
guid.TryWriteBytes(bytes);
|
guid.TryWriteBytes(bytes);
|
||||||
var high = BitConverter.ToInt64(bytes);
|
return BitConverter.ToInt64(bytes) ^ BitConverter.ToInt64(bytes.Slice(8));
|
||||||
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<JsonIgnoreAttribute>() != null ||
|
|
||||||
p.GetCustomAttribute<System.Text.Json.Serialization.JsonIgnoreAttribute>() != 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for IId collections that supports merging by ID (update existing, add new, keep unmentioned)
|
/// Converter for IId collections that supports merging by ID.
|
||||||
/// 🔑 OPTIMIZATION: Uses pre-computed type ID and EqualityComparer
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class IdAwareCollectionMergeConverter<TItem, TId> : JsonConverter
|
public class IdAwareCollectionMergeConverter<TItem, TId> : JsonConverter
|
||||||
where TItem : class, IId<TId>, new() where TId : struct
|
where TItem : class, IId<TId>, new() where TId : struct
|
||||||
|
|
@ -325,7 +164,6 @@ namespace AyCode.Core.Extensions
|
||||||
var jsonCount = jsonArray.Count;
|
var jsonCount = jsonArray.Count;
|
||||||
var existingCount = targetList.Count;
|
var existingCount = targetList.Count;
|
||||||
|
|
||||||
// 🔑 OPTIMIZATION: Pre-sized dictionary with exact capacity
|
|
||||||
var existingItemsMap = new Dictionary<long, TItem>(existingCount);
|
var existingItemsMap = new Dictionary<long, TItem>(existingCount);
|
||||||
for (var index = 0; index < existingCount; index++)
|
for (var index = 0; index < existingCount; index++)
|
||||||
{
|
{
|
||||||
|
|
@ -333,9 +171,7 @@ namespace AyCode.Core.Extensions
|
||||||
existingItemsMap[GetSemanticId(item.Id)] = item;
|
existingItemsMap[GetSemanticId(item.Id)] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔑 OPTIMIZATION: Pre-sized collections
|
var finalItems = new List<TItem>(jsonCount + existingCount);
|
||||||
var estimatedCapacity = jsonCount + existingCount;
|
|
||||||
var finalItems = new List<TItem>(estimatedCapacity);
|
|
||||||
var processedIds = new HashSet<long>(jsonCount);
|
var processedIds = new HashSet<long>(jsonCount);
|
||||||
|
|
||||||
for (var i = 0; i < jsonCount; i++)
|
for (var i = 0; i < jsonCount; i++)
|
||||||
|
|
@ -358,32 +194,23 @@ namespace AyCode.Core.Extensions
|
||||||
itemResult = existingItem;
|
itemResult = existingItem;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
itemResult = jObj.ToObject<TItem>(serializer);
|
itemResult = jObj.ToObject<TItem>(serializer);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
itemResult = itemToken.ToObject<TItem>(serializer);
|
itemResult = itemToken.ToObject<TItem>(serializer);
|
||||||
}
|
|
||||||
|
|
||||||
if (itemResult == null) continue;
|
if (itemResult == null) continue;
|
||||||
|
|
||||||
var currentId = itemResult.Id;
|
var currentId = itemResult.Id;
|
||||||
var isIdentifiable = !IdComparer.Equals(currentId, default);
|
if (!IdComparer.Equals(currentId, default))
|
||||||
|
|
||||||
if (isIdentifiable)
|
|
||||||
{
|
{
|
||||||
if (processedIds.Add(GetSemanticId(currentId)))
|
if (processedIds.Add(GetSemanticId(currentId)))
|
||||||
finalItems.Add(itemResult);
|
finalItems.Add(itemResult);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
finalItems.Add(itemResult);
|
finalItems.Add(itemResult);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// KEEP logic - add items that weren't in the JSON
|
|
||||||
foreach (var kvp in existingItemsMap)
|
foreach (var kvp in existingItemsMap)
|
||||||
{
|
{
|
||||||
if (processedIds.Add(kvp.Key))
|
if (processedIds.Add(kvp.Key))
|
||||||
|
|
@ -412,14 +239,12 @@ namespace AyCode.Core.Extensions
|
||||||
return targetList;
|
return targetList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) =>
|
||||||
{
|
|
||||||
throw new InvalidOperationException("IdAwareCollectionMergeConverter is read-only.");
|
throw new InvalidOperationException("IdAwareCollectionMergeConverter is read-only.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 🔑 OPTIMIZATION: Static class with inlined ID extraction methods
|
/// Static class with inlined ID extraction methods.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class IdExtractor
|
public static class IdExtractor
|
||||||
{
|
{
|
||||||
|
|
@ -431,7 +256,6 @@ namespace AyCode.Core.Extensions
|
||||||
var idPropToken = obj.GetValue(IdPropertyName, StringComparison.OrdinalIgnoreCase);
|
var idPropToken = obj.GetValue(IdPropertyName, StringComparison.OrdinalIgnoreCase);
|
||||||
if (idPropToken == null || idPropToken.Type == JTokenType.Null) return default;
|
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 (typeof(TId) == typeof(int))
|
||||||
{
|
{
|
||||||
if (idPropToken.Type == JTokenType.Integer)
|
if (idPropToken.Type == JTokenType.Integer)
|
||||||
|
|
@ -471,16 +295,13 @@ namespace AyCode.Core.Extensions
|
||||||
private static readonly ConcurrentDictionary<MemberInfo, CachedPropertyConfig> PropertyConfigCache = new();
|
private static readonly ConcurrentDictionary<MemberInfo, CachedPropertyConfig> PropertyConfigCache = new();
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool HasNoMergeAttribute(MemberInfo member)
|
private static bool HasNoMergeAttribute(MemberInfo member) =>
|
||||||
{
|
NoMergeAttributeCache.GetOrAdd(member, static m => m.GetCustomAttribute<JsonNoMergeCollectionAttribute>() != null);
|
||||||
return NoMergeAttributeCache.GetOrAdd(member, static m =>
|
|
||||||
m.GetCustomAttribute<JsonNoMergeCollectionAttribute>() != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override JsonArrayContract CreateArrayContract(Type objectType)
|
protected override JsonArrayContract CreateArrayContract(Type objectType)
|
||||||
{
|
{
|
||||||
var contract = base.CreateArrayContract(objectType);
|
var contract = base.CreateArrayContract(objectType);
|
||||||
if (TypeCache.IsPrimitiveElementCollection(objectType))
|
if (IsPrimitiveElementCollection(objectType))
|
||||||
{
|
{
|
||||||
contract.ItemIsReference = false;
|
contract.ItemIsReference = false;
|
||||||
contract.IsReference = false;
|
contract.IsReference = false;
|
||||||
|
|
@ -523,31 +344,29 @@ namespace AyCode.Core.Extensions
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static CachedPropertyConfig GetOrCreatePropertyConfig(MemberInfo member, Type propertyType)
|
private static CachedPropertyConfig GetOrCreatePropertyConfig(MemberInfo member, Type propertyType)
|
||||||
{
|
=> PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType));
|
||||||
return PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType)
|
private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType)
|
||||||
{
|
{
|
||||||
var config = new CachedPropertyConfig
|
var config = new CachedPropertyConfig
|
||||||
{
|
{
|
||||||
IsPrimitiveElementCollection = TypeCache.IsPrimitiveElementCollection(propertyType),
|
IsPrimitiveElementCollection = IsPrimitiveElementCollection(propertyType),
|
||||||
IsExcludedFromMerge = HasNoMergeAttribute(member),
|
IsExcludedFromMerge = HasNoMergeAttribute(member),
|
||||||
IsCollection = TypeCache.IsCollectionType(propertyType)
|
IsCollection = IsGenericCollectionType(propertyType)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (config.IsCollection)
|
if (config.IsCollection)
|
||||||
{
|
{
|
||||||
config.ElementType = TypeCache.GetElementType(propertyType);
|
config.ElementType = GetCollectionElementType(propertyType);
|
||||||
if (config.ElementType != null)
|
if (config.ElementType != null)
|
||||||
{
|
{
|
||||||
var (hasId, elemIdType) = TypeCache.GetIdInfo(config.ElementType);
|
var (hasId, elemIdType) = GetIdInfo(config.ElementType);
|
||||||
if (hasId && elemIdType != null)
|
if (hasId && elemIdType != null)
|
||||||
{
|
{
|
||||||
config.IsIdCollection = true;
|
config.IsIdCollection = true;
|
||||||
config.IdType = elemIdType;
|
config.IdType = elemIdType;
|
||||||
}
|
}
|
||||||
config.IsPrimitiveElement = TypeCache.IsPrimitive(config.ElementType);
|
config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -570,21 +389,18 @@ namespace AyCode.Core.Extensions
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> RootConverterCache = new();
|
private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> RootConverterCache = new();
|
||||||
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
|
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
|
||||||
|
|
||||||
// Cached serializer for merge operations
|
|
||||||
private static readonly JsonSerializer CachedMergeSerializer = CreateMergeSerializer();
|
private static readonly JsonSerializer CachedMergeSerializer = CreateMergeSerializer();
|
||||||
|
|
||||||
private static JsonSerializer CreateMergeSerializer()
|
private static JsonSerializer CreateMergeSerializer()
|
||||||
{
|
{
|
||||||
var settings = new JsonSerializerSettings
|
return JsonSerializer.Create(new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
ContractResolver = SharedContractResolver,
|
ContractResolver = SharedContractResolver,
|
||||||
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
||||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||||
NullValueHandling = NullValueHandling.Ignore,
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
|
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
|
||||||
};
|
});
|
||||||
return JsonSerializer.Create(settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeepPopulateWithMerge<T>(this T target, string json, JsonSerializerSettings? settings = null) where T : notnull
|
public static void DeepPopulateWithMerge<T>(this T target, string json, JsonSerializerSettings? settings = null) where T : notnull
|
||||||
|
|
@ -592,29 +408,18 @@ namespace AyCode.Core.Extensions
|
||||||
ArgumentNullException.ThrowIfNull(target);
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
ArgumentNullException.ThrowIfNull(json);
|
ArgumentNullException.ThrowIfNull(json);
|
||||||
|
|
||||||
// Use centralized unwrap helper
|
json = UnwrapJsonString(json);
|
||||||
json = JsonUtilities.UnwrapJsonString(json);
|
|
||||||
|
|
||||||
// Create a local resolver indicating this serializer is used for a MERGE operation
|
|
||||||
var resolver = new HybridReferenceResolver(isForMerge: true, estimatedObjectCount: 32);
|
var resolver = new HybridReferenceResolver(isForMerge: true, estimatedObjectCount: 32);
|
||||||
|
|
||||||
JsonSerializer serializer;
|
JsonSerializer serializer;
|
||||||
if (settings == null)
|
if (settings == null)
|
||||||
{
|
|
||||||
// Fast path: reuse cached serializer
|
|
||||||
serializer = CachedMergeSerializer;
|
serializer = CachedMergeSerializer;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
settings.ContractResolver ??= SharedContractResolver;
|
settings.ContractResolver ??= SharedContractResolver;
|
||||||
var serializerSettings = new JsonSerializerSettings(settings)
|
serializer = JsonSerializer.Create(new JsonSerializerSettings(settings) { ReferenceResolverProvider = () => resolver });
|
||||||
{
|
|
||||||
ReferenceResolverProvider = () => resolver
|
|
||||||
};
|
|
||||||
serializer = JsonSerializer.Create(serializerSettings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily set the reference resolver for cached serializer
|
|
||||||
var originalResolver = serializer.ReferenceResolver;
|
var originalResolver = serializer.ReferenceResolver;
|
||||||
serializer.ReferenceResolver = resolver;
|
serializer.ReferenceResolver = resolver;
|
||||||
|
|
||||||
|
|
@ -623,18 +428,17 @@ namespace AyCode.Core.Extensions
|
||||||
if (target is IList targetList)
|
if (target is IList targetList)
|
||||||
{
|
{
|
||||||
var type = target.GetType();
|
var type = target.GetType();
|
||||||
var elemType = TypeCache.GetElementType(type);
|
var elemType = GetCollectionElementType(type);
|
||||||
|
|
||||||
if (elemType != null)
|
if (elemType != null)
|
||||||
{
|
{
|
||||||
var (isId, idType) = TypeCache.GetIdInfo(elemType);
|
var (isId, idType) = GetIdInfo(elemType);
|
||||||
if (isId && idType != null)
|
if (isId && idType != null)
|
||||||
{
|
{
|
||||||
var converterInstance = RootConverterCache.GetOrAdd((elemType, idType), static k =>
|
var converterInstance = RootConverterCache.GetOrAdd((elemType, idType), static k =>
|
||||||
(JsonConverter)Activator.CreateInstance(
|
(JsonConverter)Activator.CreateInstance(
|
||||||
typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!);
|
typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!);
|
||||||
|
|
||||||
// Use JToken for collection merge (needed for complex logic)
|
|
||||||
var token = JToken.Parse(json);
|
var token = JToken.Parse(json);
|
||||||
using var reader = token.CreateReader();
|
using var reader = token.CreateReader();
|
||||||
converterInstance.ReadJson(reader, target.GetType(), target, serializer);
|
converterInstance.ReadJson(reader, target.GetType(), target, serializer);
|
||||||
|
|
@ -643,7 +447,6 @@ namespace AyCode.Core.Extensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-collection targets, use direct JsonTextReader for better performance
|
|
||||||
using var stringReader = new StringReader(json);
|
using var stringReader = new StringReader(json);
|
||||||
using var jsonReader = new JsonTextReader(stringReader);
|
using var jsonReader = new JsonTextReader(stringReader);
|
||||||
serializer.Populate(jsonReader, target);
|
serializer.Populate(jsonReader, target);
|
||||||
|
|
@ -654,4 +457,3 @@ namespace AyCode.Core.Extensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
using AyCode.Core.Interfaces;
|
using System.Collections.Concurrent;
|
||||||
using MessagePack;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Serialization;
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text;
|
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;
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
||||||
|
|
@ -36,8 +35,7 @@ internal static class Base62
|
||||||
value /= 62;
|
value /= 62;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNegative)
|
if (isNegative) buffer[--index] = '-';
|
||||||
buffer[--index] = '-';
|
|
||||||
|
|
||||||
return new string(buffer[index..]);
|
return new string(buffer[index..]);
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +51,7 @@ public class HybridReferenceResolver : IReferenceResolver
|
||||||
internal HashSet<string>? _referencedIds;
|
internal HashSet<string>? _referencedIds;
|
||||||
|
|
||||||
private int _nextNumericId = 1;
|
private int _nextNumericId = 1;
|
||||||
private static readonly ConcurrentDictionary<Type, Func<object, object?>> _idGetterCache = new();
|
private static readonly ConcurrentDictionary<Type, Func<object, object?>> IdGetterCache = new();
|
||||||
|
|
||||||
public bool IsForMerge { get; }
|
public bool IsForMerge { get; }
|
||||||
private readonly int _estimatedObjectCount;
|
private readonly int _estimatedObjectCount;
|
||||||
|
|
@ -86,13 +84,12 @@ public class HybridReferenceResolver : IReferenceResolver
|
||||||
var objectToId = GetObjectToId();
|
var objectToId = GetObjectToId();
|
||||||
if (objectToId.TryGetValue(value, out var existingId))
|
if (objectToId.TryGetValue(value, out var existingId))
|
||||||
{
|
{
|
||||||
if (!IsForMerge)
|
if (!IsForMerge) ReferencedIds.Add(existingId);
|
||||||
ReferencedIds.Add(existingId);
|
|
||||||
return existingId;
|
return existingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
var type = value.GetType();
|
var type = value.GetType();
|
||||||
var (isId, idType) = TypeCache.GetIdInfo(type);
|
var (isId, idType) = GetIdInfo(type);
|
||||||
|
|
||||||
string newRef;
|
string newRef;
|
||||||
if (isId && idType != null)
|
if (isId && idType != null)
|
||||||
|
|
@ -108,14 +105,10 @@ public class HybridReferenceResolver : IReferenceResolver
|
||||||
newRef = Base62.Encode(semanticId);
|
newRef = Base62.Encode(semanticId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
newRef = Base62.Encode(-_nextNumericId++);
|
newRef = Base62.Encode(-_nextNumericId++);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
newRef = Base62.Encode(-_nextNumericId++);
|
newRef = Base62.Encode(-_nextNumericId++);
|
||||||
}
|
|
||||||
|
|
||||||
GetIdToObject()[newRef] = value;
|
GetIdToObject()[newRef] = value;
|
||||||
objectToId[value] = newRef;
|
objectToId[value] = newRef;
|
||||||
|
|
@ -130,7 +123,7 @@ public class HybridReferenceResolver : IReferenceResolver
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static Func<object, object?> GetOrCreateIdGetter(Type type) =>
|
private static Func<object, object?> GetOrCreateIdGetter(Type type) =>
|
||||||
_idGetterCache.GetOrAdd(type, static t =>
|
IdGetterCache.GetOrAdd(type, static t =>
|
||||||
{
|
{
|
||||||
var prop = t.GetProperty("Id");
|
var prop = t.GetProperty("Id");
|
||||||
if (prop == null) return static _ => null;
|
if (prop == null) return static _ => null;
|
||||||
|
|
@ -138,44 +131,18 @@ public class HybridReferenceResolver : IReferenceResolver
|
||||||
if (getMethod == null) return static _ => null;
|
if (getMethod == null) return static _ => null;
|
||||||
return obj => getMethod.Invoke(obj, 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<object>
|
|
||||||
{
|
|
||||||
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
|
internal static class JsonReferencePostProcessor
|
||||||
{
|
{
|
||||||
private const string IdMarker = "\"$id\"";
|
private const string IdMarker = "\"$id\"";
|
||||||
private const string RefMarker = "\"$ref\"";
|
|
||||||
|
|
||||||
public static string RemoveUnreferencedIds(string json, HashSet<string>? referencedIds)
|
public static string RemoveUnreferencedIds(string json, HashSet<string>? referencedIds)
|
||||||
{
|
{
|
||||||
if (!json.Contains(IdMarker))
|
if (!json.Contains(IdMarker)) return json;
|
||||||
return json;
|
return referencedIds == null || referencedIds.Count == 0
|
||||||
|
? RemoveAllIdsSpan(json)
|
||||||
if (referencedIds == null || referencedIds.Count == 0)
|
: RemoveUnreferencedIdsSpan(json, referencedIds);
|
||||||
return RemoveAllIdsSpan(json);
|
|
||||||
|
|
||||||
return RemoveUnreferencedIdsSpan(json, referencedIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RemoveAllIdsSpan(string json)
|
private static string RemoveAllIdsSpan(string json)
|
||||||
|
|
@ -189,16 +156,14 @@ internal static class JsonReferencePostProcessor
|
||||||
var idIndex = json.IndexOf(IdMarker, searchStart, StringComparison.Ordinal);
|
var idIndex = json.IndexOf(IdMarker, searchStart, StringComparison.Ordinal);
|
||||||
if (idIndex < 0) break;
|
if (idIndex < 0) break;
|
||||||
|
|
||||||
if (idIndex > lastCopyEnd)
|
if (idIndex > lastCopyEnd) sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
|
||||||
sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
|
|
||||||
|
|
||||||
var endIndex = SkipIdEntry(json, idIndex);
|
var endIndex = SkipIdEntry(json, idIndex);
|
||||||
lastCopyEnd = endIndex;
|
lastCopyEnd = endIndex;
|
||||||
searchStart = endIndex;
|
searchStart = endIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastCopyEnd < json.Length)
|
if (lastCopyEnd < json.Length) sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
|
||||||
sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
|
|
||||||
|
|
||||||
return sb.Length == json.Length ? json : sb.ToString();
|
return sb.Length == json.Length ? json : sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
@ -225,9 +190,7 @@ internal static class JsonReferencePostProcessor
|
||||||
{
|
{
|
||||||
valueStart++;
|
valueStart++;
|
||||||
valueEnd = valueStart;
|
valueEnd = valueStart;
|
||||||
while (valueEnd < json.Length && json[valueEnd] != '"')
|
while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++;
|
||||||
valueEnd++;
|
|
||||||
|
|
||||||
idValue = json.Substring(valueStart, valueEnd - valueStart);
|
idValue = json.Substring(valueStart, valueEnd - valueStart);
|
||||||
valueEnd++;
|
valueEnd++;
|
||||||
}
|
}
|
||||||
|
|
@ -236,24 +199,17 @@ internal static class JsonReferencePostProcessor
|
||||||
valueEnd++;
|
valueEnd++;
|
||||||
|
|
||||||
if (idValue != null && referencedIds.Contains(idValue))
|
if (idValue != null && referencedIds.Contains(idValue))
|
||||||
{
|
|
||||||
searchStart = valueEnd;
|
searchStart = valueEnd;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (idIndex > lastCopyEnd)
|
if (idIndex > lastCopyEnd) sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
|
||||||
sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
|
|
||||||
|
|
||||||
lastCopyEnd = valueEnd;
|
lastCopyEnd = valueEnd;
|
||||||
searchStart = valueEnd;
|
searchStart = valueEnd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastCopyEnd == 0)
|
if (lastCopyEnd == 0) return json;
|
||||||
return json;
|
if (lastCopyEnd < json.Length) sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
|
||||||
|
|
||||||
if (lastCopyEnd < json.Length)
|
|
||||||
sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
|
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
@ -262,32 +218,29 @@ internal static class JsonReferencePostProcessor
|
||||||
private static int SkipIdEntry(string json, int idIndex)
|
private static int SkipIdEntry(string json, int idIndex)
|
||||||
{
|
{
|
||||||
var pos = idIndex + IdMarker.Length;
|
var pos = idIndex + IdMarker.Length;
|
||||||
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ':'))
|
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ':')) pos++;
|
||||||
pos++;
|
|
||||||
if (pos < json.Length && json[pos] == '"')
|
if (pos < json.Length && json[pos] == '"')
|
||||||
{
|
{
|
||||||
pos++;
|
pos++;
|
||||||
while (pos < json.Length && json[pos] != '"')
|
while (pos < json.Length && json[pos] != '"') pos++;
|
||||||
pos++;
|
if (pos < json.Length) pos++;
|
||||||
if (pos < json.Length)
|
|
||||||
pos++;
|
|
||||||
}
|
}
|
||||||
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ','))
|
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ',')) pos++;
|
||||||
pos++;
|
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HashSet<string> CollectReferencedIds(string json)
|
public static HashSet<string> CollectReferencedIds(string json)
|
||||||
{
|
{
|
||||||
|
const string refMarker = "\"$ref\"";
|
||||||
var result = new HashSet<string>(StringComparer.Ordinal);
|
var result = new HashSet<string>(StringComparer.Ordinal);
|
||||||
var searchStart = 0;
|
var searchStart = 0;
|
||||||
|
|
||||||
while (searchStart < json.Length)
|
while (searchStart < json.Length)
|
||||||
{
|
{
|
||||||
var refIndex = json.IndexOf(RefMarker, searchStart, StringComparison.Ordinal);
|
var refIndex = json.IndexOf(refMarker, searchStart, StringComparison.Ordinal);
|
||||||
if (refIndex < 0) break;
|
if (refIndex < 0) break;
|
||||||
|
|
||||||
var valueStart = refIndex + RefMarker.Length;
|
var valueStart = refIndex + refMarker.Length;
|
||||||
while (valueStart < json.Length && (json[valueStart] == ' ' || json[valueStart] == ':'))
|
while (valueStart < json.Length && (json[valueStart] == ' ' || json[valueStart] == ':'))
|
||||||
valueStart++;
|
valueStart++;
|
||||||
|
|
||||||
|
|
@ -295,19 +248,13 @@ internal static class JsonReferencePostProcessor
|
||||||
{
|
{
|
||||||
valueStart++;
|
valueStart++;
|
||||||
var valueEnd = valueStart;
|
var valueEnd = valueStart;
|
||||||
while (valueEnd < json.Length && json[valueEnd] != '"')
|
while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++;
|
||||||
valueEnd++;
|
if (valueEnd > valueStart) result.Add(json.Substring(valueStart, valueEnd - valueStart));
|
||||||
|
|
||||||
if (valueEnd > valueStart)
|
|
||||||
result.Add(json.Substring(valueStart, valueEnd - valueStart));
|
|
||||||
|
|
||||||
searchStart = valueEnd + 1;
|
searchStart = valueEnd + 1;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
searchStart = valueStart;
|
searchStart = valueStart;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -332,20 +279,12 @@ internal sealed class PooledStringWriter : StringWriter
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed) { _disposed = true; StringBuilderPool.Return(_pooledBuilder); }
|
||||||
{
|
|
||||||
_disposed = true;
|
|
||||||
StringBuilderPool.Return(_pooledBuilder);
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface ObjectPool<T> where T : class
|
internal interface ObjectPool<T> where T : class { T Get(); void Return(T obj); }
|
||||||
{
|
|
||||||
T Get();
|
|
||||||
void Return(T obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class DefaultObjectPool<T> : ObjectPool<T> where T : class
|
internal sealed class DefaultObjectPool<T> : ObjectPool<T> where T : class
|
||||||
{
|
{
|
||||||
|
|
@ -385,7 +324,6 @@ public static class SerializeObjectExtensions
|
||||||
{
|
{
|
||||||
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
|
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
|
||||||
private static readonly Dictionary<object, object> EmptyContextDict = new();
|
private static readonly Dictionary<object, object> EmptyContextDict = new();
|
||||||
private static readonly JsonSerializer CachedSerializer = CreateCachedSerializer();
|
|
||||||
|
|
||||||
public static JsonSerializerSettings Options => new()
|
public static JsonSerializerSettings Options => new()
|
||||||
{
|
{
|
||||||
|
|
@ -399,143 +337,113 @@ public static class SerializeObjectExtensions
|
||||||
Formatting = Formatting.None,
|
Formatting = Formatting.None,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static JsonSerializer CreateCachedSerializer() => JsonSerializer.Create(new JsonSerializerSettings
|
/// <summary>
|
||||||
{
|
/// Serialize object to JSON string with default options.
|
||||||
ContractResolver = SharedContractResolver,
|
/// </summary>
|
||||||
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
public static string ToJson<T>(this T source)
|
||||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
=> AcJsonSerializer.Serialize(source);
|
||||||
NullValueHandling = NullValueHandling.Ignore,
|
|
||||||
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
|
|
||||||
Formatting = Formatting.None,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serialize object to JSON string using high-performance AcJsonSerializer.
|
/// Serialize object to JSON string with specified options.
|
||||||
/// Uses optimized reference handling with $id/$ref for shared objects.
|
|
||||||
/// Skips default values (0, false, empty strings, empty collections) to reduce JSON size.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string ToJson<T>(this T source, JsonSerializerSettings? options = null)
|
public static string ToJson<T>(this T source, AcJsonSerializerOptions options)
|
||||||
|
=> AcJsonSerializer.Serialize(source, options);
|
||||||
|
|
||||||
|
public static string ToJson<T>(this IQueryable<T> source) where T : class, IAcSerializableToJson
|
||||||
|
=> AcJsonSerializer.Serialize(source);
|
||||||
|
|
||||||
|
public static string ToJson<T>(this IQueryable<T> source, AcJsonSerializerOptions options) where T : class, IAcSerializableToJson
|
||||||
|
=> AcJsonSerializer.Serialize(source, options);
|
||||||
|
|
||||||
|
public static string ToJson<T>(this IEnumerable<T> source) where T : class, IAcSerializableToJson
|
||||||
|
=> AcJsonSerializer.Serialize(source);
|
||||||
|
|
||||||
|
public static string ToJson<T>(this IEnumerable<T> source, AcJsonSerializerOptions options) where T : class, IAcSerializableToJson
|
||||||
|
=> AcJsonSerializer.Serialize(source, options);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize JSON to object with default options.
|
||||||
|
/// </summary>
|
||||||
|
public static T? JsonTo<T>(this string json)
|
||||||
{
|
{
|
||||||
// If custom options are provided, use Newtonsoft for full compatibility
|
json = UnwrapJsonString(json);
|
||||||
//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<T>(this IQueryable<T> 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<T>(this IEnumerable<T> source, JsonSerializerSettings? options = null) where T : class, IAcSerializableToJson
|
|
||||||
//=> options != null ? JsonConvert.SerializeObject(source, options) : AcJsonSerializer.Serialize(source);
|
|
||||||
=> ((object)source).ToJson(options);
|
|
||||||
|
|
||||||
public static T? JsonTo<T>(this string json, JsonSerializerSettings? options = null)
|
|
||||||
{
|
|
||||||
json = JsonUtilities.UnwrapJsonString(json);
|
|
||||||
return AcJsonDeserializer.Deserialize<T>(json);
|
return AcJsonDeserializer.Deserialize<T>(json);
|
||||||
|
|
||||||
// Use our high-performance custom deserializer
|
|
||||||
// AcJsonDeserializer now supports primitives, enums, and complex types
|
|
||||||
//if (options == null)
|
|
||||||
//{
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// return AcJsonDeserializer.Deserialize<T>(json);
|
|
||||||
// }
|
|
||||||
// catch
|
|
||||||
// {
|
|
||||||
// // Fallback to Newtonsoft if custom deserializer fails
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
//return JsonConvert.DeserializeObject<T>(json, options ?? Options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static object? JsonTo(this string json, Type toType, JsonSerializerSettings? options = null)
|
/// <summary>
|
||||||
|
/// Deserialize JSON to object with specified options.
|
||||||
|
/// </summary>
|
||||||
|
public static T? JsonTo<T>(this string json, AcJsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
json = JsonUtilities.UnwrapJsonString(json);
|
json = UnwrapJsonString(json);
|
||||||
|
return AcJsonDeserializer.Deserialize<T>(json, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize JSON to specified type with default options.
|
||||||
|
/// </summary>
|
||||||
|
public static object? JsonTo(this string json, Type toType)
|
||||||
|
{
|
||||||
|
json = UnwrapJsonString(json);
|
||||||
return AcJsonDeserializer.Deserialize(json, toType);
|
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)
|
/// <summary>
|
||||||
|
/// Deserialize JSON to specified type with specified options.
|
||||||
|
/// </summary>
|
||||||
|
public static object? JsonTo(this string json, Type toType, AcJsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
json = JsonUtilities.UnwrapJsonString(json);
|
json = UnwrapJsonString(json);
|
||||||
|
return AcJsonDeserializer.Deserialize(json, toType, options);
|
||||||
|
}
|
||||||
|
|
||||||
// Use runtime type instead of compile-time type for Populate
|
/// <summary>
|
||||||
|
/// Populate existing object from JSON with default options.
|
||||||
|
/// </summary>
|
||||||
|
public static void JsonTo(this string json, object target)
|
||||||
|
{
|
||||||
|
json = UnwrapJsonString(json);
|
||||||
AcJsonDeserializer.Populate(json, target);
|
AcJsonDeserializer.Populate(json, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
[return: NotNullIfNotNull(nameof(src))]
|
/// <summary>
|
||||||
public static TDestination? CloneTo<TDestination>(this object? src, JsonSerializerSettings? options = null) where TDestination : class
|
/// Populate existing object from JSON with specified options.
|
||||||
|
/// </summary>
|
||||||
|
public static void JsonTo(this string json, object target, AcJsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
json = UnwrapJsonString(json);
|
||||||
|
AcJsonDeserializer.Populate(json, target, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clone object via JSON serialization with default options.
|
||||||
|
/// </summary>
|
||||||
|
public static TDestination? CloneTo<TDestination>(this object? src) where TDestination : class
|
||||||
|
=> src?.ToJson().JsonTo<TDestination>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clone object via JSON serialization with specified options.
|
||||||
|
/// </summary>
|
||||||
|
public static TDestination? CloneTo<TDestination>(this object? src, AcJsonSerializerOptions options) where TDestination : class
|
||||||
=> src?.ToJson(options).JsonTo<TDestination>(options);
|
=> src?.ToJson(options).JsonTo<TDestination>(options);
|
||||||
|
|
||||||
public static void CopyTo(this object? src, object target, JsonSerializerSettings? options = null)
|
/// <summary>
|
||||||
|
/// Copy object properties to target via JSON with default options.
|
||||||
|
/// </summary>
|
||||||
|
public static void CopyTo(this object? src, object target)
|
||||||
|
=> src?.ToJson().JsonTo(target);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copy object properties to target via JSON with specified options.
|
||||||
|
/// </summary>
|
||||||
|
public static void CopyTo(this object? src, object target, AcJsonSerializerOptions options)
|
||||||
=> src?.ToJson(options).JsonTo(target, 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)
|
||||||
public static byte[] ToMessagePack(this object message, MessagePackSerializerOptions options) => MessagePackSerializer.Serialize(message, options);
|
=> MessagePackSerializer.Serialize(message, options);
|
||||||
//public static T MessagePackTo<T>(this byte[] message) => MessagePackSerializer.Deserialize<T>(message);
|
|
||||||
public static T MessagePackTo<T>(this byte[] message, MessagePackSerializerOptions options) => MessagePackSerializer.Deserialize<T>(message, options);
|
public static T MessagePackTo<T>(this byte[] message, MessagePackSerializerOptions options)
|
||||||
|
=> MessagePackSerializer.Deserialize<T>(message, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContractResolver
|
public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContractResolver
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,7 @@ namespace AyCode.Services.SignalRs
|
||||||
if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, TransportSendTimeout, MsDelay, MsFirstDelay) &&
|
if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, TransportSendTimeout, MsDelay, MsFirstDelay) &&
|
||||||
_responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage<string> responseMessage)
|
_responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage<string> responseMessage)
|
||||||
{
|
{
|
||||||
|
startTime = obj.RequestDateTime;
|
||||||
SignalRRequestModelPool.Return(obj);
|
SignalRRequestModelPool.Return(obj);
|
||||||
|
|
||||||
if (responseMessage.Status == SignalResponseStatus.Error || responseMessage.ResponseData == null)
|
if (responseMessage.Status == SignalResponseStatus.Error || responseMessage.ResponseData == null)
|
||||||
|
|
@ -355,14 +356,16 @@ namespace AyCode.Services.SignalRs
|
||||||
//return default;
|
//return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseMessage.ResponseData.JsonTo<TResponse>();
|
var responseData = responseMessage.ResponseData.JsonTo<TResponse>();
|
||||||
|
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)}]");
|
Logger.Error($"Client timeout after: {(DateTime.Now - startTime).TotalSeconds} sec! ConnectionState: {GetConnectionState()}; requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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))
|
if (_responseByRequestId.TryRemove(requestId, out var removedModel))
|
||||||
|
|
|
||||||
|
|
@ -215,17 +215,19 @@ public sealed class SignalResponseMessage<TResponseData> : ISignalResponseMessag
|
||||||
{
|
{
|
||||||
if (!_isDeserialized)
|
if (!_isDeserialized)
|
||||||
{
|
{
|
||||||
|
_isDeserialized = true;
|
||||||
|
|
||||||
_responseData = ResponseDataJson != null
|
_responseData = ResponseDataJson != null
|
||||||
? ResponseDataJson.JsonTo<TResponseData>()
|
? ResponseDataJson.JsonTo<TResponseData>()
|
||||||
: default;
|
: default;
|
||||||
_isDeserialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _responseData;
|
return _responseData;
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_responseData = value;
|
|
||||||
_isDeserialized = true;
|
_isDeserialized = true;
|
||||||
|
_responseData = value;
|
||||||
ResponseDataJson = value?.ToJson();
|
ResponseDataJson = value?.ToJson();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public class SignalRRequestModel : IResettable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryReset()
|
public bool TryReset()
|
||||||
{
|
{
|
||||||
RequestDateTime = DateTime.UtcNow;
|
RequestDateTime = default;
|
||||||
ResponseDateTime = default;
|
ResponseDateTime = default;
|
||||||
ResponseByRequestId = null;
|
ResponseByRequestId = null;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ public class SerializationBenchmarks
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
private JsonSerializerSettings _newtonsoftNoRefSettings = null!;
|
private JsonSerializerSettings _newtonsoftNoRefSettings = null!;
|
||||||
private JsonSerializerSettings _ayCodeSettings = null!;
|
|
||||||
|
|
||||||
[GlobalSetup]
|
[GlobalSetup]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
|
|
@ -39,9 +38,6 @@ public class SerializationBenchmarks
|
||||||
Formatting = Formatting.None
|
Formatting = Formatting.None
|
||||||
};
|
};
|
||||||
|
|
||||||
// AyCode WITH reference handling
|
|
||||||
_ayCodeSettings = SerializeObjectExtensions.Options;
|
|
||||||
|
|
||||||
// Create benchmark data using shared factory
|
// Create benchmark data using shared factory
|
||||||
// ~1500 objects: 5 items × 4 pallets × 3 measurements × 5 points = 300 points + containers
|
// ~1500 objects: 5 items × 4 pallets × 3 measurements × 5 points = 300 points + containers
|
||||||
_testOrder = TestDataFactory.CreateBenchmarkOrder(
|
_testOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||||
|
|
@ -52,7 +48,7 @@ public class SerializationBenchmarks
|
||||||
|
|
||||||
// Pre-serialize for deserialization benchmarks
|
// Pre-serialize for deserialization benchmarks
|
||||||
_newtonsoftJson = JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings);
|
_newtonsoftJson = JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings);
|
||||||
_ayCodeJson = _testOrder.ToJson(_ayCodeSettings);
|
_ayCodeJson = _testOrder.ToJson();
|
||||||
|
|
||||||
// Create target for populate benchmarks
|
// Create target for populate benchmarks
|
||||||
_populateTarget = new TestOrder();
|
_populateTarget = new TestOrder();
|
||||||
|
|
@ -77,7 +73,7 @@ public class SerializationBenchmarks
|
||||||
[Benchmark(Description = "AyCode (with refs)")]
|
[Benchmark(Description = "AyCode (with refs)")]
|
||||||
[BenchmarkCategory("Serialize")]
|
[BenchmarkCategory("Serialize")]
|
||||||
public string Serialize_AyCode_WithRefs()
|
public string Serialize_AyCode_WithRefs()
|
||||||
=> _testOrder.ToJson(_ayCodeSettings);
|
=> _testOrder.ToJson();
|
||||||
|
|
||||||
[Benchmark(Description = "AcJsonSerializer (custom)")]
|
[Benchmark(Description = "AcJsonSerializer (custom)")]
|
||||||
[BenchmarkCategory("Serialize")]
|
[BenchmarkCategory("Serialize")]
|
||||||
|
|
@ -96,7 +92,7 @@ public class SerializationBenchmarks
|
||||||
[Benchmark(Description = "AyCode (with refs)")]
|
[Benchmark(Description = "AyCode (with refs)")]
|
||||||
[BenchmarkCategory("Deserialize")]
|
[BenchmarkCategory("Deserialize")]
|
||||||
public TestOrder? Deserialize_AyCode_WithRefs()
|
public TestOrder? Deserialize_AyCode_WithRefs()
|
||||||
=> _ayCodeJson.JsonTo<TestOrder>(_ayCodeSettings);
|
=> _ayCodeJson.JsonTo<TestOrder>();
|
||||||
|
|
||||||
[Benchmark(Description = "AcJsonDeserializer (custom)")]
|
[Benchmark(Description = "AcJsonDeserializer (custom)")]
|
||||||
[BenchmarkCategory("Deserialize")]
|
[BenchmarkCategory("Deserialize")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue