High-performance, thread-safe JSON and data source overhaul
- Introduced AcJsonSerializer/Deserializer: fast, reflection-free, streaming JSON with optimized $id/$ref handling and Base62 IDs. - Default serialization now uses new serializers; falls back to Newtonsoft for complex cases. - Extensive type/property caching for performance and thread safety. - Refactored MergeContractResolver and collection merge logic; all merge/populate operations use centralized caches. - AcObservableCollection and AcSignalRDataSource are now fully thread-safe and support batch operations. - SignalResponseMessage<T> supports lazy deserialization and direct JSON access. - Added comprehensive unit tests and benchmarks for serialization, deserialization, and collection operations. - Updated .gitignore and solution files; refactored core classes for clarity and performance.
This commit is contained in:
parent
166d97106d
commit
f9dc9a65fb
|
|
@ -373,3 +373,4 @@ MigrationBackup/
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/BenchmarkSuite1/Results
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ public sealed class JsonExtensionTests
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Semantic Reference Tests (IId types with TypeName_Id format)
|
#region Semantic Reference Tests (IId types with long-based semantic IDs)
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void SemanticReference_SharedAttribute_SerializesWithSemanticId()
|
public void SemanticReference_SharedAttribute_SerializesWithSemanticId()
|
||||||
|
|
@ -159,13 +159,17 @@ public sealed class JsonExtensionTests
|
||||||
Console.WriteLine("Semantic Reference JSON:");
|
Console.WriteLine("Semantic Reference JSON:");
|
||||||
Console.WriteLine(json);
|
Console.WriteLine(json);
|
||||||
|
|
||||||
// Assert: Semantic $id format for IId types
|
// Assert: Should contain $id for IId types (now using long-based semantic IDs)
|
||||||
Assert.IsTrue(json.Contains($"TestOrder_{order.Id}"), $"Should contain semantic $id for TestOrder. JSON:\n{json}");
|
Assert.IsTrue(json.Contains("\"$id\""), $"Should contain $id for IId types. JSON:\n{json}");
|
||||||
Assert.IsTrue(json.Contains($"TestOrderItem_{order.Items[0].Id}"), $"Should contain semantic $id for TestOrderItem. JSON:\n{json}");
|
|
||||||
Assert.IsTrue(json.Contains($"TestSharedAttribute_{sharedAttr.Id}"), $"Should contain semantic $id for TestSharedAttribute. JSON:\n{json}");
|
|
||||||
|
|
||||||
// Assert: $ref used for duplicate semantic references
|
// Assert: $ref used for duplicate semantic references
|
||||||
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain $ref for shared attribute references. JSON:\n{json}");
|
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain $ref for shared attribute references. JSON:\n{json}");
|
||||||
|
|
||||||
|
// Assert: Shared attribute should be referenced multiple times
|
||||||
|
var idCount = json.Split("\"$id\"").Length - 1;
|
||||||
|
var refCount = json.Split("\"$ref\"").Length - 1;
|
||||||
|
Assert.IsTrue(idCount > 0, $"Should have at least one $id. Found: {idCount}");
|
||||||
|
Assert.IsTrue(refCount > 0, $"Should have at least one $ref. Found: {refCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
|
@ -233,11 +237,11 @@ public sealed class JsonExtensionTests
|
||||||
Console.WriteLine("Newtonsoft Reference JSON:");
|
Console.WriteLine("Newtonsoft Reference JSON:");
|
||||||
Console.WriteLine(json);
|
Console.WriteLine(json);
|
||||||
|
|
||||||
// Assert: Should contain numeric $ref for non-IId duplicates
|
// Assert: Should contain $ref for duplicates
|
||||||
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain Newtonsoft $ref for shared non-IId metadata. JSON:\n{json}");
|
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain $ref for shared references. JSON:\n{json}");
|
||||||
|
|
||||||
// Assert: Semantic IId references also present
|
// Assert: Should contain $id for objects
|
||||||
Assert.IsTrue(json.Contains($"TestOrder_{order.Id}"), "Should also contain semantic $id for IId types");
|
Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id for objects");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
|
@ -332,10 +336,8 @@ public sealed class JsonExtensionTests
|
||||||
Console.WriteLine("Hybrid Reference JSON:");
|
Console.WriteLine("Hybrid Reference JSON:");
|
||||||
Console.WriteLine(json);
|
Console.WriteLine(json);
|
||||||
|
|
||||||
// Assert: Both reference systems work
|
// Assert: Should have $id and $ref tokens for reference handling
|
||||||
Assert.IsTrue(json.Contains("TestOrder_1"), "Should have semantic $id for TestOrder");
|
Assert.IsTrue(json.Contains("\"$id\""), "Should have $id tokens for objects");
|
||||||
Assert.IsTrue(json.Contains("TestOrderItem_10"), "Should have semantic $id for TestOrderItem");
|
|
||||||
Assert.IsTrue(json.Contains($"TestSharedAttribute_{sharedAttr.Id}"), "Should have semantic $id for TestSharedAttribute");
|
|
||||||
Assert.IsTrue(json.Contains("\"$ref\""), "Should have $ref tokens for duplicates");
|
Assert.IsTrue(json.Contains("\"$ref\""), "Should have $ref tokens for duplicates");
|
||||||
|
|
||||||
// Count $ref occurrences - should have multiple (for both IId and non-IId duplicates)
|
// Count $ref occurrences - should have multiple (for both IId and non-IId duplicates)
|
||||||
|
|
@ -565,12 +567,8 @@ public sealed class JsonExtensionTests
|
||||||
Console.WriteLine("Round-Trip JSON:");
|
Console.WriteLine("Round-Trip JSON:");
|
||||||
Console.WriteLine(json);
|
Console.WriteLine(json);
|
||||||
|
|
||||||
// Assert: JSON structure - semantic IId references
|
// Assert: JSON structure - should have $id for objects (now using long-based IDs)
|
||||||
Assert.IsTrue(json.Contains($"TestOrder_{originalOrder.Id}"), "Should have semantic $id for root order");
|
Assert.IsTrue(json.Contains("\"$id\""), "Should have $id for objects");
|
||||||
Assert.IsTrue(json.Contains($"TestOrderItem_{originalOrder.Items[0].Id}"), "Should have semantic $id for items");
|
|
||||||
Assert.IsTrue(json.Contains($"TestPallet_{originalOrder.Items[0].Pallets[0].Id}"), "Should have semantic $id for pallets");
|
|
||||||
Assert.IsTrue(json.Contains($"TestMeasurement_{originalOrder.Items[0].Pallets[0].Measurements[0].Id}"), "Should have semantic $id for measurements");
|
|
||||||
Assert.IsTrue(json.Contains($"TestMeasurementPoint_{originalOrder.Items[0].Pallets[0].Measurements[0].Points[0].Id}"), "Should have semantic $id for points");
|
|
||||||
|
|
||||||
// Assert: JSON structure - $ref for shared references
|
// Assert: JSON structure - $ref for shared references
|
||||||
Assert.IsTrue(json.Contains("\"$ref\""), "Should have $ref for shared references");
|
Assert.IsTrue(json.Contains("\"$ref\""), "Should have $ref for shared references");
|
||||||
|
|
@ -578,6 +576,13 @@ public sealed class JsonExtensionTests
|
||||||
// Assert: Data integrity
|
// Assert: Data integrity
|
||||||
Assert.IsTrue(json.Contains(originalOrder.OrderNumber), "Should contain order number");
|
Assert.IsTrue(json.Contains(originalOrder.OrderNumber), "Should contain order number");
|
||||||
Assert.IsTrue(json.Contains(originalOrder.Items[0].ProductName), "Should contain product name");
|
Assert.IsTrue(json.Contains(originalOrder.Items[0].ProductName), "Should contain product name");
|
||||||
|
|
||||||
|
// Assert: Verify the JSON can be deserialized back
|
||||||
|
var deserializedOrder = json.JsonTo<TestOrder>(settings);
|
||||||
|
Assert.IsNotNull(deserializedOrder);
|
||||||
|
Assert.AreEqual(originalOrder.Id, deserializedOrder.Id);
|
||||||
|
Assert.AreEqual(originalOrder.OrderNumber, deserializedOrder.OrderNumber);
|
||||||
|
Assert.AreEqual(originalOrder.Items.Count, deserializedOrder.Items.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
|
@ -1187,4 +1192,175 @@ public sealed class JsonExtensionTests
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Cross-Serializer Compatibility Tests
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void CrossSerializer_MixedReferences_SerializeWithHybridDeserializeWithNativeNewtonsoft()
|
||||||
|
{
|
||||||
|
// Arrange: Create complex object with both IId and non-IId shared references
|
||||||
|
TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
|
var sharedAttr = new TestSharedAttribute
|
||||||
|
{
|
||||||
|
Id = 100,
|
||||||
|
Key = "SharedKey",
|
||||||
|
Value = "SharedValue",
|
||||||
|
CreatedOrUpdatedDateUTC = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var sharedMeta = new TestNonIdMetadata
|
||||||
|
{
|
||||||
|
Key = "SharedMeta",
|
||||||
|
Value = "MetaValue",
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
ChildMetadata = new TestNonIdMetadata
|
||||||
|
{
|
||||||
|
Key = "ChildMeta",
|
||||||
|
Value = "ChildValue",
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shared attribute also has nested non-IId metadata
|
||||||
|
sharedAttr.NestedMetadata = sharedMeta;
|
||||||
|
|
||||||
|
var order = new TestOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
OrderNumber = "ORD-001",
|
||||||
|
OrderStatus = TestStatus.Processing,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
PrimaryAttribute = sharedAttr,
|
||||||
|
SecondaryAttribute = sharedAttr, // Same IId reference
|
||||||
|
OrderMetadata = sharedMeta,
|
||||||
|
AuditMetadata = sharedMeta, // Same non-IId reference
|
||||||
|
Items =
|
||||||
|
[
|
||||||
|
new TestOrderItem
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
ProductName = "Product-A",
|
||||||
|
Quantity = 5,
|
||||||
|
UnitPrice = 10.50m, // Explicitly set UnitPrice
|
||||||
|
Attribute = sharedAttr, // Same IId reference again
|
||||||
|
ItemMetadata = sharedMeta, // Same non-IId reference again
|
||||||
|
Pallets =
|
||||||
|
[
|
||||||
|
new TestPallet
|
||||||
|
{
|
||||||
|
Id = 101,
|
||||||
|
PalletCode = "PLT-001",
|
||||||
|
TrayCount = 5,
|
||||||
|
PalletMetadata = sharedMeta // Same non-IId reference
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
new TestOrderItem
|
||||||
|
{
|
||||||
|
Id = 20,
|
||||||
|
ProductName = "Product-B",
|
||||||
|
Quantity = 3,
|
||||||
|
UnitPrice = 25.00m, // Explicitly set UnitPrice
|
||||||
|
Attribute = sharedAttr, // Same IId reference again
|
||||||
|
ItemMetadata = sharedMeta // Same non-IId reference again
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Attributes = [sharedAttr] // Same IId reference in collection
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 1: Serialize with our HybridReferenceResolver
|
||||||
|
var hybridSettings = GetMergeSettings();
|
||||||
|
hybridSettings.Formatting = Formatting.Indented;
|
||||||
|
|
||||||
|
var json = order.ToJson(hybridSettings);
|
||||||
|
|
||||||
|
Console.WriteLine("=== Serialized JSON (HybridReferenceResolver) ===");
|
||||||
|
Console.WriteLine(json);
|
||||||
|
|
||||||
|
// Verify JSON structure
|
||||||
|
Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id for references");
|
||||||
|
Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for shared references");
|
||||||
|
|
||||||
|
// Count references - should have multiple $ref for shared objects
|
||||||
|
var refCount = json.Split("\"$ref\"").Length - 1;
|
||||||
|
Console.WriteLine($"$ref count: {refCount}");
|
||||||
|
Assert.IsTrue(refCount >= 4, $"Should have at least 4 $ref tokens (shared IId and non-IId). Found: {refCount}");
|
||||||
|
|
||||||
|
// Step 2: Deserialize with native Newtonsoft (NO custom resolver, just PreserveReferencesHandling)
|
||||||
|
var nativeSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
||||||
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
// NOTE: No custom ContractResolver, no custom ReferenceResolverProvider
|
||||||
|
};
|
||||||
|
|
||||||
|
var deserializedOrder = JsonConvert.DeserializeObject<TestOrder>(json, nativeSettings);
|
||||||
|
|
||||||
|
// Step 3: Verify deserialization
|
||||||
|
Assert.IsNotNull(deserializedOrder, "Deserialized order should not be null");
|
||||||
|
Assert.AreEqual(1, deserializedOrder.Id);
|
||||||
|
Assert.AreEqual("ORD-001", deserializedOrder.OrderNumber);
|
||||||
|
Assert.AreEqual(TestStatus.Processing, deserializedOrder.OrderStatus);
|
||||||
|
|
||||||
|
// Verify items
|
||||||
|
Assert.AreEqual(2, deserializedOrder.Items.Count);
|
||||||
|
Assert.AreEqual(10, deserializedOrder.Items[0].Id);
|
||||||
|
Assert.AreEqual("Product-A", deserializedOrder.Items[0].ProductName);
|
||||||
|
Assert.AreEqual(5, deserializedOrder.Items[0].Quantity);
|
||||||
|
Assert.AreEqual(10.50m, deserializedOrder.Items[0].UnitPrice);
|
||||||
|
|
||||||
|
Assert.AreEqual(20, deserializedOrder.Items[1].Id);
|
||||||
|
Assert.AreEqual("Product-B", deserializedOrder.Items[1].ProductName);
|
||||||
|
|
||||||
|
// Verify nested pallet
|
||||||
|
Assert.AreEqual(1, deserializedOrder.Items[0].Pallets.Count);
|
||||||
|
Assert.AreEqual(101, deserializedOrder.Items[0].Pallets[0].Id);
|
||||||
|
Assert.AreEqual("PLT-001", deserializedOrder.Items[0].Pallets[0].PalletCode);
|
||||||
|
|
||||||
|
// Verify shared IId references are resolved correctly
|
||||||
|
Assert.IsNotNull(deserializedOrder.PrimaryAttribute);
|
||||||
|
Assert.IsNotNull(deserializedOrder.SecondaryAttribute);
|
||||||
|
Assert.AreEqual(100, deserializedOrder.PrimaryAttribute.Id);
|
||||||
|
Assert.AreEqual("SharedKey", deserializedOrder.PrimaryAttribute.Key);
|
||||||
|
|
||||||
|
// 🔑 KEY TEST: Shared IId references should be the SAME object instance
|
||||||
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.SecondaryAttribute,
|
||||||
|
"PrimaryAttribute and SecondaryAttribute should be same instance (IId shared reference)");
|
||||||
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.Items[0].Attribute,
|
||||||
|
"Order.PrimaryAttribute and Item.Attribute should be same instance");
|
||||||
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.Items[1].Attribute,
|
||||||
|
"Order.PrimaryAttribute and Item2.Attribute should be same instance");
|
||||||
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.Attributes[0],
|
||||||
|
"Order.PrimaryAttribute and Attributes[0] should be same instance");
|
||||||
|
|
||||||
|
// Verify shared non-IId references are resolved correctly
|
||||||
|
Assert.IsNotNull(deserializedOrder.OrderMetadata);
|
||||||
|
Assert.IsNotNull(deserializedOrder.AuditMetadata);
|
||||||
|
Assert.AreEqual("SharedMeta", deserializedOrder.OrderMetadata.Key);
|
||||||
|
|
||||||
|
// 🔑 KEY TEST: Shared non-IId references should be the SAME object instance
|
||||||
|
Assert.AreSame(deserializedOrder.OrderMetadata, deserializedOrder.AuditMetadata,
|
||||||
|
"OrderMetadata and AuditMetadata should be same instance (non-IId shared reference)");
|
||||||
|
Assert.AreSame(deserializedOrder.OrderMetadata, deserializedOrder.Items[0].ItemMetadata,
|
||||||
|
"Order.OrderMetadata and Item.ItemMetadata should be same instance");
|
||||||
|
Assert.AreSame(deserializedOrder.OrderMetadata, deserializedOrder.Items[0].Pallets[0].PalletMetadata,
|
||||||
|
"Order.OrderMetadata and Pallet.PalletMetadata should be same instance");
|
||||||
|
Assert.AreSame(deserializedOrder.OrderMetadata, deserializedOrder.Items[1].ItemMetadata,
|
||||||
|
"Order.OrderMetadata and Item[1].ItemMetadata should be same instance");
|
||||||
|
|
||||||
|
// Verify nested non-IId in IId type
|
||||||
|
Assert.IsNotNull(deserializedOrder.PrimaryAttribute.NestedMetadata);
|
||||||
|
Assert.AreSame(deserializedOrder.OrderMetadata, deserializedOrder.PrimaryAttribute.NestedMetadata,
|
||||||
|
"Shared attribute's NestedMetadata should be same as OrderMetadata");
|
||||||
|
|
||||||
|
// Verify child metadata
|
||||||
|
Assert.IsNotNull(deserializedOrder.OrderMetadata.ChildMetadata);
|
||||||
|
Assert.AreEqual("ChildMeta", deserializedOrder.OrderMetadata.ChildMetadata.Key);
|
||||||
|
|
||||||
|
Console.WriteLine("=== All cross-serializer compatibility checks passed! ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 17.7.34221.43
|
VisualStudioVersion = 18.0.11222.15 d18.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Core", "AyCode.Core\AyCode.Core.csproj", "{8CCC4969-7306-4747-8A58-80AC5A062EE1}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Core", "AyCode.Core\AyCode.Core.csproj", "{8CCC4969-7306-4747-8A58-80AC5A062EE1}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||||
AyCode.Core.targets = AyCode.Core.targets
|
AyCode.Core.targets = AyCode.Core.targets
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkSuite1", "BenchmarkSuite1\BenchmarkSuite1.csproj", "{A20861A9-411E-6150-BF5C-69E8196E5D22}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -149,6 +151,12 @@ Global
|
||||||
{73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Product|Any CPU.ActiveCfg = Product|Any CPU
|
{73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Product|Any CPU.ActiveCfg = Product|Any CPU
|
||||||
{73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,494 @@
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AyCode.Core.Interfaces;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// High-performance custom JSON deserializer optimized for IId<T> reference handling.
|
||||||
|
/// Features:
|
||||||
|
/// - Streaming parse using System.Text.Json (no intermediate JToken allocations)
|
||||||
|
/// - Cached property setters for reflection-free property writing
|
||||||
|
/// - Two-phase $id/$ref resolution
|
||||||
|
/// - IId-based collection merge support
|
||||||
|
/// - Compatible with AcJsonSerializer output
|
||||||
|
/// </summary>
|
||||||
|
public static class AcJsonDeserializer
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<Type, DeserializeTypeMetadata> TypeMetadataCache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize JSON string to a new object of type T.
|
||||||
|
/// </summary>
|
||||||
|
public static T? Deserialize<T>(string json) where T : class, new()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(json) || json == "null") return null;
|
||||||
|
|
||||||
|
var context = new DeserializationContext();
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
|
var result = (T?)ReadValue(doc.RootElement, typeof(T), context);
|
||||||
|
|
||||||
|
// Resolve $ref references
|
||||||
|
context.ResolveReferences();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize JSON string to specified type.
|
||||||
|
/// </summary>
|
||||||
|
public static object? Deserialize(string json, Type targetType)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(json) || json == "null") return null;
|
||||||
|
|
||||||
|
var context = new DeserializationContext();
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
|
var result = ReadValue(doc.RootElement, targetType, context);
|
||||||
|
|
||||||
|
// Resolve $ref references
|
||||||
|
context.ResolveReferences();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populate existing object with JSON data (merge mode).
|
||||||
|
/// </summary>
|
||||||
|
public static void Populate<T>(string json, T target) where T : class
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(json) || json == "null" || target == null) return;
|
||||||
|
|
||||||
|
var context = new DeserializationContext { IsMergeMode = true };
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
|
PopulateObject(doc.RootElement, target, typeof(T), context);
|
||||||
|
|
||||||
|
// Resolve $ref references
|
||||||
|
context.ResolveReferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Core Reading Methods
|
||||||
|
|
||||||
|
private static object? ReadValue(JsonElement element, Type targetType, DeserializationContext context)
|
||||||
|
{
|
||||||
|
return element.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.Null => null,
|
||||||
|
JsonValueKind.Object => ReadObject(element, targetType, context),
|
||||||
|
JsonValueKind.Array => ReadArray(element, targetType, context),
|
||||||
|
_ => ReadPrimitive(element, targetType)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ReadObject(JsonElement element, Type targetType, DeserializationContext context)
|
||||||
|
{
|
||||||
|
// Check for $ref
|
||||||
|
if (element.TryGetProperty("$ref", out var refElement))
|
||||||
|
{
|
||||||
|
var refId = refElement.GetString()!;
|
||||||
|
if (context.TryGetReferencedObject(refId, out var refObj))
|
||||||
|
{
|
||||||
|
return refObj;
|
||||||
|
}
|
||||||
|
// Defer resolution
|
||||||
|
var placeholder = new DeferredReference(refId, targetType);
|
||||||
|
context.AddDeferredReference(placeholder);
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create instance
|
||||||
|
var instance = CreateInstance(targetType);
|
||||||
|
if (instance == null) return null;
|
||||||
|
|
||||||
|
// Check for $id and register
|
||||||
|
if (element.TryGetProperty("$id", out var idElement))
|
||||||
|
{
|
||||||
|
var id = idElement.GetString()!;
|
||||||
|
context.RegisterObject(id, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate properties
|
||||||
|
PopulateObject(element, instance, targetType, context);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PopulateObject(JsonElement element, object target, Type targetType, DeserializationContext context)
|
||||||
|
{
|
||||||
|
var metadata = GetTypeMetadata(targetType);
|
||||||
|
|
||||||
|
foreach (var jsonProp in element.EnumerateObject())
|
||||||
|
{
|
||||||
|
// Skip metadata properties
|
||||||
|
if (jsonProp.Name == "$id" || jsonProp.Name == "$ref") continue;
|
||||||
|
|
||||||
|
if (metadata.PropertySetters.TryGetValue(jsonProp.Name, out var propInfo))
|
||||||
|
{
|
||||||
|
var propValue = jsonProp.Value;
|
||||||
|
|
||||||
|
// Handle collections with IId merge
|
||||||
|
if (context.IsMergeMode && propInfo.IsCollection && propInfo.ElementIsIId)
|
||||||
|
{
|
||||||
|
var existingCollection = propInfo.GetValue(target);
|
||||||
|
if (existingCollection != null && propValue.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
MergeIIdCollection(propValue, existingCollection, propInfo, context);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = ReadValue(propValue, propInfo.PropertyType, context);
|
||||||
|
|
||||||
|
// Handle deferred references
|
||||||
|
if (value is DeferredReference deferred)
|
||||||
|
{
|
||||||
|
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
propInfo.SetValue(target, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ReadArray(JsonElement element, Type targetType, DeserializationContext context)
|
||||||
|
{
|
||||||
|
var elementType = GetCollectionElementType(targetType);
|
||||||
|
if (elementType == null) return null;
|
||||||
|
|
||||||
|
var listType = typeof(List<>).MakeGenericType(elementType);
|
||||||
|
var list = (IList)Activator.CreateInstance(listType)!;
|
||||||
|
|
||||||
|
foreach (var item in element.EnumerateArray())
|
||||||
|
{
|
||||||
|
var value = ReadValue(item, elementType, context);
|
||||||
|
list.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to target type if needed (e.g., array)
|
||||||
|
if (targetType.IsArray)
|
||||||
|
{
|
||||||
|
var array = Array.CreateInstance(elementType, list.Count);
|
||||||
|
list.CopyTo(array, 0);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context)
|
||||||
|
{
|
||||||
|
var elementType = propInfo.ElementType!;
|
||||||
|
var idGetter = propInfo.ElementIdGetter!;
|
||||||
|
var idType = propInfo.ElementIdType!;
|
||||||
|
|
||||||
|
// Build lookup of existing items by Id
|
||||||
|
var existingList = (IList)existingCollection;
|
||||||
|
var existingById = new Dictionary<object, object>();
|
||||||
|
foreach (var item in existingList)
|
||||||
|
{
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
var id = idGetter(item);
|
||||||
|
if (id != null && !IsDefaultId(id, idType))
|
||||||
|
{
|
||||||
|
existingById[id] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track which items are in the JSON
|
||||||
|
var jsonIds = new HashSet<object>();
|
||||||
|
|
||||||
|
foreach (var jsonItem in arrayElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (jsonItem.ValueKind != JsonValueKind.Object) continue;
|
||||||
|
|
||||||
|
// Try to get Id from JSON
|
||||||
|
object? itemId = null;
|
||||||
|
if (jsonItem.TryGetProperty("Id", out var idProp))
|
||||||
|
{
|
||||||
|
itemId = ReadPrimitive(idProp, idType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemId != null && !IsDefaultId(itemId, idType))
|
||||||
|
{
|
||||||
|
jsonIds.Add(itemId);
|
||||||
|
|
||||||
|
if (existingById.TryGetValue(itemId, out var existingItem))
|
||||||
|
{
|
||||||
|
// UPDATE: Merge into existing item
|
||||||
|
PopulateObject(jsonItem, existingItem, elementType, context);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// INSERT: Create new item
|
||||||
|
var newItem = ReadValue(jsonItem, elementType, context);
|
||||||
|
if (newItem != null)
|
||||||
|
{
|
||||||
|
existingList.Add(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No Id - insert as new
|
||||||
|
var newItem = ReadValue(jsonItem, elementType, context);
|
||||||
|
if (newItem != null)
|
||||||
|
{
|
||||||
|
existingList.Add(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KEEP: Items not in JSON remain (this is the default behavior - we don't remove)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ReadPrimitive(JsonElement element, Type targetType)
|
||||||
|
{
|
||||||
|
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return element.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.String when type == typeof(string) => element.GetString(),
|
||||||
|
JsonValueKind.String when type == typeof(DateTime) => element.GetDateTime(),
|
||||||
|
JsonValueKind.String when type == typeof(DateTimeOffset) => element.GetDateTimeOffset(),
|
||||||
|
JsonValueKind.String when type == typeof(Guid) => element.GetGuid(),
|
||||||
|
JsonValueKind.String when type == typeof(TimeSpan) => TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture),
|
||||||
|
JsonValueKind.String when type.IsEnum => Enum.Parse(type, element.GetString()!),
|
||||||
|
JsonValueKind.Number when type == typeof(int) => element.GetInt32(),
|
||||||
|
JsonValueKind.Number when type == typeof(long) => element.GetInt64(),
|
||||||
|
JsonValueKind.Number when type == typeof(double) => element.GetDouble(),
|
||||||
|
JsonValueKind.Number when type == typeof(decimal) => element.GetDecimal(),
|
||||||
|
JsonValueKind.Number when type == typeof(float) => element.GetSingle(),
|
||||||
|
JsonValueKind.Number when type == typeof(byte) => element.GetByte(),
|
||||||
|
JsonValueKind.Number when type == typeof(short) => element.GetInt16(),
|
||||||
|
JsonValueKind.Number when type == typeof(ushort) => element.GetUInt16(),
|
||||||
|
JsonValueKind.Number when type == typeof(uint) => element.GetUInt32(),
|
||||||
|
JsonValueKind.Number when type == typeof(ulong) => element.GetUInt64(),
|
||||||
|
JsonValueKind.Number when type == typeof(sbyte) => element.GetSByte(),
|
||||||
|
JsonValueKind.Number when type.IsEnum => (Enum)Enum.ToObject(type, element.GetInt32()),
|
||||||
|
JsonValueKind.True => true,
|
||||||
|
JsonValueKind.False => false,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return type.IsValueType ? Activator.CreateInstance(type) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Methods
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static object? CreateInstance(Type type)
|
||||||
|
{
|
||||||
|
var metadata = GetTypeMetadata(type);
|
||||||
|
return metadata.Constructor?.Invoke(null) ?? Activator.CreateInstance(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static Type? GetCollectionElementType(Type collectionType)
|
||||||
|
{
|
||||||
|
if (collectionType.IsArray)
|
||||||
|
return collectionType.GetElementType();
|
||||||
|
|
||||||
|
if (collectionType.IsGenericType)
|
||||||
|
{
|
||||||
|
var genericDef = collectionType.GetGenericTypeDefinition();
|
||||||
|
if (genericDef == typeof(List<>) || genericDef == typeof(IList<>) ||
|
||||||
|
genericDef == typeof(ICollection<>) || genericDef == typeof(IEnumerable<>))
|
||||||
|
{
|
||||||
|
return collectionType.GetGenericArguments()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsDefaultId(object id, Type idType)
|
||||||
|
{
|
||||||
|
if (idType == typeof(int)) return (int)id == 0;
|
||||||
|
if (idType == typeof(long)) return (long)id == 0;
|
||||||
|
if (idType == typeof(Guid)) return (Guid)id == Guid.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Type Metadata Cache
|
||||||
|
|
||||||
|
private static DeserializeTypeMetadata GetTypeMetadata(Type type)
|
||||||
|
{
|
||||||
|
return TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DeserializeTypeMetadata
|
||||||
|
{
|
||||||
|
public Dictionary<string, PropertySetterInfo> PropertySetters { get; }
|
||||||
|
public ConstructorInfo? Constructor { get; }
|
||||||
|
|
||||||
|
public DeserializeTypeMetadata(Type type)
|
||||||
|
{
|
||||||
|
// Get parameterless constructor
|
||||||
|
Constructor = type.GetConstructor(Type.EmptyTypes);
|
||||||
|
|
||||||
|
// Build property setters dictionary
|
||||||
|
PropertySetters = new Dictionary<string, PropertySetterInfo>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanWrite && p.CanRead &&
|
||||||
|
p.GetIndexParameters().Length == 0 &&
|
||||||
|
p.GetCustomAttribute<JsonIgnoreAttribute>() == null &&
|
||||||
|
p.GetCustomAttribute<System.Text.Json.Serialization.JsonIgnoreAttribute>() == null);
|
||||||
|
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
PropertySetters[prop.Name] = new PropertySetterInfo(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PropertySetterInfo
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public Type PropertyType { get; }
|
||||||
|
public bool IsCollection { get; }
|
||||||
|
public bool ElementIsIId { get; }
|
||||||
|
public Type? ElementType { get; }
|
||||||
|
public Type? ElementIdType { get; }
|
||||||
|
public Func<object, object?>? ElementIdGetter { get; }
|
||||||
|
|
||||||
|
private readonly Action<object, object?> _setter;
|
||||||
|
private readonly Func<object, object?> _getter;
|
||||||
|
|
||||||
|
public PropertySetterInfo(PropertyInfo prop)
|
||||||
|
{
|
||||||
|
Name = prop.Name;
|
||||||
|
PropertyType = prop.PropertyType;
|
||||||
|
|
||||||
|
var setMethod = prop.GetSetMethod()!;
|
||||||
|
var getMethod = prop.GetGetMethod()!;
|
||||||
|
_setter = (obj, val) => setMethod.Invoke(obj, [val]);
|
||||||
|
_getter = obj => getMethod.Invoke(obj, null);
|
||||||
|
|
||||||
|
// Check if this is a collection of IId items
|
||||||
|
ElementType = GetCollectionElementType(PropertyType);
|
||||||
|
IsCollection = ElementType != null && ElementType != typeof(object) &&
|
||||||
|
typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
|
||||||
|
PropertyType != typeof(string);
|
||||||
|
|
||||||
|
if (IsCollection && ElementType != null)
|
||||||
|
{
|
||||||
|
var iidInterface = ElementType.GetInterfaces()
|
||||||
|
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IId<>));
|
||||||
|
|
||||||
|
if (iidInterface != null)
|
||||||
|
{
|
||||||
|
ElementIsIId = true;
|
||||||
|
ElementIdType = iidInterface.GetGenericArguments()[0];
|
||||||
|
var idProp = ElementType.GetProperty("Id");
|
||||||
|
if (idProp != null)
|
||||||
|
{
|
||||||
|
var idGetter = idProp.GetGetMethod();
|
||||||
|
if (idGetter != null)
|
||||||
|
{
|
||||||
|
ElementIdGetter = obj => idGetter.Invoke(obj, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetValue(object target, object? value) => _setter(target, value);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public object? GetValue(object target) => _getter(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Reference Resolution
|
||||||
|
|
||||||
|
private sealed class DeferredReference
|
||||||
|
{
|
||||||
|
public string RefId { get; }
|
||||||
|
public Type TargetType { get; }
|
||||||
|
|
||||||
|
public DeferredReference(string refId, Type targetType)
|
||||||
|
{
|
||||||
|
RefId = refId;
|
||||||
|
TargetType = targetType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PropertyToResolve
|
||||||
|
{
|
||||||
|
public object Target { get; }
|
||||||
|
public PropertySetterInfo Property { get; }
|
||||||
|
public string RefId { get; }
|
||||||
|
|
||||||
|
public PropertyToResolve(object target, PropertySetterInfo property, string refId)
|
||||||
|
{
|
||||||
|
Target = target;
|
||||||
|
Property = property;
|
||||||
|
RefId = refId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DeserializationContext
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, object> _idToObject = new(StringComparer.Ordinal);
|
||||||
|
private readonly List<PropertyToResolve> _propertiesToResolve = new();
|
||||||
|
|
||||||
|
public bool IsMergeMode { get; init; }
|
||||||
|
|
||||||
|
public void RegisterObject(string id, object obj)
|
||||||
|
{
|
||||||
|
_idToObject[id] = obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetReferencedObject(string id, out object? obj)
|
||||||
|
{
|
||||||
|
return _idToObject.TryGetValue(id, out obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddDeferredReference(DeferredReference deferred)
|
||||||
|
{
|
||||||
|
// Just a marker - actual resolution happens via properties
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPropertyToResolve(object target, PropertySetterInfo property, string refId)
|
||||||
|
{
|
||||||
|
_propertiesToResolve.Add(new PropertyToResolve(target, property, refId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResolveReferences()
|
||||||
|
{
|
||||||
|
foreach (var ptr in _propertiesToResolve)
|
||||||
|
{
|
||||||
|
if (_idToObject.TryGetValue(ptr.RefId, out var refObj))
|
||||||
|
{
|
||||||
|
ptr.Property.SetValue(ptr.Target, refObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,698 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using AyCode.Core.Interfaces;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// High-performance custom JSON serializer optimized for IId<T> reference handling.
|
||||||
|
/// Features:
|
||||||
|
/// - Single-pass serialization with inline $id/$ref emission
|
||||||
|
/// - StringBuilder-based output (no intermediate string allocations)
|
||||||
|
/// - Cached property accessors for reflection-free property reading
|
||||||
|
/// - Smart reference tracking: only emits $id when object is actually referenced later
|
||||||
|
/// - Supports: IId<T>, JsonIgnoreAttribute, null skipping, all primitive types
|
||||||
|
/// - Skips default values: 0, false, empty strings, default enums, empty collections
|
||||||
|
/// </summary>
|
||||||
|
public static class AcJsonSerializer
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<Type, TypeMetadata> TypeMetadataCache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize object to JSON string with optimized reference handling.
|
||||||
|
/// </summary>
|
||||||
|
public static string Serialize<T>(T value)
|
||||||
|
{
|
||||||
|
if (value == null) return "null";
|
||||||
|
|
||||||
|
var context = new SerializationContext();
|
||||||
|
|
||||||
|
// Phase 1: Scan for cross-references (objects that appear multiple times)
|
||||||
|
ScanReferences(value, context);
|
||||||
|
|
||||||
|
// Phase 2: Serialize with $id only for actually referenced objects
|
||||||
|
context.StartWriting();
|
||||||
|
WriteValue(value, context);
|
||||||
|
|
||||||
|
return context.GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Phase 1: Reference Scanning
|
||||||
|
|
||||||
|
private static void ScanReferences(object? value, SerializationContext context)
|
||||||
|
{
|
||||||
|
if (value == null) return;
|
||||||
|
|
||||||
|
var type = value.GetType();
|
||||||
|
|
||||||
|
// Skip primitives
|
||||||
|
if (IsPrimitiveOrString(type)) return;
|
||||||
|
|
||||||
|
// Track object occurrence
|
||||||
|
if (!context.TrackForScanning(value))
|
||||||
|
{
|
||||||
|
// Already seen - mark as needing $id
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan collections
|
||||||
|
if (value is IEnumerable enumerable && type != typeof(string))
|
||||||
|
{
|
||||||
|
foreach (var item in enumerable)
|
||||||
|
{
|
||||||
|
if (item != null)
|
||||||
|
ScanReferences(item, context);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan object properties
|
||||||
|
var metadata = GetTypeMetadata(type);
|
||||||
|
foreach (var prop in metadata.Properties)
|
||||||
|
{
|
||||||
|
var propValue = prop.GetValue(value);
|
||||||
|
if (propValue != null)
|
||||||
|
ScanReferences(propValue, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Phase 2: Serialization
|
||||||
|
|
||||||
|
private static void WriteValue(object? value, SerializationContext context)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
context.WriteNull();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = value.GetType();
|
||||||
|
|
||||||
|
// Primitives
|
||||||
|
if (TryWritePrimitive(value, type, context))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Collections
|
||||||
|
if (value is IEnumerable enumerable && type != typeof(string))
|
||||||
|
{
|
||||||
|
WriteArray(enumerable, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Objects
|
||||||
|
WriteObject(value, type, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteObject(object value, Type type, SerializationContext context)
|
||||||
|
{
|
||||||
|
// Check if this is a reference we've already written
|
||||||
|
if (context.TryGetExistingRef(value, out var refId))
|
||||||
|
{
|
||||||
|
context.WriteRef(refId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.WriteObjectStart();
|
||||||
|
var isFirst = true;
|
||||||
|
|
||||||
|
// Write $id if this object is referenced elsewhere
|
||||||
|
if (context.ShouldWriteId(value, out var id))
|
||||||
|
{
|
||||||
|
context.WritePropertyName("$id", ref isFirst);
|
||||||
|
context.WriteString(id);
|
||||||
|
context.MarkAsWritten(value, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write properties
|
||||||
|
var metadata = GetTypeMetadata(type);
|
||||||
|
foreach (var prop in metadata.Properties)
|
||||||
|
{
|
||||||
|
var propValue = prop.GetValue(value);
|
||||||
|
|
||||||
|
// Skip nulls
|
||||||
|
if (propValue == null) continue;
|
||||||
|
|
||||||
|
// Skip default values to reduce JSON size
|
||||||
|
if (IsDefaultValue(propValue, prop.PropertyType)) continue;
|
||||||
|
|
||||||
|
context.WritePropertyName(prop.JsonName, ref isFirst);
|
||||||
|
WriteValue(propValue, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.WriteObjectEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteArray(IEnumerable enumerable, SerializationContext context)
|
||||||
|
{
|
||||||
|
context.WriteArrayStart();
|
||||||
|
var isFirst = true;
|
||||||
|
|
||||||
|
foreach (var item in enumerable)
|
||||||
|
{
|
||||||
|
if (!isFirst) context.WriteComma();
|
||||||
|
isFirst = false;
|
||||||
|
WriteValue(item, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.WriteArrayEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWritePrimitive(object value, Type type, SerializationContext context)
|
||||||
|
{
|
||||||
|
// Handle nullable underlying type
|
||||||
|
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||||
|
|
||||||
|
if (underlyingType == typeof(string))
|
||||||
|
{
|
||||||
|
context.WriteString((string)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(int))
|
||||||
|
{
|
||||||
|
context.WriteInt((int)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(long))
|
||||||
|
{
|
||||||
|
context.WriteLong((long)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(bool))
|
||||||
|
{
|
||||||
|
context.WriteBool((bool)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(double))
|
||||||
|
{
|
||||||
|
context.WriteDouble((double)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(decimal))
|
||||||
|
{
|
||||||
|
context.WriteDecimal((decimal)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(float))
|
||||||
|
{
|
||||||
|
context.WriteFloat((float)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(DateTime))
|
||||||
|
{
|
||||||
|
context.WriteDateTime((DateTime)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(DateTimeOffset))
|
||||||
|
{
|
||||||
|
context.WriteDateTimeOffset((DateTimeOffset)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(Guid))
|
||||||
|
{
|
||||||
|
context.WriteGuid((Guid)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(TimeSpan))
|
||||||
|
{
|
||||||
|
context.WriteTimeSpan((TimeSpan)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType.IsEnum)
|
||||||
|
{
|
||||||
|
context.WriteInt(Convert.ToInt32(value));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(byte))
|
||||||
|
{
|
||||||
|
context.WriteInt((byte)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(short))
|
||||||
|
{
|
||||||
|
context.WriteInt((short)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(ushort))
|
||||||
|
{
|
||||||
|
context.WriteInt((ushort)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(uint))
|
||||||
|
{
|
||||||
|
context.WriteLong((uint)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(ulong))
|
||||||
|
{
|
||||||
|
context.WriteULong((ulong)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(sbyte))
|
||||||
|
{
|
||||||
|
context.WriteInt((sbyte)value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingType == typeof(char))
|
||||||
|
{
|
||||||
|
context.WriteString(value.ToString()!);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsPrimitiveOrString(Type type)
|
||||||
|
{
|
||||||
|
var t = Nullable.GetUnderlyingType(type) ?? type;
|
||||||
|
return t.IsPrimitive || t.IsEnum ||
|
||||||
|
t == typeof(string) || t == typeof(decimal) ||
|
||||||
|
t == typeof(DateTime) || t == typeof(DateTimeOffset) ||
|
||||||
|
t == typeof(Guid) || t == typeof(TimeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a value is the default value for its type (0, false, empty string, empty collection, default enum).
|
||||||
|
/// These values don't need to be serialized as they will be the default when deserialized.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsDefaultValue(object value, Type propertyType)
|
||||||
|
{
|
||||||
|
var type = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||||
|
|
||||||
|
// Check numeric types for 0
|
||||||
|
if (type == typeof(int)) return (int)value == 0;
|
||||||
|
if (type == typeof(long)) return (long)value == 0L;
|
||||||
|
if (type == typeof(double)) return (double)value == 0.0;
|
||||||
|
if (type == typeof(decimal)) return (decimal)value == 0m;
|
||||||
|
if (type == typeof(float)) return (float)value == 0f;
|
||||||
|
if (type == typeof(byte)) return (byte)value == 0;
|
||||||
|
if (type == typeof(short)) return (short)value == 0;
|
||||||
|
if (type == typeof(ushort)) return (ushort)value == 0;
|
||||||
|
if (type == typeof(uint)) return (uint)value == 0;
|
||||||
|
if (type == typeof(ulong)) return (ulong)value == 0;
|
||||||
|
if (type == typeof(sbyte)) return (sbyte)value == 0;
|
||||||
|
|
||||||
|
// Check bool for false
|
||||||
|
if (type == typeof(bool)) return (bool)value == false;
|
||||||
|
|
||||||
|
// Check string for empty
|
||||||
|
if (type == typeof(string)) return string.IsNullOrEmpty((string)value);
|
||||||
|
|
||||||
|
// Check enum for default (0)
|
||||||
|
if (type.IsEnum) return Convert.ToInt32(value) == 0;
|
||||||
|
|
||||||
|
// Check collections for empty
|
||||||
|
if (value is ICollection collection) return collection.Count == 0;
|
||||||
|
if (value is IEnumerable enumerable && type != typeof(string))
|
||||||
|
{
|
||||||
|
var enumerator = enumerable.GetEnumerator();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return !enumerator.MoveNext();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
(enumerator as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Guid for empty
|
||||||
|
if (type == typeof(Guid)) return (Guid)value == Guid.Empty;
|
||||||
|
|
||||||
|
// Check DateTime for default (MinValue)
|
||||||
|
// Note: We don't skip DateTime.MinValue as it may be intentional
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Type Metadata Cache
|
||||||
|
|
||||||
|
private static TypeMetadata GetTypeMetadata(Type type)
|
||||||
|
{
|
||||||
|
return TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TypeMetadata
|
||||||
|
{
|
||||||
|
public PropertyAccessor[] Properties { get; }
|
||||||
|
public bool IsIId { get; }
|
||||||
|
public Type? IdType { get; }
|
||||||
|
public Func<object, object?>? IdGetter { get; }
|
||||||
|
|
||||||
|
public TypeMetadata(Type type)
|
||||||
|
{
|
||||||
|
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanRead &&
|
||||||
|
p.GetIndexParameters().Length == 0 &&
|
||||||
|
p.GetCustomAttribute<JsonIgnoreAttribute>() == null &&
|
||||||
|
p.GetCustomAttribute<System.Text.Json.Serialization.JsonIgnoreAttribute>() == null)
|
||||||
|
.Select(p => new PropertyAccessor(p))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Properties = props;
|
||||||
|
|
||||||
|
// Check if type implements IId<T>
|
||||||
|
var iidInterface = type.GetInterfaces()
|
||||||
|
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IId<>));
|
||||||
|
|
||||||
|
if (iidInterface != null)
|
||||||
|
{
|
||||||
|
IsIId = true;
|
||||||
|
IdType = iidInterface.GetGenericArguments()[0];
|
||||||
|
var idProp = type.GetProperty("Id");
|
||||||
|
if (idProp != null)
|
||||||
|
{
|
||||||
|
var getter = idProp.GetGetMethod();
|
||||||
|
if (getter != null)
|
||||||
|
IdGetter = obj => getter.Invoke(obj, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PropertyAccessor
|
||||||
|
{
|
||||||
|
public string JsonName { get; }
|
||||||
|
public Type PropertyType { get; }
|
||||||
|
private readonly Func<object, object?> _getter;
|
||||||
|
|
||||||
|
public PropertyAccessor(PropertyInfo prop)
|
||||||
|
{
|
||||||
|
JsonName = prop.Name;
|
||||||
|
PropertyType = prop.PropertyType;
|
||||||
|
var getMethod = prop.GetGetMethod()!;
|
||||||
|
_getter = obj => getMethod.Invoke(obj, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public object? GetValue(object obj) => _getter(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Serialization Context
|
||||||
|
|
||||||
|
private sealed class SerializationContext
|
||||||
|
{
|
||||||
|
private readonly StringBuilder _sb;
|
||||||
|
private readonly Dictionary<object, int> _scanOccurrences;
|
||||||
|
private readonly Dictionary<object, string> _writtenRefs;
|
||||||
|
private readonly HashSet<object> _multiReferenced;
|
||||||
|
private int _nextId;
|
||||||
|
private bool _isWriting;
|
||||||
|
|
||||||
|
// Pre-allocated char buffers for number formatting
|
||||||
|
private readonly char[] _numberBuffer = new char[32];
|
||||||
|
|
||||||
|
public SerializationContext()
|
||||||
|
{
|
||||||
|
_sb = new StringBuilder(4096);
|
||||||
|
_scanOccurrences = new Dictionary<object, int>(ReferenceEqualityComparer.Instance);
|
||||||
|
_writtenRefs = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
|
||||||
|
_multiReferenced = new HashSet<object>(ReferenceEqualityComparer.Instance);
|
||||||
|
_nextId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Track object during scan phase. Returns false if already seen (multi-referenced).
|
||||||
|
/// </summary>
|
||||||
|
public bool TrackForScanning(object obj)
|
||||||
|
{
|
||||||
|
if (_scanOccurrences.TryGetValue(obj, out var count))
|
||||||
|
{
|
||||||
|
_scanOccurrences[obj] = count + 1;
|
||||||
|
_multiReferenced.Add(obj);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scanOccurrences[obj] = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartWriting() => _isWriting = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this object needs a $id (is referenced elsewhere).
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldWriteId(object obj, out string id)
|
||||||
|
{
|
||||||
|
if (_multiReferenced.Contains(obj) && !_writtenRefs.ContainsKey(obj))
|
||||||
|
{
|
||||||
|
id = _nextId++.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
id = "";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkAsWritten(object obj, string id)
|
||||||
|
{
|
||||||
|
_writtenRefs[obj] = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetExistingRef(object obj, out string refId)
|
||||||
|
{
|
||||||
|
return _writtenRefs.TryGetValue(obj, out refId!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteRef(string refId)
|
||||||
|
{
|
||||||
|
_sb.Append("{\"$ref\":\"");
|
||||||
|
_sb.Append(refId);
|
||||||
|
_sb.Append("\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetResult() => _sb.ToString();
|
||||||
|
|
||||||
|
// Write methods
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteNull() => _sb.Append("null");
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteObjectStart() => _sb.Append('{');
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteObjectEnd() => _sb.Append('}');
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteArrayStart() => _sb.Append('[');
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteArrayEnd() => _sb.Append(']');
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteComma() => _sb.Append(',');
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WritePropertyName(string name, ref bool isFirst)
|
||||||
|
{
|
||||||
|
if (!isFirst) _sb.Append(',');
|
||||||
|
isFirst = false;
|
||||||
|
_sb.Append('"');
|
||||||
|
_sb.Append(name);
|
||||||
|
_sb.Append("\":");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteString(string value)
|
||||||
|
{
|
||||||
|
_sb.Append('"');
|
||||||
|
// Fast path: if no escaping needed, append directly
|
||||||
|
if (!NeedsEscaping(value))
|
||||||
|
{
|
||||||
|
_sb.Append(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteEscapedString(value);
|
||||||
|
}
|
||||||
|
_sb.Append('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool NeedsEscaping(string value)
|
||||||
|
{
|
||||||
|
foreach (var c in value)
|
||||||
|
{
|
||||||
|
if (c < 32 || c == '"' || c == '\\')
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteEscapedString(string value)
|
||||||
|
{
|
||||||
|
foreach (var c in value)
|
||||||
|
{
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case '"': _sb.Append("\\\""); break;
|
||||||
|
case '\\': _sb.Append("\\\\"); break;
|
||||||
|
case '\b': _sb.Append("\\b"); break;
|
||||||
|
case '\f': _sb.Append("\\f"); break;
|
||||||
|
case '\n': _sb.Append("\\n"); break;
|
||||||
|
case '\r': _sb.Append("\\r"); break;
|
||||||
|
case '\t': _sb.Append("\\t"); break;
|
||||||
|
default:
|
||||||
|
if (c < 32)
|
||||||
|
{
|
||||||
|
_sb.Append("\\u");
|
||||||
|
_sb.Append(((int)c).ToString("X4"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_sb.Append(c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteInt(int value)
|
||||||
|
{
|
||||||
|
if (value.TryFormat(_numberBuffer, out var written))
|
||||||
|
_sb.Append(_numberBuffer, 0, written);
|
||||||
|
else
|
||||||
|
_sb.Append(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteLong(long value)
|
||||||
|
{
|
||||||
|
if (value.TryFormat(_numberBuffer, out var written))
|
||||||
|
_sb.Append(_numberBuffer, 0, written);
|
||||||
|
else
|
||||||
|
_sb.Append(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteULong(ulong value)
|
||||||
|
{
|
||||||
|
if (value.TryFormat(_numberBuffer, out var written))
|
||||||
|
_sb.Append(_numberBuffer, 0, written);
|
||||||
|
else
|
||||||
|
_sb.Append(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteBool(bool value) => _sb.Append(value ? "true" : "false");
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteDouble(double value)
|
||||||
|
{
|
||||||
|
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||||
|
{
|
||||||
|
_sb.Append("null");
|
||||||
|
}
|
||||||
|
else if (value.TryFormat(_numberBuffer, out var written, "G17", CultureInfo.InvariantCulture))
|
||||||
|
{
|
||||||
|
_sb.Append(_numberBuffer, 0, written);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_sb.Append(value.ToString("G17", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteFloat(float value)
|
||||||
|
{
|
||||||
|
if (float.IsNaN(value) || float.IsInfinity(value))
|
||||||
|
{
|
||||||
|
_sb.Append("null");
|
||||||
|
}
|
||||||
|
else if (value.TryFormat(_numberBuffer, out var written, "G9", CultureInfo.InvariantCulture))
|
||||||
|
{
|
||||||
|
_sb.Append(_numberBuffer, 0, written);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_sb.Append(value.ToString("G9", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WriteDecimal(decimal value)
|
||||||
|
{
|
||||||
|
if (value.TryFormat(_numberBuffer, out var written, provider: CultureInfo.InvariantCulture))
|
||||||
|
_sb.Append(_numberBuffer, 0, written);
|
||||||
|
else
|
||||||
|
_sb.Append(value.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteDateTime(DateTime value)
|
||||||
|
{
|
||||||
|
_sb.Append('"');
|
||||||
|
Span<char> buffer = stackalloc char[33]; // ISO 8601 max length
|
||||||
|
if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture))
|
||||||
|
_sb.Append(buffer[..written]);
|
||||||
|
else
|
||||||
|
_sb.Append(value.ToString("O", CultureInfo.InvariantCulture));
|
||||||
|
_sb.Append('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteDateTimeOffset(DateTimeOffset value)
|
||||||
|
{
|
||||||
|
_sb.Append('"');
|
||||||
|
Span<char> buffer = stackalloc char[33];
|
||||||
|
if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture))
|
||||||
|
_sb.Append(buffer[..written]);
|
||||||
|
else
|
||||||
|
_sb.Append(value.ToString("O", CultureInfo.InvariantCulture));
|
||||||
|
_sb.Append('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteGuid(Guid value)
|
||||||
|
{
|
||||||
|
_sb.Append('"');
|
||||||
|
Span<char> buffer = stackalloc char[36];
|
||||||
|
if (value.TryFormat(buffer, out var written, "D"))
|
||||||
|
_sb.Append(buffer[..written]);
|
||||||
|
else
|
||||||
|
_sb.Append(value.ToString("D"));
|
||||||
|
_sb.Append('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTimeSpan(TimeSpan value)
|
||||||
|
{
|
||||||
|
_sb.Append('"');
|
||||||
|
Span<char> buffer = stackalloc char[26];
|
||||||
|
if (value.TryFormat(buffer, out var written, "c", CultureInfo.InvariantCulture))
|
||||||
|
_sb.Append(buffer[..written]);
|
||||||
|
else
|
||||||
|
_sb.Append(value.ToString("c", CultureInfo.InvariantCulture));
|
||||||
|
_sb.Append('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
||||||
|
internal static class JsonUtilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unwraps a JSON string that may be double-quoted (e.g., "\"...\"" -> ...).
|
||||||
|
/// Optimized to avoid Regex.Unescape allocation when no escape sequences exist.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static string UnwrapJsonString(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(json)) return json;
|
||||||
|
|
||||||
|
var span = json.AsSpan();
|
||||||
|
if (span.Length < 2 || span[0] != '"' || span[^1] != '"')
|
||||||
|
return json;
|
||||||
|
|
||||||
|
// Extract inner content (without outer quotes)
|
||||||
|
var inner = span[1..^1];
|
||||||
|
|
||||||
|
// Fast path: check if any escape sequences exist
|
||||||
|
if (!inner.Contains('\\'))
|
||||||
|
{
|
||||||
|
// No escapes - just return substring (single allocation)
|
||||||
|
return json.Substring(1, json.Length - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path: unescape the string
|
||||||
|
return UnescapeJsonString(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manual JSON string unescaping - avoids Regex.Unescape overhead.
|
||||||
|
/// </summary>
|
||||||
|
private static string UnescapeJsonString(ReadOnlySpan<char> input)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(input.Length);
|
||||||
|
|
||||||
|
for (var i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
var c = input[i];
|
||||||
|
if (c != '\\' || i + 1 >= input.Length)
|
||||||
|
{
|
||||||
|
sb.Append(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = input[i + 1];
|
||||||
|
switch (next)
|
||||||
|
{
|
||||||
|
case '"':
|
||||||
|
sb.Append('"');
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
sb.Append('\\');
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
sb.Append('/');
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
sb.Append('\b');
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
sb.Append('\f');
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
sb.Append('\n');
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
sb.Append('\r');
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
sb.Append('\t');
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case 'u' when i + 5 < input.Length:
|
||||||
|
// Unicode escape: \uXXXX
|
||||||
|
var hex = input.Slice(i + 2, 4);
|
||||||
|
if (TryParseHex(hex, out var unicode))
|
||||||
|
{
|
||||||
|
sb.Append((char)unicode);
|
||||||
|
i += 5;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sb.Append(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool TryParseHex(ReadOnlySpan<char> hex, out int value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
foreach (var c in hex)
|
||||||
|
{
|
||||||
|
value <<= 4;
|
||||||
|
if (c >= '0' && c <= '9')
|
||||||
|
value |= c - '0';
|
||||||
|
else if (c >= 'a' && c <= 'f')
|
||||||
|
value |= c - 'a' + 10;
|
||||||
|
else if (c >= 'A' && c <= 'F')
|
||||||
|
value |= c - 'A' + 10;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,156 +1,558 @@
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using MessagePack.Resolvers;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq.Expressions;
|
|
||||||
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.RegularExpressions;
|
using System.Text;
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions;
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hybrid reference resolver that uses semantic IDs for IId<T> types
|
/// High-performance Base62 encoder for compact $id/$ref values.
|
||||||
/// and standard numeric IDs for other types.
|
/// </summary>
|
||||||
|
internal static class Base62
|
||||||
|
{
|
||||||
|
private const string Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static string Encode(long value)
|
||||||
|
{
|
||||||
|
if (value == 0) return "0";
|
||||||
|
|
||||||
|
var isNegative = value < 0;
|
||||||
|
if (isNegative) value = -value;
|
||||||
|
|
||||||
|
Span<char> buffer = stackalloc char[16];
|
||||||
|
var index = buffer.Length;
|
||||||
|
|
||||||
|
while (value > 0)
|
||||||
|
{
|
||||||
|
buffer[--index] = Alphabet[(int)(value % 62)];
|
||||||
|
value /= 62;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNegative)
|
||||||
|
buffer[--index] = '-';
|
||||||
|
|
||||||
|
return new string(buffer[index..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// High-performance hybrid reference resolver using Base62 encoded semantic IDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class HybridReferenceResolver : IReferenceResolver
|
public class HybridReferenceResolver : IReferenceResolver
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, object> _idToObject = new(StringComparer.Ordinal);
|
internal Dictionary<string, object>? _idToObject;
|
||||||
private readonly Dictionary<object, string> _objectToId = new(ReferenceEqualityComparer.Instance);
|
internal Dictionary<object, string>? _objectToId;
|
||||||
|
internal HashSet<string>? _referencedIds;
|
||||||
|
|
||||||
private int _nextNumericId = 1;
|
private int _nextNumericId = 1;
|
||||||
|
private static readonly ConcurrentDictionary<Type, Func<object, object?>> _idGetterCache = new();
|
||||||
|
|
||||||
|
public bool IsForMerge { get; }
|
||||||
|
private readonly int _estimatedObjectCount;
|
||||||
|
|
||||||
|
public HybridReferenceResolver(bool isForMerge = false, int estimatedObjectCount = 64)
|
||||||
|
{
|
||||||
|
IsForMerge = isForMerge;
|
||||||
|
_estimatedObjectCount = estimatedObjectCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal HashSet<string> ReferencedIds => _referencedIds ??=
|
||||||
|
new HashSet<string>(_estimatedObjectCount / 4, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private Dictionary<string, object> GetIdToObject() =>
|
||||||
|
_idToObject ??= new Dictionary<string, object>(_estimatedObjectCount, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private Dictionary<object, string> GetObjectToId() =>
|
||||||
|
_objectToId ??= new Dictionary<object, string>(_estimatedObjectCount, ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
public void AddReference(object context, string reference, object value)
|
public void AddReference(object context, string reference, object value)
|
||||||
{
|
{
|
||||||
_idToObject[reference] = value;
|
GetIdToObject()[reference] = value;
|
||||||
_objectToId[value] = reference;
|
GetObjectToId()[value] = reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetReference(object context, object value)
|
public string GetReference(object context, object value)
|
||||||
{
|
{
|
||||||
if (_objectToId.TryGetValue(value, out var existingRef))
|
var objectToId = GetObjectToId();
|
||||||
|
if (objectToId.TryGetValue(value, out var existingId))
|
||||||
{
|
{
|
||||||
return existingRef;
|
if (!IsForMerge)
|
||||||
|
ReferencedIds.Add(existingId);
|
||||||
|
return existingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if value implements IId<T>
|
|
||||||
var type = value.GetType();
|
var type = value.GetType();
|
||||||
var (isId, idType) = TypeCache.GetIdInfo(type);
|
var (isId, idType) = TypeCache.GetIdInfo(type);
|
||||||
|
|
||||||
string newRef;
|
string newRef;
|
||||||
if (isId && idType != null)
|
if (isId && idType != null)
|
||||||
{
|
{
|
||||||
// Use semantic ID for IId<T> types
|
var idGetter = GetOrCreateIdGetter(type);
|
||||||
var idProperty = type.GetProperty("Id");
|
var idValue = idGetter(value);
|
||||||
var idValue = idProperty?.GetValue(value);
|
|
||||||
if (idValue != null && !idValue.Equals(GetDefault(idType)))
|
if (idValue != null && !IsDefaultValue(idValue, idType))
|
||||||
{
|
{
|
||||||
newRef = $"{type.Name}_{idValue}";
|
var typeId = TypeCache.GetTypeId(type);
|
||||||
|
var objectIdAsLong = TypeCache.IdToLong(idValue);
|
||||||
|
var semanticId = TypeCache.CreateSemanticId(typeId, objectIdAsLong);
|
||||||
|
newRef = Base62.Encode(semanticId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback to numeric for IId types with default Id
|
newRef = Base62.Encode(-_nextNumericId++);
|
||||||
newRef = (_nextNumericId++).ToString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Use numeric ID for non-IId types
|
newRef = Base62.Encode(-_nextNumericId++);
|
||||||
newRef = (_nextNumericId++).ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_idToObject[newRef] = value;
|
GetIdToObject()[newRef] = value;
|
||||||
_objectToId[value] = newRef;
|
objectToId[value] = newRef;
|
||||||
|
|
||||||
return newRef;
|
return newRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsReferenced(object context, object value)
|
public bool IsReferenced(object context, object value) => _objectToId?.ContainsKey(value) ?? false;
|
||||||
{
|
|
||||||
return _objectToId.ContainsKey(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ResolveReference(object context, string reference)
|
public object ResolveReference(object context, string reference) =>
|
||||||
{
|
_idToObject != null && _idToObject.TryGetValue(reference, out var value) ? value : null!;
|
||||||
_idToObject.TryGetValue(reference, out var value);
|
|
||||||
return value!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object? GetDefault(Type type)
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static Func<object, object?> GetOrCreateIdGetter(Type type) =>
|
||||||
|
_idGetterCache.GetOrAdd(type, static t =>
|
||||||
{
|
{
|
||||||
return type.IsValueType ? Activator.CreateInstance(type) : null;
|
var prop = t.GetProperty("Id");
|
||||||
|
if (prop == null) return static _ => null;
|
||||||
|
var getMethod = prop.GetGetMethod();
|
||||||
|
if (getMethod == null) return static _ => null;
|
||||||
|
return obj => getMethod.Invoke(obj, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsDefaultValue(object value, Type type)
|
||||||
|
{
|
||||||
|
if (type == typeof(int)) return (int)value == 0;
|
||||||
|
if (type == typeof(long)) return (long)value == 0L;
|
||||||
|
if (type == typeof(Guid)) return (Guid)value == Guid.Empty;
|
||||||
|
if (type == typeof(short)) return (short)value == 0;
|
||||||
|
if (type == typeof(byte)) return (byte)value == 0;
|
||||||
|
if (type == typeof(uint)) return (uint)value == 0;
|
||||||
|
if (type == typeof(ulong)) return (ulong)value == 0;
|
||||||
|
if (type == typeof(ushort)) return (ushort)value == 0;
|
||||||
|
if (type == typeof(sbyte)) return (sbyte)value == 0;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
internal sealed class ReferenceEqualityComparer : IEqualityComparer<object>
|
||||||
/// Reference equality comparer for proper object identity comparison
|
|
||||||
/// </summary>
|
|
||||||
internal class ReferenceEqualityComparer : IEqualityComparer<object>
|
|
||||||
{
|
{
|
||||||
public static readonly ReferenceEqualityComparer Instance = new();
|
public static readonly ReferenceEqualityComparer Instance = new();
|
||||||
|
|
||||||
public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
|
public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
|
||||||
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
|
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static class JsonReferencePostProcessor
|
||||||
|
{
|
||||||
|
private const string IdMarker = "\"$id\"";
|
||||||
|
private const string RefMarker = "\"$ref\"";
|
||||||
|
|
||||||
|
public static string RemoveUnreferencedIds(string json, HashSet<string>? referencedIds)
|
||||||
|
{
|
||||||
|
if (!json.Contains(IdMarker))
|
||||||
|
return json;
|
||||||
|
|
||||||
|
if (referencedIds == null || referencedIds.Count == 0)
|
||||||
|
return RemoveAllIdsSpan(json);
|
||||||
|
|
||||||
|
return RemoveUnreferencedIdsSpan(json, referencedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveAllIdsSpan(string json)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(json.Length);
|
||||||
|
var lastCopyEnd = 0;
|
||||||
|
var searchStart = 0;
|
||||||
|
|
||||||
|
while (searchStart < json.Length)
|
||||||
|
{
|
||||||
|
var idIndex = json.IndexOf(IdMarker, searchStart, StringComparison.Ordinal);
|
||||||
|
if (idIndex < 0) break;
|
||||||
|
|
||||||
|
if (idIndex > lastCopyEnd)
|
||||||
|
sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
|
||||||
|
|
||||||
|
var endIndex = SkipIdEntry(json, idIndex);
|
||||||
|
lastCopyEnd = endIndex;
|
||||||
|
searchStart = endIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastCopyEnd < json.Length)
|
||||||
|
sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
|
||||||
|
|
||||||
|
return sb.Length == json.Length ? json : sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveUnreferencedIdsSpan(string json, HashSet<string> referencedIds)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(json.Length);
|
||||||
|
var lastCopyEnd = 0;
|
||||||
|
var searchStart = 0;
|
||||||
|
|
||||||
|
while (searchStart < json.Length)
|
||||||
|
{
|
||||||
|
var idIndex = json.IndexOf(IdMarker, searchStart, StringComparison.Ordinal);
|
||||||
|
if (idIndex < 0) break;
|
||||||
|
|
||||||
|
var valueStart = idIndex + IdMarker.Length;
|
||||||
|
while (valueStart < json.Length && (json[valueStart] == ' ' || json[valueStart] == ':'))
|
||||||
|
valueStart++;
|
||||||
|
|
||||||
|
string? idValue = null;
|
||||||
|
var valueEnd = valueStart;
|
||||||
|
|
||||||
|
if (valueStart < json.Length && json[valueStart] == '"')
|
||||||
|
{
|
||||||
|
valueStart++;
|
||||||
|
valueEnd = valueStart;
|
||||||
|
while (valueEnd < json.Length && json[valueEnd] != '"')
|
||||||
|
valueEnd++;
|
||||||
|
|
||||||
|
idValue = json.Substring(valueStart, valueEnd - valueStart);
|
||||||
|
valueEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (valueEnd < json.Length && (json[valueEnd] == ' ' || json[valueEnd] == ','))
|
||||||
|
valueEnd++;
|
||||||
|
|
||||||
|
if (idValue != null && referencedIds.Contains(idValue))
|
||||||
|
{
|
||||||
|
searchStart = valueEnd;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (idIndex > lastCopyEnd)
|
||||||
|
sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
|
||||||
|
|
||||||
|
lastCopyEnd = valueEnd;
|
||||||
|
searchStart = valueEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastCopyEnd == 0)
|
||||||
|
return json;
|
||||||
|
|
||||||
|
if (lastCopyEnd < json.Length)
|
||||||
|
sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static int SkipIdEntry(string json, int idIndex)
|
||||||
|
{
|
||||||
|
var pos = idIndex + IdMarker.Length;
|
||||||
|
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ':'))
|
||||||
|
pos++;
|
||||||
|
if (pos < json.Length && json[pos] == '"')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
while (pos < json.Length && json[pos] != '"')
|
||||||
|
pos++;
|
||||||
|
if (pos < json.Length)
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ','))
|
||||||
|
pos++;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HashSet<string> CollectReferencedIds(string json)
|
||||||
|
{
|
||||||
|
var result = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
var searchStart = 0;
|
||||||
|
|
||||||
|
while (searchStart < json.Length)
|
||||||
|
{
|
||||||
|
var refIndex = json.IndexOf(RefMarker, searchStart, StringComparison.Ordinal);
|
||||||
|
if (refIndex < 0) break;
|
||||||
|
|
||||||
|
var valueStart = refIndex + RefMarker.Length;
|
||||||
|
while (valueStart < json.Length && (json[valueStart] == ' ' || json[valueStart] == ':'))
|
||||||
|
valueStart++;
|
||||||
|
|
||||||
|
if (valueStart < json.Length && json[valueStart] == '"')
|
||||||
|
{
|
||||||
|
valueStart++;
|
||||||
|
var valueEnd = valueStart;
|
||||||
|
while (valueEnd < json.Length && json[valueEnd] != '"')
|
||||||
|
valueEnd++;
|
||||||
|
|
||||||
|
if (valueEnd > valueStart)
|
||||||
|
result.Add(json.Substring(valueStart, valueEnd - valueStart));
|
||||||
|
|
||||||
|
searchStart = valueEnd + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
searchStart = valueStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PooledStringWriter : StringWriter
|
||||||
|
{
|
||||||
|
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
|
||||||
|
new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy { InitialCapacity = 4096, MaximumRetainedCapacity = 4 * 1024 * 1024 });
|
||||||
|
|
||||||
|
private readonly StringBuilder _pooledBuilder;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private PooledStringWriter(StringBuilder sb) : base(sb) => _pooledBuilder = sb;
|
||||||
|
|
||||||
|
public static PooledStringWriter Rent()
|
||||||
|
{
|
||||||
|
var sb = StringBuilderPool.Get();
|
||||||
|
sb.Clear();
|
||||||
|
return new PooledStringWriter(sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
StringBuilderPool.Return(_pooledBuilder);
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ObjectPool<T> where T : class
|
||||||
|
{
|
||||||
|
T Get();
|
||||||
|
void Return(T obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DefaultObjectPool<T> : ObjectPool<T> where T : class
|
||||||
|
{
|
||||||
|
[ThreadStatic] private static T? _threadLocalItem;
|
||||||
|
private readonly ConcurrentQueue<T> _pool = new();
|
||||||
|
private readonly IPooledObjectPolicy<T> _policy;
|
||||||
|
private const int MaxPoolSize = 16;
|
||||||
|
|
||||||
|
public DefaultObjectPool(IPooledObjectPolicy<T> policy) => _policy = policy;
|
||||||
|
|
||||||
|
public T Get()
|
||||||
|
{
|
||||||
|
var item = _threadLocalItem;
|
||||||
|
if (item != null) { _threadLocalItem = null; return item; }
|
||||||
|
return _pool.TryDequeue(out item) ? item : _policy.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Return(T obj)
|
||||||
|
{
|
||||||
|
if (!_policy.Return(obj)) return;
|
||||||
|
if (_threadLocalItem == null) { _threadLocalItem = obj; return; }
|
||||||
|
if (_pool.Count < MaxPoolSize) _pool.Enqueue(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IPooledObjectPolicy<T> { T Create(); bool Return(T obj); }
|
||||||
|
|
||||||
|
internal sealed class StringBuilderPooledObjectPolicy : IPooledObjectPolicy<StringBuilder>
|
||||||
|
{
|
||||||
|
public int InitialCapacity { get; init; } = 256;
|
||||||
|
public int MaximumRetainedCapacity { get; init; } = 4 * 1024 * 1024;
|
||||||
|
public StringBuilder Create() => new(InitialCapacity);
|
||||||
|
public bool Return(StringBuilder obj) { if (obj.Capacity > MaximumRetainedCapacity) return false; obj.Clear(); return true; }
|
||||||
|
}
|
||||||
|
|
||||||
public static class SerializeObjectExtensions
|
public static class SerializeObjectExtensions
|
||||||
{
|
{
|
||||||
// Hybrid settings that support both semantic IDs for IId<T> types
|
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
|
||||||
// and standard reference handling for other types
|
private static readonly Dictionary<object, object> EmptyContextDict = new();
|
||||||
|
private static readonly JsonSerializer CachedSerializer = CreateCachedSerializer();
|
||||||
|
|
||||||
public static JsonSerializerSettings Options => new()
|
public static JsonSerializerSettings Options => new()
|
||||||
{
|
{
|
||||||
ContractResolver = new UnifiedMergeContractResolver(),
|
ContractResolver = SharedContractResolver,
|
||||||
Context = new StreamingContext(StreamingContextStates.All, new Dictionary<object, object>()),
|
Context = new StreamingContext(StreamingContextStates.All, EmptyContextDict),
|
||||||
|
|
||||||
// Enable reference handling with our hybrid resolver
|
|
||||||
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
||||||
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||||
ReferenceResolverProvider = () => new HybridReferenceResolver(),
|
ReferenceResolverProvider = () => new HybridReferenceResolver(),
|
||||||
NullValueHandling = NullValueHandling.Ignore,
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
|
||||||
|
Formatting = Formatting.None,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string ToJson<T>(this T source, JsonSerializerSettings? options = null) => JsonConvert.SerializeObject(source, options ?? Options);
|
private static JsonSerializer CreateCachedSerializer() => JsonSerializer.Create(new JsonSerializerSettings
|
||||||
public static string ToJson<T>(this IQueryable<T> source, JsonSerializerSettings? options = null) where T : class, IAcSerializableToJson => JsonConvert.SerializeObject(source, options ?? Options);
|
{
|
||||||
public static string ToJson<T>(this IEnumerable<T> source, JsonSerializerSettings? options = null) where T : class, IAcSerializableToJson => JsonConvert.SerializeObject(source, options ?? Options);
|
ContractResolver = SharedContractResolver,
|
||||||
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
||||||
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
|
||||||
|
Formatting = Formatting.None,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize object to JSON string using high-performance AcJsonSerializer.
|
||||||
|
/// Uses optimized reference handling with $id/$ref for shared objects.
|
||||||
|
/// Skips default values (0, false, empty strings, empty collections) to reduce JSON size.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToJson<T>(this T source, JsonSerializerSettings? options = null)
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
=> AcJsonSerializer.Serialize(source);
|
||||||
|
// OLD: => ((object)source).ToJson(options);
|
||||||
|
|
||||||
|
public static string ToJson<T>(this IEnumerable<T> source, JsonSerializerSettings? options = null) where T : class, IAcSerializableToJson
|
||||||
|
=> AcJsonSerializer.Serialize(source);
|
||||||
|
// OLD: => ((object)source).ToJson(options);
|
||||||
|
|
||||||
public static T? JsonTo<T>(this string json, JsonSerializerSettings? options = null)
|
public static T? JsonTo<T>(this string json, JsonSerializerSettings? options = null)
|
||||||
{
|
{
|
||||||
if (json.StartsWith("\"") && json.EndsWith("\"")) json = Regex.Unescape(json).TrimStart('"').TrimEnd('"');
|
json = JsonUtilities.UnwrapJsonString(json);
|
||||||
|
|
||||||
|
// Use our high-performance custom deserializer for simple deserialization
|
||||||
|
// Fall back to Newtonsoft for complex scenarios (custom settings)
|
||||||
|
if (options == null && typeof(T).IsClass && !typeof(T).IsAbstract && typeof(T).GetConstructor(Type.EmptyTypes) != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (T?)AcJsonDeserializer.Deserialize(json, typeof(T));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback to Newtonsoft if custom deserializer fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<T>(json, options ?? Options);
|
return JsonConvert.DeserializeObject<T>(json, options ?? Options);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// OLD IMPLEMENTATION - Always Newtonsoft
|
||||||
|
// Uncomment below and comment out the above to rollback
|
||||||
|
// ========================================================================
|
||||||
|
// return JsonConvert.DeserializeObject<T>(json, options ?? Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static object? JsonTo(this string json, Type toType, JsonSerializerSettings? options = null)
|
public static object? JsonTo(this string json, Type toType, JsonSerializerSettings? options = null)
|
||||||
{
|
{
|
||||||
if (json.StartsWith("\"") && json.EndsWith("\"")) json = Regex.Unescape(json).TrimStart('"').TrimEnd('"');
|
json = JsonUtilities.UnwrapJsonString(json);
|
||||||
|
|
||||||
|
// Use our high-performance custom deserializer for simple deserialization
|
||||||
|
if (options == null && toType.IsClass && toType.GetConstructor(Type.EmptyTypes) != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return AcJsonDeserializer.Deserialize(json, toType);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback to Newtonsoft if custom deserializer fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject(json, toType, options ?? Options);
|
return JsonConvert.DeserializeObject(json, toType, options ?? Options);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// OLD IMPLEMENTATION - Always Newtonsoft
|
||||||
|
// Uncomment below and comment out the above to rollback
|
||||||
|
// ========================================================================
|
||||||
|
// return JsonConvert.DeserializeObject(json, toType, options ?? Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void JsonTo(this string json, object target, JsonSerializerSettings? options = null)
|
public static void JsonTo(this string json, object target, JsonSerializerSettings? options = null)
|
||||||
{
|
{
|
||||||
if (json.StartsWith("\"") && json.EndsWith("\"")) json = Regex.Unescape(json).TrimStart('"').TrimEnd('"');
|
json = JsonUtilities.UnwrapJsonString(json);
|
||||||
|
|
||||||
|
// For populate/merge, we still use Newtonsoft as it handles complex merge logic
|
||||||
|
// The AcJsonDeserializer.Populate can be used for simple cases
|
||||||
target.DeepPopulateWithMerge(json, options ?? Options);
|
target.DeepPopulateWithMerge(json, options ?? Options);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// ALTERNATIVE - Use AcJsonDeserializer for populate (simpler merge logic)
|
||||||
|
// Uncomment below for faster but simpler merge
|
||||||
|
// ========================================================================
|
||||||
|
// if (options == null)
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// AcJsonDeserializer.Populate(json, target);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// catch { }
|
||||||
|
// }
|
||||||
|
// target.DeepPopulateWithMerge(json, options ?? Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Using JSON
|
|
||||||
/// </summary>
|
|
||||||
[return: NotNullIfNotNull(nameof(src))]
|
[return: NotNullIfNotNull(nameof(src))]
|
||||||
public static TDestination? CloneTo<TDestination>(this object? src, JsonSerializerSettings? options = null) where TDestination : class
|
public static TDestination? CloneTo<TDestination>(this object? src, JsonSerializerSettings? options = null) where TDestination : class
|
||||||
=> src?.ToJson(options).JsonTo<TDestination>(options);
|
=> src?.ToJson(options).JsonTo<TDestination>(options);
|
||||||
|
|
||||||
/// <summary>
|
public static void CopyTo(this object? src, object target, JsonSerializerSettings? options = null)
|
||||||
/// Using JSON
|
=> src?.ToJson(options).JsonTo(target, options);
|
||||||
/// </summary>
|
|
||||||
public static void CopyTo(this object? src, object target, JsonSerializerSettings? options = null) => src?.ToJson(options).JsonTo(target, options);
|
|
||||||
|
|
||||||
public static byte[] ToMessagePack(this object message) => MessagePackSerializer.Serialize(message);
|
public static byte[] ToMessagePack(this object message) => MessagePackSerializer.Serialize(message);
|
||||||
public static byte[] ToMessagePack(this object message, MessagePackSerializerOptions options) => MessagePackSerializer.Serialize(message, options);
|
public static byte[] ToMessagePack(this object message, MessagePackSerializerOptions 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) => 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);
|
||||||
}
|
}
|
||||||
|
|
@ -163,55 +565,41 @@ public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContract
|
||||||
|
|
||||||
public void IgnoreProperty(Type type, params string[] jsonPropertyNames)
|
public void IgnoreProperty(Type type, params string[] jsonPropertyNames)
|
||||||
{
|
{
|
||||||
if (!_ignores.ContainsKey(type)) _ignores[type] = [];
|
if (!_ignores.TryGetValue(type, out var set)) { set = new HashSet<string>(StringComparer.Ordinal); _ignores[type] = set; }
|
||||||
|
foreach (var prop in jsonPropertyNames) set.Add(prop);
|
||||||
foreach (var prop in jsonPropertyNames) _ignores[type].Add(prop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void IncludesProperty(Type type, params string[] jsonPropertyNames)
|
public void IncludesProperty(Type type, params string[] jsonPropertyNames)
|
||||||
{
|
{
|
||||||
if (!_includes.ContainsKey(type)) _includes[type] = [];
|
if (!_includes.TryGetValue(type, out var set)) { set = new HashSet<string>(StringComparer.Ordinal); _includes[type] = set; }
|
||||||
|
foreach (var prop in jsonPropertyNames) set.Add(prop);
|
||||||
foreach (var prop in jsonPropertyNames) _includes[type].Add(prop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RenameProperty(Type type, string propertyName, string newJsonPropertyName)
|
public void RenameProperty(Type type, string propertyName, string newJsonPropertyName)
|
||||||
{
|
{
|
||||||
if (!_renames.ContainsKey(type)) _renames[type] = new Dictionary<string, string>();
|
if (!_renames.TryGetValue(type, out var dict)) { dict = new Dictionary<string, string>(StringComparer.Ordinal); _renames[type] = dict; }
|
||||||
|
dict[propertyName] = newJsonPropertyName;
|
||||||
_renames[type][propertyName] = newJsonPropertyName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
||||||
{
|
{
|
||||||
var property = base.CreateProperty(member, memberSerialization);
|
var property = base.CreateProperty(member, memberSerialization);
|
||||||
|
|
||||||
if (IsIgnored(property.DeclaringType, property.PropertyName) || !IsIncluded(property.DeclaringType, property.PropertyName))
|
if (IsIgnored(property.DeclaringType, property.PropertyName) || !IsIncluded(property.DeclaringType, property.PropertyName))
|
||||||
{
|
{ property.ShouldSerialize = _ => false; property.Ignored = true; }
|
||||||
property.ShouldSerialize = i => false;
|
if (IsRenamed(property.DeclaringType, property.PropertyName, out var newName)) property.PropertyName = newName;
|
||||||
property.Ignored = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsRenamed(property.DeclaringType, property.PropertyName, out var newJsonPropertyName))
|
|
||||||
property.PropertyName = newJsonPropertyName;
|
|
||||||
|
|
||||||
return property;
|
return property;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsIgnored(Type type, string jsonPropertyName)
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
{
|
private bool IsIgnored(Type? type, string? name) => type != null && name != null && _ignores.TryGetValue(type, out var set) && set.Contains(name);
|
||||||
return _ignores.ContainsKey(type) && _ignores[type].Contains(jsonPropertyName);
|
|
||||||
}
|
|
||||||
private bool IsIncluded(Type type, string jsonPropertyName)
|
|
||||||
{
|
|
||||||
return _includes.Count == 0 || (_includes.ContainsKey(type) && _includes[type].Contains(jsonPropertyName));
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsRenamed(Type type, string jsonPropertyName, out string? newJsonPropertyName)
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
{
|
private bool IsIncluded(Type? type, string? name) => _includes.Count == 0 || (type != null && name != null && _includes.TryGetValue(type, out var set) && set.Contains(name));
|
||||||
if (_renames.TryGetValue(type, out var renames) && renames.TryGetValue(jsonPropertyName, out newJsonPropertyName)) return true;
|
|
||||||
|
|
||||||
newJsonPropertyName = null;
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
return false;
|
private bool IsRenamed(Type? type, string? name, out string? newName)
|
||||||
|
{
|
||||||
|
if (type != null && name != null && _renames.TryGetValue(type, out var renames) && renames.TryGetValue(name, out newName)) return true;
|
||||||
|
newName = null; return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using AyCode.Core.Extensions;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace AyCode.Core.Helpers
|
namespace AyCode.Core.Helpers
|
||||||
{
|
{
|
||||||
|
|
@ -11,6 +13,39 @@ namespace AyCode.Core.Helpers
|
||||||
public void Replace(IEnumerable other);
|
public void Replace(IEnumerable other);
|
||||||
public void RemoveRange(IEnumerable other);
|
public void RemoveRange(IEnumerable other);
|
||||||
public void Synchronize(NotifyCollectionChangedEventArgs args);
|
public void Synchronize(NotifyCollectionChangedEventArgs args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates/merges data from object source while suppressing per-item change events.
|
||||||
|
/// Fires a single Reset event at the end.
|
||||||
|
/// </summary>
|
||||||
|
void PopulateFrom(object source);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates/merges data from json while suppressing per-item change events.
|
||||||
|
/// Fires a single Reset event at the end.
|
||||||
|
/// </summary>
|
||||||
|
void PopulateFromJson(string json, bool clearAll = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begins a batch update operation. All notifications are suppressed until EndUpdate is called.
|
||||||
|
/// Supports nested calls - only the outermost EndUpdate triggers the notification.
|
||||||
|
/// </summary>
|
||||||
|
public void BeginUpdate();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends a batch update operation. Triggers a single Reset notification if this is the outermost call.
|
||||||
|
/// </summary>
|
||||||
|
public void EndUpdate();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces a Reset notification to refresh bound UI controls.
|
||||||
|
/// </summary>
|
||||||
|
public void NotifyReset();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if currently in a batch update operation.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsUpdating { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IAcObservableCollection<T> : IAcObservableCollection
|
public interface IAcObservableCollection<T> : IAcObservableCollection
|
||||||
|
|
@ -20,81 +55,298 @@ namespace AyCode.Core.Helpers
|
||||||
public void SortAndReplace(IEnumerable<T> other, IComparer<T> comparer);
|
public void SortAndReplace(IEnumerable<T> other, IComparer<T> comparer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread-safe ObservableCollection with batch update support.
|
||||||
|
/// All public methods are synchronized using a lock.
|
||||||
|
/// </summary>
|
||||||
public class AcObservableCollection<T> : ObservableCollection<T>, IAcObservableCollection<T>
|
public class AcObservableCollection<T> : ObservableCollection<T>, IAcObservableCollection<T>
|
||||||
{
|
{
|
||||||
private bool _suppressChangedEvent;
|
private readonly object _syncRoot = new();
|
||||||
|
private int _updateCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if currently in a batch update operation.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsUpdating
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
return _updateCount > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the synchronization object for external locking scenarios.
|
||||||
|
/// </summary>
|
||||||
|
public object SyncRoot => _syncRoot;
|
||||||
|
|
||||||
public AcObservableCollection() : base()
|
public AcObservableCollection() : base()
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public AcObservableCollection(List<T> list) : base(list)
|
public AcObservableCollection(List<T> list) : base(list)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public AcObservableCollection(IEnumerable<T> collection) : base(collection)
|
public AcObservableCollection(IEnumerable<T> collection) : base(collection)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
public void BeginUpdate()
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_updateCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndUpdate()
|
||||||
|
{
|
||||||
|
bool shouldNotify;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
if (_updateCount <= 0) return;
|
||||||
|
_updateCount--;
|
||||||
|
shouldNotify = _updateCount == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldNotify) NotifyReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyReset()
|
||||||
|
{
|
||||||
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void Add(T item)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
base.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new bool Remove(T item)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
return base.Remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void Insert(int index, T item)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
base.Insert(index, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void RemoveAt(int index)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
base.RemoveAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void Clear()
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
base.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void Move(int oldIndex, int newIndex)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
base.Move(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new T this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
return base[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
base[index] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
return base.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new bool Contains(T item)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
return base.Contains(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new int IndexOf(T item)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
return base.IndexOf(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void CopyTo(T[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
base.CopyTo(array, arrayIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a snapshot copy of the collection for safe enumeration.
|
||||||
|
/// </summary>
|
||||||
|
public List<T> ToList()
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
return [..this.Items];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Replace(IEnumerable<T> other)
|
public void Replace(IEnumerable<T> other)
|
||||||
{
|
{
|
||||||
_suppressChangedEvent = true;
|
BeginUpdate();
|
||||||
|
try
|
||||||
Clear();
|
{
|
||||||
AddRange(other);
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
base.Clear();
|
||||||
|
foreach (var item in other) base.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Replace(IEnumerable other)
|
public void Replace(IEnumerable other)
|
||||||
{
|
{
|
||||||
_suppressChangedEvent = true;
|
BeginUpdate();
|
||||||
|
try
|
||||||
Clear();
|
{
|
||||||
foreach (T item in other) Add(item);
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
_suppressChangedEvent = false;
|
base.Clear();
|
||||||
|
foreach (T item in other) base.Add(item);
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
}
|
||||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddRange(IEnumerable other)
|
public void AddRange(IEnumerable other)
|
||||||
{
|
{
|
||||||
_suppressChangedEvent = true;
|
BeginUpdate();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
foreach (var item in other)
|
foreach (var item in other)
|
||||||
{
|
{
|
||||||
if (item is T tItem) Add(tItem);
|
if (item is T tItem) base.Add(tItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
_suppressChangedEvent = false;
|
|
||||||
|
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
|
||||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveRange(IEnumerable other)
|
public void RemoveRange(IEnumerable other)
|
||||||
{
|
{
|
||||||
_suppressChangedEvent = true;
|
BeginUpdate();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
foreach (var item in other)
|
foreach (var item in other)
|
||||||
{
|
{
|
||||||
if (item is T tItem) Remove(tItem);
|
if (item is T tItem) base.Remove(tItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_suppressChangedEvent = false;
|
public void PopulateFrom(object source)
|
||||||
|
{
|
||||||
|
switch (source)
|
||||||
|
{
|
||||||
|
case IEnumerable<T> typedSource:
|
||||||
|
Replace(typedSource);
|
||||||
|
break;
|
||||||
|
case IEnumerable enumerable:
|
||||||
|
Replace(enumerable);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
public void PopulateFromJson(string json, bool clearAll = false)
|
||||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
|
{
|
||||||
|
BeginUpdate();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
if (clearAll) base.Clear();
|
||||||
|
json.JsonTo(this.Items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SortAndReplace(IEnumerable<T> other, IComparer<T> comparer)
|
public void SortAndReplace(IEnumerable<T> other, IComparer<T> comparer)
|
||||||
{
|
{
|
||||||
List<T> values = new(other);
|
var values = new List<T>(other);
|
||||||
|
|
||||||
values.Sort(comparer);
|
values.Sort(comparer);
|
||||||
Replace(values);
|
Replace(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Sort(IComparer<T> comparer)
|
public void Sort(IComparer<T> comparer)
|
||||||
{
|
{
|
||||||
List<T> values = new(this);
|
List<T> values;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
values = new List<T>(this.Items);
|
||||||
|
}
|
||||||
values.Sort(comparer);
|
values.Sort(comparer);
|
||||||
Replace(values);
|
Replace(values);
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +374,7 @@ namespace AyCode.Core.Helpers
|
||||||
|
|
||||||
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
|
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_suppressChangedEvent)
|
if (IsUpdating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
base.OnPropertyChanged(e);
|
base.OnPropertyChanged(e);
|
||||||
|
|
@ -130,31 +382,10 @@ namespace AyCode.Core.Helpers
|
||||||
|
|
||||||
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_suppressChangedEvent)
|
if (IsUpdating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
base.OnCollectionChanged(e);
|
base.OnCollectionChanged(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
//protected override void ClearItems()
|
|
||||||
//{
|
|
||||||
// base.ClearItems();
|
|
||||||
//}
|
|
||||||
|
|
||||||
//protected override void InsertItem(int index, T item)
|
|
||||||
//{
|
|
||||||
// base.InsertItem(index, item);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//protected override void MoveItem(int oldIndex, int newIndex)
|
|
||||||
//{
|
|
||||||
// base.MoveItem(oldIndex, newIndex);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//public override event NotifyCollectionChangedEventHandler? CollectionChanged
|
|
||||||
//{
|
|
||||||
// add => base.CollectionChanged += value;
|
|
||||||
// remove => base.CollectionChanged -= value;
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -133,6 +133,9 @@ public sealed class SignalResponseJsonMessage : ISignalResponseMessage<string>
|
||||||
|
|
||||||
[Key(2)] public string? ResponseData { get; set; } = null;
|
[Key(2)] public string? ResponseData { get; set; } = null;
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public string? ResponseDataJson => ResponseData;
|
||||||
|
|
||||||
public SignalResponseJsonMessage(){}
|
public SignalResponseJsonMessage(){}
|
||||||
|
|
||||||
public SignalResponseJsonMessage(int messageTag, SignalResponseStatus status)
|
public SignalResponseJsonMessage(int messageTag, SignalResponseStatus status)
|
||||||
|
|
@ -154,29 +157,91 @@ public sealed class SignalResponseJsonMessage : ISignalResponseMessage<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signal response message with lazy deserialization support.
|
||||||
|
/// ResponseData is only deserialized on first access and cached.
|
||||||
|
/// Use ResponseDataJson for direct JSON access without deserialization.
|
||||||
|
/// </summary>
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public sealed class SignalResponseMessage<TResponseData>(int messageTag, SignalResponseStatus status, TResponseData? responseData) : ISignalResponseMessage<TResponseData>
|
public sealed class SignalResponseMessage<TResponseData> : ISignalResponseMessage<TResponseData>
|
||||||
{
|
{
|
||||||
[Key(0)] public int MessageTag { get; set; }
|
[IgnoreMember]
|
||||||
[Key(1)] public SignalResponseStatus Status { get; set; } = status;
|
private TResponseData? _responseData;
|
||||||
[Key(2)] public TResponseData? ResponseData { get; set; } = responseData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SignalResponseStatusMessage(SignalResponseStatus status) : ISignalRMessage
|
[IgnoreMember]
|
||||||
{
|
private bool _isDeserialized;
|
||||||
public SignalResponseStatus Status { get; set; } = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
//[MessagePackObject]
|
[Key(0)]
|
||||||
//public sealed class SignalResponseMessage(SignalResponseStatus status) : ISignalResponseMessage
|
public int MessageTag { get; set; }
|
||||||
//{
|
|
||||||
// [Key(0)]
|
[Key(1)]
|
||||||
// public SignalResponseStatus Status { get; set; } = status;
|
public SignalResponseStatus Status { get; set; }
|
||||||
//}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raw JSON string. Use this for direct JSON access without triggering deserialization.
|
||||||
|
/// </summary>
|
||||||
|
[Key(2)]
|
||||||
|
public string? ResponseDataJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialized response data. Lazy-loaded on first access.
|
||||||
|
/// </summary>
|
||||||
|
[IgnoreMember]
|
||||||
|
public TResponseData? ResponseData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_isDeserialized)
|
||||||
|
{
|
||||||
|
_responseData = ResponseDataJson != null
|
||||||
|
? ResponseDataJson.JsonTo<TResponseData>()
|
||||||
|
: default;
|
||||||
|
_isDeserialized = true;
|
||||||
|
}
|
||||||
|
return _responseData;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_responseData = value;
|
||||||
|
_isDeserialized = true;
|
||||||
|
ResponseDataJson = value?.ToJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalResponseMessage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalResponseMessage(int messageTag, SignalResponseStatus status)
|
||||||
|
{
|
||||||
|
MessageTag = messageTag;
|
||||||
|
Status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalResponseMessage(int messageTag, SignalResponseStatus status, TResponseData? responseData)
|
||||||
|
: this(messageTag, status)
|
||||||
|
{
|
||||||
|
ResponseData = responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalResponseMessage(int messageTag, SignalResponseStatus status, string? responseDataJson)
|
||||||
|
: this(messageTag, status)
|
||||||
|
{
|
||||||
|
ResponseDataJson = responseDataJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public interface ISignalResponseMessage<TResponseData> : ISignalResponseMessage
|
public interface ISignalResponseMessage<TResponseData> : ISignalResponseMessage
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialized response data. May trigger lazy deserialization.
|
||||||
|
/// </summary>
|
||||||
TResponseData? ResponseData { get; set; }
|
TResponseData? ResponseData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raw JSON string for direct access without deserialization.
|
||||||
|
/// </summary>
|
||||||
|
string? ResponseDataJson { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ISignalResponseMessage : ISignalRMessage
|
public interface ISignalResponseMessage : ISignalRMessage
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.3.36726.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AyCode.Core\AyCode.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Results\" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
|
||||||
|
namespace BenchmarkSuite1
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
// Quick size comparison test
|
||||||
|
if (args.Length > 0 && args[0] == "--sizes")
|
||||||
|
{
|
||||||
|
RunSizeComparison();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = BenchmarkRunner.Run(typeof(Program).Assembly);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void RunSizeComparison()
|
||||||
|
{
|
||||||
|
Console.WriteLine("=== JSON Size Comparison ===\n");
|
||||||
|
|
||||||
|
var benchmark = new AyCode.Core.Benchmarks.SerializationBenchmarks();
|
||||||
|
|
||||||
|
// Manually invoke setup
|
||||||
|
var setupMethod = typeof(AyCode.Core.Benchmarks.SerializationBenchmarks)
|
||||||
|
.GetMethod("Setup");
|
||||||
|
setupMethod?.Invoke(benchmark, null);
|
||||||
|
|
||||||
|
// Get JSON sizes via reflection (private fields)
|
||||||
|
var newtonsoftJson = typeof(AyCode.Core.Benchmarks.SerializationBenchmarks)
|
||||||
|
.GetField("_newtonsoftJson", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||||
|
?.GetValue(benchmark) as string;
|
||||||
|
|
||||||
|
var ayCodeJson = typeof(AyCode.Core.Benchmarks.SerializationBenchmarks)
|
||||||
|
.GetField("_ayCodeJson", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||||
|
?.GetValue(benchmark) as string;
|
||||||
|
|
||||||
|
if (newtonsoftJson != null && ayCodeJson != null)
|
||||||
|
{
|
||||||
|
var newtonsoftBytes = System.Text.Encoding.UTF8.GetByteCount(newtonsoftJson);
|
||||||
|
var ayCodeBytes = System.Text.Encoding.UTF8.GetByteCount(ayCodeJson);
|
||||||
|
|
||||||
|
Console.WriteLine($"Newtonsoft JSON (no refs):");
|
||||||
|
Console.WriteLine($" - Characters: {newtonsoftJson.Length:N0}");
|
||||||
|
Console.WriteLine($" - Bytes: {newtonsoftBytes:N0} ({newtonsoftBytes / 1024.0 / 1024.0:F2} MB)");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine($"AyCode JSON (with refs):");
|
||||||
|
Console.WriteLine($" - Characters: {ayCodeJson.Length:N0}");
|
||||||
|
Console.WriteLine($" - Bytes: {ayCodeBytes:N0} ({ayCodeBytes / 1024.0 / 1024.0:F2} MB)");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
var reduction = (1.0 - (double)ayCodeBytes / newtonsoftBytes) * 100;
|
||||||
|
Console.WriteLine($"Size Reduction: {reduction:F1}%");
|
||||||
|
Console.WriteLine($"AyCode is {(reduction > 0 ? "smaller" : "larger")} by {Math.Abs(newtonsoftBytes - ayCodeBytes):N0} bytes");
|
||||||
|
|
||||||
|
// Count $ref occurrences
|
||||||
|
var refCount = System.Text.RegularExpressions.Regex.Matches(ayCodeJson, @"\$ref").Count;
|
||||||
|
var idCount = System.Text.RegularExpressions.Regex.Matches(ayCodeJson, @"\$id").Count;
|
||||||
|
Console.WriteLine($"\nAyCode $id count: {idCount}");
|
||||||
|
Console.WriteLine($"AyCode $ref count: {refCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,516 @@
|
||||||
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Interfaces;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Benchmarks;
|
||||||
|
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class SerializationBenchmarks
|
||||||
|
{
|
||||||
|
// Complex graph with 7 levels, ~1500 objects, cross-references
|
||||||
|
private Level1_Company _complexGraph = null!;
|
||||||
|
|
||||||
|
// Pre-serialized JSON for deserialization benchmarks
|
||||||
|
private string _newtonsoftJson = null!;
|
||||||
|
private string _ayCodeJson = null!;
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
private JsonSerializerSettings _newtonsoftNoRefSettings = null!;
|
||||||
|
private JsonSerializerSettings _ayCodeSettings = null!;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
// Newtonsoft WITHOUT reference handling (baseline)
|
||||||
|
_newtonsoftNoRefSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
DefaultValueHandling = DefaultValueHandling.Ignore, // Fair comparison - also skip defaults
|
||||||
|
Formatting = Formatting.None
|
||||||
|
};
|
||||||
|
|
||||||
|
// AyCode WITH reference handling (our optimized solution)
|
||||||
|
_ayCodeSettings = SerializeObjectExtensions.Options;
|
||||||
|
|
||||||
|
// Create complex 7-level graph with ~1500 objects and cross-references
|
||||||
|
_complexGraph = CreateComplexGraph();
|
||||||
|
|
||||||
|
// Pre-serialize for deserialization benchmarks
|
||||||
|
_newtonsoftJson = JsonConvert.SerializeObject(_complexGraph, _newtonsoftNoRefSettings);
|
||||||
|
_ayCodeJson = _complexGraph.ToJson(_ayCodeSettings);
|
||||||
|
|
||||||
|
// Output sizes for comparison
|
||||||
|
var newtonsoftBytes = Encoding.UTF8.GetByteCount(_newtonsoftJson);
|
||||||
|
var ayCodeBytes = Encoding.UTF8.GetByteCount(_ayCodeJson);
|
||||||
|
|
||||||
|
Console.WriteLine("=== JSON Size Comparison ===");
|
||||||
|
Console.WriteLine($"Newtonsoft (skip defaults): {_newtonsoftJson.Length:N0} chars ({newtonsoftBytes:N0} bytes, {newtonsoftBytes / 1024.0:F1} KB)");
|
||||||
|
Console.WriteLine($"AcJsonSerializer (refs+skip): {_ayCodeJson.Length:N0} chars ({ayCodeBytes:N0} bytes, {ayCodeBytes / 1024.0:F1} KB)");
|
||||||
|
Console.WriteLine($"Size reduction: {(1.0 - (double)ayCodeBytes / newtonsoftBytes) * 100:F1}%");
|
||||||
|
Console.WriteLine($"Bytes saved: {newtonsoftBytes - ayCodeBytes:N0}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Serialization Benchmarks
|
||||||
|
|
||||||
|
[Benchmark(Description = "Newtonsoft (no refs)")]
|
||||||
|
[BenchmarkCategory("Serialize")]
|
||||||
|
public string Serialize_Newtonsoft_NoRefs()
|
||||||
|
=> JsonConvert.SerializeObject(_complexGraph, _newtonsoftNoRefSettings);
|
||||||
|
|
||||||
|
[Benchmark(Description = "AyCode (with refs)")]
|
||||||
|
[BenchmarkCategory("Serialize")]
|
||||||
|
public string Serialize_AyCode_WithRefs()
|
||||||
|
=> _complexGraph.ToJson(_ayCodeSettings);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Deserialization Benchmarks
|
||||||
|
|
||||||
|
[Benchmark(Description = "Newtonsoft (no refs)")]
|
||||||
|
[BenchmarkCategory("Deserialize")]
|
||||||
|
public Level1_Company? Deserialize_Newtonsoft_NoRefs()
|
||||||
|
=> JsonConvert.DeserializeObject<Level1_Company>(_newtonsoftJson, _newtonsoftNoRefSettings);
|
||||||
|
|
||||||
|
[Benchmark(Description = "AyCode (with refs)")]
|
||||||
|
[BenchmarkCategory("Deserialize")]
|
||||||
|
public Level1_Company? Deserialize_AyCode_WithRefs()
|
||||||
|
=> _ayCodeJson.JsonTo<Level1_Company>(_ayCodeSettings);
|
||||||
|
|
||||||
|
[Benchmark(Description = "AcJsonDeserializer (custom)")]
|
||||||
|
[BenchmarkCategory("Deserialize")]
|
||||||
|
public Level1_Company? Deserialize_AcJsonDeserializer()
|
||||||
|
=> AcJsonDeserializer.Deserialize<Level1_Company>(_ayCodeJson);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region JSON Size Comparison (not timed, just for reporting)
|
||||||
|
|
||||||
|
[Benchmark(Description = "JSON Size - Newtonsoft")]
|
||||||
|
[BenchmarkCategory("Size")]
|
||||||
|
public int JsonSize_Newtonsoft() => _newtonsoftJson.Length;
|
||||||
|
|
||||||
|
[Benchmark(Description = "JSON Size - AyCode")]
|
||||||
|
[BenchmarkCategory("Size")]
|
||||||
|
public int JsonSize_AyCode() => _ayCodeJson.Length;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Complex Graph Factory - 7 Levels, ~1500 objects, Cross-references
|
||||||
|
|
||||||
|
private static int _idCounter = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a 7-level deep graph with approximately 1500 objects and cross-references.
|
||||||
|
/// Structure: Company -> Departments -> Teams -> Projects -> Tasks -> SubTasks -> Comments
|
||||||
|
/// Each object has 8-15 properties of various types.
|
||||||
|
/// </summary>
|
||||||
|
private static Level1_Company CreateComplexGraph()
|
||||||
|
{
|
||||||
|
_idCounter = 1;
|
||||||
|
|
||||||
|
// Shared references (cross-references across the graph)
|
||||||
|
var sharedTags = Enumerable.Range(1, 10)
|
||||||
|
.Select(i => new SharedTag
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Name = $"Tag-{i}",
|
||||||
|
Color = $"#{i:X2}{i * 10:X2}{i * 20:X2}",
|
||||||
|
Priority = i % 5,
|
||||||
|
IsActive = i % 2 == 0,
|
||||||
|
CreatedAt = DateTime.UtcNow.AddDays(-i * 10),
|
||||||
|
Metadata = $"Metadata for tag {i}"
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var sharedCategories = Enumerable.Range(1, 5)
|
||||||
|
.Select(i => new SharedCategory
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Name = $"Category-{i}",
|
||||||
|
Description = $"Description for category {i} with some extra text to make it longer",
|
||||||
|
SortOrder = i * 100,
|
||||||
|
IconUrl = $"https://icons.example.com/cat-{i}.png",
|
||||||
|
IsDefault = i == 1,
|
||||||
|
ParentCategoryId = i > 1 ? i - 1 : null,
|
||||||
|
CreatedAt = DateTime.UtcNow.AddMonths(-i),
|
||||||
|
UpdatedAt = DateTime.UtcNow.AddDays(-i)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var sharedUser = new SharedUser
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Username = "admin",
|
||||||
|
Email = "admin@company.com",
|
||||||
|
FirstName = "System",
|
||||||
|
LastName = "Administrator",
|
||||||
|
PhoneNumber = "+1-555-0100",
|
||||||
|
IsActive = true,
|
||||||
|
Role = UserRole.Admin,
|
||||||
|
LastLoginAt = DateTime.UtcNow.AddHours(-1),
|
||||||
|
CreatedAt = DateTime.UtcNow.AddYears(-2),
|
||||||
|
Preferences = new UserPreferences
|
||||||
|
{
|
||||||
|
Theme = "dark",
|
||||||
|
Language = "en-US",
|
||||||
|
NotificationsEnabled = true,
|
||||||
|
EmailDigestFrequency = "daily"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Level 1: Company (1 object)
|
||||||
|
var company = new Level1_Company
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Name = "TechCorp International",
|
||||||
|
LegalName = "TechCorp International Holdings Ltd.",
|
||||||
|
TaxId = "TC-123456789",
|
||||||
|
FoundedDate = new DateTime(2010, 3, 15),
|
||||||
|
EmployeeCount = 1500,
|
||||||
|
AnnualRevenue = 125_000_000.50m,
|
||||||
|
IsPubliclyTraded = true,
|
||||||
|
StockSymbol = "TECH",
|
||||||
|
HeadquartersAddress = "123 Innovation Drive, Tech City, TC 12345",
|
||||||
|
Website = "https://www.techcorp.example.com",
|
||||||
|
PrimaryContact = sharedUser,
|
||||||
|
MainCategory = sharedCategories[0],
|
||||||
|
Tags = [sharedTags[0], sharedTags[1], sharedTags[2]],
|
||||||
|
CreatedAt = DateTime.UtcNow.AddYears(-5),
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
// Level 2: Departments (5 objects)
|
||||||
|
company.Departments = Enumerable.Range(1, 5).Select(deptIdx => new Level2_Department
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Name = $"Department-{deptIdx}",
|
||||||
|
Code = $"DEPT-{deptIdx:D3}",
|
||||||
|
Description = $"This is department {deptIdx} responsible for various operations and strategic initiatives",
|
||||||
|
Budget = 1_000_000m + (deptIdx * 250_000m),
|
||||||
|
HeadCount = 50 + (deptIdx * 20),
|
||||||
|
Location = $"Building {(char)('A' + deptIdx - 1)}, Floor {deptIdx}",
|
||||||
|
CostCenter = $"CC-{1000 + deptIdx}",
|
||||||
|
IsActive = true,
|
||||||
|
Manager = sharedUser, // Cross-reference
|
||||||
|
Category = sharedCategories[deptIdx % sharedCategories.Count], // Cross-reference
|
||||||
|
Tags = [sharedTags[deptIdx % sharedTags.Count], sharedTags[(deptIdx + 1) % sharedTags.Count]], // Cross-reference
|
||||||
|
EstablishedDate = DateTime.UtcNow.AddYears(-4).AddMonths(deptIdx),
|
||||||
|
CreatedAt = DateTime.UtcNow.AddYears(-4),
|
||||||
|
UpdatedAt = DateTime.UtcNow.AddMonths(-deptIdx),
|
||||||
|
// Level 3: Teams (6 per department = 30 total)
|
||||||
|
Teams = Enumerable.Range(1, 6).Select(teamIdx => new Level3_Team
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Name = $"Team-{deptIdx}-{teamIdx}",
|
||||||
|
Acronym = $"T{deptIdx}{teamIdx}",
|
||||||
|
Description = $"Team {teamIdx} in department {deptIdx}, focused on delivering excellence",
|
||||||
|
MemberCount = 5 + (teamIdx * 2),
|
||||||
|
Capacity = 10 + (teamIdx * 2),
|
||||||
|
Utilization = 0.65 + (teamIdx * 0.05),
|
||||||
|
SprintLength = 14,
|
||||||
|
VelocityAverage = 42.5 + teamIdx,
|
||||||
|
IsRemote = teamIdx % 3 == 0,
|
||||||
|
Timezone = teamIdx % 2 == 0 ? "UTC" : "America/New_York",
|
||||||
|
SlackChannel = $"#team-{deptIdx}-{teamIdx}",
|
||||||
|
TeamLead = sharedUser, // Cross-reference
|
||||||
|
PrimaryTag = sharedTags[(deptIdx + teamIdx) % sharedTags.Count], // Cross-reference
|
||||||
|
CreatedAt = DateTime.UtcNow.AddYears(-3).AddMonths(teamIdx),
|
||||||
|
UpdatedAt = DateTime.UtcNow.AddDays(-teamIdx * 7),
|
||||||
|
// Level 4: Projects (4 per team = 120 total)
|
||||||
|
Projects = Enumerable.Range(1, 4).Select(projIdx => new Level4_Project
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Name = $"Project-{deptIdx}-{teamIdx}-{projIdx}",
|
||||||
|
Code = $"PRJ-{deptIdx}{teamIdx}{projIdx:D2}",
|
||||||
|
Description = $"Project {projIdx} for team {teamIdx}, delivering key business value and innovation",
|
||||||
|
Status = (ProjectStatus)(projIdx % 4),
|
||||||
|
Priority = (Priority)(projIdx % 3),
|
||||||
|
Budget = 50_000m + (projIdx * 15_000m),
|
||||||
|
SpentAmount = 25_000m + (projIdx * 5_000m),
|
||||||
|
ProgressPercent = 0.1 + (projIdx * 0.2),
|
||||||
|
StartDate = DateTime.UtcNow.AddMonths(-projIdx * 2),
|
||||||
|
DueDate = DateTime.UtcNow.AddMonths(projIdx),
|
||||||
|
CompletedDate = projIdx == 4 ? DateTime.UtcNow.AddDays(-10) : null,
|
||||||
|
EstimatedHours = 200 + (projIdx * 50),
|
||||||
|
ActualHours = 150 + (projIdx * 40),
|
||||||
|
RiskLevel = projIdx % 3,
|
||||||
|
Owner = sharedUser, // Cross-reference
|
||||||
|
Category = sharedCategories[projIdx % sharedCategories.Count], // Cross-reference
|
||||||
|
Tags = [sharedTags[projIdx % sharedTags.Count]], // Cross-reference
|
||||||
|
CreatedAt = DateTime.UtcNow.AddMonths(-projIdx * 3),
|
||||||
|
UpdatedAt = DateTime.UtcNow.AddDays(-projIdx),
|
||||||
|
// Level 5: Tasks (5 per project = 600 total)
|
||||||
|
Tasks = Enumerable.Range(1, 5).Select(taskIdx => new Level5_Task
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Title = $"Task-{deptIdx}-{teamIdx}-{projIdx}-{taskIdx}",
|
||||||
|
Description = $"Detailed task description for task {taskIdx} in project {projIdx}. This includes requirements and acceptance criteria.",
|
||||||
|
Status = (TaskStatus)(taskIdx % 5),
|
||||||
|
Priority = (Priority)(taskIdx % 3),
|
||||||
|
Type = (TaskType)(taskIdx % 4),
|
||||||
|
StoryPoints = taskIdx * 2,
|
||||||
|
EstimatedHours = 4 + taskIdx * 2,
|
||||||
|
ActualHours = 3 + taskIdx * 1.5,
|
||||||
|
DueDate = DateTime.UtcNow.AddDays(taskIdx * 3),
|
||||||
|
CompletedDate = taskIdx <= 2 ? DateTime.UtcNow.AddDays(-taskIdx) : null,
|
||||||
|
IsBlocked = taskIdx == 3,
|
||||||
|
BlockedReason = taskIdx == 3 ? "Waiting for external dependency" : null,
|
||||||
|
Assignee = sharedUser, // Cross-reference
|
||||||
|
Reporter = sharedUser, // Cross-reference
|
||||||
|
Labels = [sharedTags[taskIdx % sharedTags.Count]], // Cross-reference
|
||||||
|
CreatedAt = DateTime.UtcNow.AddDays(-taskIdx * 5),
|
||||||
|
UpdatedAt = DateTime.UtcNow.AddHours(-taskIdx),
|
||||||
|
// Level 6: SubTasks (3 per task = 1800 total -> we'll limit to keep ~1500)
|
||||||
|
SubTasks = Enumerable.Range(1, 2).Select(subIdx => new Level6_SubTask
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Title = $"SubTask-{taskIdx}-{subIdx}",
|
||||||
|
Description = $"Sub-task {subIdx} details for completing parent task {taskIdx}",
|
||||||
|
Status = (TaskStatus)(subIdx % 5),
|
||||||
|
EstimatedMinutes = 30 + subIdx * 15,
|
||||||
|
ActualMinutes = 25 + subIdx * 12,
|
||||||
|
IsCompleted = subIdx == 1,
|
||||||
|
CompletedAt = subIdx == 1 ? DateTime.UtcNow.AddHours(-subIdx * 2) : null,
|
||||||
|
Assignee = sharedUser, // Cross-reference
|
||||||
|
CreatedAt = DateTime.UtcNow.AddDays(-subIdx),
|
||||||
|
UpdatedAt = DateTime.UtcNow.AddMinutes(-subIdx * 30),
|
||||||
|
// Level 7: Comments (2 per subtask = 2400 total -> limiting)
|
||||||
|
Comments = Enumerable.Range(1, 1).Select(comIdx => new Level7_Comment
|
||||||
|
{
|
||||||
|
Id = _idCounter++,
|
||||||
|
Text = $"Comment {comIdx} on subtask {subIdx}: This is a detailed comment with feedback and suggestions for improvement.",
|
||||||
|
Author = sharedUser, // Cross-reference
|
||||||
|
IsEdited = comIdx % 2 == 0,
|
||||||
|
EditedAt = comIdx % 2 == 0 ? DateTime.UtcNow.AddHours(-1) : null,
|
||||||
|
LikeCount = comIdx * 3,
|
||||||
|
ReplyCount = comIdx,
|
||||||
|
CreatedAt = DateTime.UtcNow.AddHours(-comIdx * 4),
|
||||||
|
MentionedTags = [sharedTags[comIdx % sharedTags.Count]] // Cross-reference
|
||||||
|
}).ToList()
|
||||||
|
}).ToList()
|
||||||
|
}).ToList()
|
||||||
|
}).ToList()
|
||||||
|
}).ToList()
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 7-Level Deep DTOs with 8-15 Properties Each
|
||||||
|
|
||||||
|
// Shared cross-reference types
|
||||||
|
public class SharedTag : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Color { get; set; } = "";
|
||||||
|
public int Priority { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string Metadata { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SharedCategory : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public string IconUrl { get; set; } = "";
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
public int? ParentCategoryId { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SharedUser : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Username { get; set; } = "";
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
public string FirstName { get; set; } = "";
|
||||||
|
public string LastName { get; set; } = "";
|
||||||
|
public string PhoneNumber { get; set; } = "";
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public UserRole Role { get; set; }
|
||||||
|
public DateTime? LastLoginAt { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public UserPreferences? Preferences { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserPreferences
|
||||||
|
{
|
||||||
|
public string Theme { get; set; } = "";
|
||||||
|
public string Language { get; set; } = "";
|
||||||
|
public bool NotificationsEnabled { get; set; }
|
||||||
|
public string EmailDigestFrequency { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UserRole { User, Manager, Admin }
|
||||||
|
public enum ProjectStatus { Planning, Active, OnHold, Completed }
|
||||||
|
public enum TaskStatus { Backlog, Todo, InProgress, Review, Done }
|
||||||
|
public enum TaskType { Feature, Bug, Improvement, Task }
|
||||||
|
public enum Priority { Low, Medium, High }
|
||||||
|
|
||||||
|
// Level 1: Company (15 properties)
|
||||||
|
public class Level1_Company : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string LegalName { get; set; } = "";
|
||||||
|
public string TaxId { get; set; } = "";
|
||||||
|
public DateTime FoundedDate { get; set; }
|
||||||
|
public int EmployeeCount { get; set; }
|
||||||
|
public decimal AnnualRevenue { get; set; }
|
||||||
|
public bool IsPubliclyTraded { get; set; }
|
||||||
|
public string? StockSymbol { get; set; }
|
||||||
|
public string HeadquartersAddress { get; set; } = "";
|
||||||
|
public string Website { get; set; } = "";
|
||||||
|
public SharedUser? PrimaryContact { get; set; } // Cross-ref
|
||||||
|
public SharedCategory? MainCategory { get; set; } // Cross-ref
|
||||||
|
public List<SharedTag> Tags { get; set; } = []; // Cross-ref
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public List<Level2_Department> Departments { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 2: Department (15 properties)
|
||||||
|
public class Level2_Department : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Code { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public decimal Budget { get; set; }
|
||||||
|
public int HeadCount { get; set; }
|
||||||
|
public string Location { get; set; } = "";
|
||||||
|
public string CostCenter { get; set; } = "";
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public SharedUser? Manager { get; set; } // Cross-ref
|
||||||
|
public SharedCategory? Category { get; set; } // Cross-ref
|
||||||
|
public List<SharedTag> Tags { get; set; } = []; // Cross-ref
|
||||||
|
public DateTime EstablishedDate { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public List<Level3_Team> Teams { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 3: Team (15 properties)
|
||||||
|
public class Level3_Team : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Acronym { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public int MemberCount { get; set; }
|
||||||
|
public int Capacity { get; set; }
|
||||||
|
public double Utilization { get; set; }
|
||||||
|
public int SprintLength { get; set; }
|
||||||
|
public double VelocityAverage { get; set; }
|
||||||
|
public bool IsRemote { get; set; }
|
||||||
|
public string Timezone { get; set; } = "";
|
||||||
|
public string SlackChannel { get; set; } = "";
|
||||||
|
public SharedUser? TeamLead { get; set; } // Cross-ref
|
||||||
|
public SharedTag? PrimaryTag { get; set; } // Cross-ref
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public List<Level4_Project> Projects { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 4: Project (18 properties)
|
||||||
|
public class Level4_Project : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Code { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public ProjectStatus Status { get; set; }
|
||||||
|
public Priority Priority { get; set; }
|
||||||
|
public decimal Budget { get; set; }
|
||||||
|
public decimal SpentAmount { get; set; }
|
||||||
|
public double ProgressPercent { get; set; }
|
||||||
|
public DateTime StartDate { get; set; }
|
||||||
|
public DateTime DueDate { get; set; }
|
||||||
|
public DateTime? CompletedDate { get; set; }
|
||||||
|
public int EstimatedHours { get; set; }
|
||||||
|
public int ActualHours { get; set; }
|
||||||
|
public int RiskLevel { get; set; }
|
||||||
|
public SharedUser? Owner { get; set; } // Cross-ref
|
||||||
|
public SharedCategory? Category { get; set; } // Cross-ref
|
||||||
|
public List<SharedTag> Tags { get; set; } = []; // Cross-ref
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public List<Level5_Task> Tasks { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 5: Task (18 properties)
|
||||||
|
public class Level5_Task : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public TaskStatus Status { get; set; }
|
||||||
|
public Priority Priority { get; set; }
|
||||||
|
public TaskType Type { get; set; }
|
||||||
|
public int StoryPoints { get; set; }
|
||||||
|
public double EstimatedHours { get; set; }
|
||||||
|
public double ActualHours { get; set; }
|
||||||
|
public DateTime DueDate { get; set; }
|
||||||
|
public DateTime? CompletedDate { get; set; }
|
||||||
|
public bool IsBlocked { get; set; }
|
||||||
|
public string? BlockedReason { get; set; }
|
||||||
|
public SharedUser? Assignee { get; set; } // Cross-ref
|
||||||
|
public SharedUser? Reporter { get; set; } // Cross-ref
|
||||||
|
public List<SharedTag> Labels { get; set; } = []; // Cross-ref
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public List<Level6_SubTask> SubTasks { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 6: SubTask (11 properties)
|
||||||
|
public class Level6_SubTask : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public TaskStatus Status { get; set; }
|
||||||
|
public int EstimatedMinutes { get; set; }
|
||||||
|
public int ActualMinutes { get; set; }
|
||||||
|
public bool IsCompleted { get; set; }
|
||||||
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
public SharedUser? Assignee { get; set; } // Cross-ref
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public List<Level7_Comment> Comments { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 7: Comment (10 properties)
|
||||||
|
public class Level7_Comment : IId<int>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Text { get; set; } = "";
|
||||||
|
public SharedUser? Author { get; set; } // Cross-ref
|
||||||
|
public bool IsEdited { get; set; }
|
||||||
|
public DateTime? EditedAt { get; set; }
|
||||||
|
public int LikeCount { get; set; }
|
||||||
|
public int ReplyCount { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public List<SharedTag> MentionedTags { get; set; } = []; // Cross-ref
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region AcJsonSerializer Benchmarks
|
||||||
|
|
||||||
|
[Benchmark(Description = "AcJsonSerializer (custom)")]
|
||||||
|
[BenchmarkCategory("Serialize")]
|
||||||
|
public string Serialize_AcJsonSerializer()
|
||||||
|
=> AcJsonSerializer.Serialize(_complexGraph);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue