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:
Loretta 2025-12-09 03:24:51 +01:00
parent 166d97106d
commit f9dc9a65fb
15 changed files with 5154 additions and 1399 deletions

1
.gitignore vendored
View File

@ -373,3 +373,4 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/BenchmarkSuite1/Results

View File

@ -135,7 +135,7 @@ public sealed class JsonExtensionTests
#endregion
#region Semantic Reference Tests (IId types with TypeName_Id format)
#region Semantic Reference Tests (IId types with long-based semantic IDs)
[TestMethod]
public void SemanticReference_SharedAttribute_SerializesWithSemanticId()
@ -159,13 +159,17 @@ public sealed class JsonExtensionTests
Console.WriteLine("Semantic Reference JSON:");
Console.WriteLine(json);
// Assert: Semantic $id format for IId types
Assert.IsTrue(json.Contains($"TestOrder_{order.Id}"), $"Should contain semantic $id for TestOrder. 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: Should contain $id for IId types (now using long-based semantic IDs)
Assert.IsTrue(json.Contains("\"$id\""), $"Should contain $id for IId types. JSON:\n{json}");
// Assert: $ref used for duplicate semantic references
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]
@ -233,11 +237,11 @@ public sealed class JsonExtensionTests
Console.WriteLine("Newtonsoft Reference JSON:");
Console.WriteLine(json);
// Assert: Should contain numeric $ref for non-IId duplicates
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain Newtonsoft $ref for shared non-IId metadata. JSON:\n{json}");
// Assert: Should contain $ref for duplicates
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain $ref for shared references. JSON:\n{json}");
// Assert: Semantic IId references also present
Assert.IsTrue(json.Contains($"TestOrder_{order.Id}"), "Should also contain semantic $id for IId types");
// Assert: Should contain $id for objects
Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id for objects");
}
[TestMethod]
@ -332,10 +336,8 @@ public sealed class JsonExtensionTests
Console.WriteLine("Hybrid Reference JSON:");
Console.WriteLine(json);
// Assert: Both reference systems work
Assert.IsTrue(json.Contains("TestOrder_1"), "Should have semantic $id for TestOrder");
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: Should have $id and $ref tokens for reference handling
Assert.IsTrue(json.Contains("\"$id\""), "Should have $id tokens for objects");
Assert.IsTrue(json.Contains("\"$ref\""), "Should have $ref tokens for 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(json);
// Assert: JSON structure - semantic IId references
Assert.IsTrue(json.Contains($"TestOrder_{originalOrder.Id}"), "Should have semantic $id for root order");
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 - should have $id for objects (now using long-based IDs)
Assert.IsTrue(json.Contains("\"$id\""), "Should have $id for objects");
// Assert: JSON structure - $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.IsTrue(json.Contains(originalOrder.OrderNumber), "Should contain order number");
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]
@ -1187,4 +1192,175 @@ public sealed class JsonExtensionTests
}
#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
}

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34221.43
# Visual Studio Version 18
VisualStudioVersion = 18.0.11222.15 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Core", "AyCode.Core\AyCode.Core.csproj", "{8CCC4969-7306-4747-8A58-80AC5A062EE1}"
EndProject
@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
AyCode.Core.targets = AyCode.Core.targets
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkSuite1", "BenchmarkSuite1\BenchmarkSuite1.csproj", "{A20861A9-411E-6150-BF5C-69E8196E5D22}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Product|Any CPU.ActiveCfg = Product|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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -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&lt;T&gt; 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
}

View File

@ -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&lt;T&gt; 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&lt;T&gt;, 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
}

View File

@ -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

View File

@ -1,156 +1,558 @@
using AyCode.Core.Interfaces;
using MessagePack;
using MessagePack.Resolvers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Buffers;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Text;
namespace AyCode.Core.Extensions;
/// <summary>
/// Hybrid reference resolver that uses semantic IDs for IId&lt;T&gt; types
/// and standard numeric IDs for other types.
/// High-performance Base62 encoder for compact $id/$ref values.
/// </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>
public class HybridReferenceResolver : IReferenceResolver
{
private readonly Dictionary<string, object> _idToObject = new(StringComparer.Ordinal);
private readonly Dictionary<object, string> _objectToId = new(ReferenceEqualityComparer.Instance);
internal Dictionary<string, object>? _idToObject;
internal Dictionary<object, string>? _objectToId;
internal HashSet<string>? _referencedIds;
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)
{
_idToObject[reference] = value;
_objectToId[value] = reference;
GetIdToObject()[reference] = value;
GetObjectToId()[value] = reference;
}
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 (isId, idType) = TypeCache.GetIdInfo(type);
string newRef;
if (isId && idType != null)
{
// Use semantic ID for IId<T> types
var idProperty = type.GetProperty("Id");
var idValue = idProperty?.GetValue(value);
if (idValue != null && !idValue.Equals(GetDefault(idType)))
var idGetter = GetOrCreateIdGetter(type);
var idValue = idGetter(value);
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
{
// Fallback to numeric for IId types with default Id
newRef = (_nextNumericId++).ToString();
newRef = Base62.Encode(-_nextNumericId++);
}
}
else
{
// Use numeric ID for non-IId types
newRef = (_nextNumericId++).ToString();
newRef = Base62.Encode(-_nextNumericId++);
}
_idToObject[newRef] = value;
_objectToId[value] = newRef;
GetIdToObject()[newRef] = value;
objectToId[value] = newRef;
return newRef;
}
public bool IsReferenced(object context, object value)
{
return _objectToId.ContainsKey(value);
}
public bool IsReferenced(object context, object value) => _objectToId?.ContainsKey(value) ?? false;
public object ResolveReference(object context, string reference)
{
_idToObject.TryGetValue(reference, out var value);
return value!;
}
public object ResolveReference(object context, string reference) =>
_idToObject != null && _idToObject.TryGetValue(reference, out var value) ? value : null!;
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>
/// Reference equality comparer for proper object identity comparison
/// </summary>
internal class ReferenceEqualityComparer : IEqualityComparer<object>
internal sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
public static readonly ReferenceEqualityComparer Instance = new();
public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}
internal static class JsonReferencePostProcessor
{
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
{
// Hybrid settings that support both semantic IDs for IId<T> types
// and standard reference handling for other types
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
private static readonly Dictionary<object, object> EmptyContextDict = new();
private static readonly JsonSerializer CachedSerializer = CreateCachedSerializer();
public static JsonSerializerSettings Options => new()
{
ContractResolver = new UnifiedMergeContractResolver(),
Context = new StreamingContext(StreamingContextStates.All, new Dictionary<object, object>()),
// Enable reference handling with our hybrid resolver
ContractResolver = SharedContractResolver,
Context = new StreamingContext(StreamingContextStates.All, EmptyContextDict),
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ReferenceResolverProvider = () => new HybridReferenceResolver(),
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);
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);
private static JsonSerializer CreateCachedSerializer() => JsonSerializer.Create(new JsonSerializerSettings
{
ContractResolver = SharedContractResolver,
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
Formatting = Formatting.None,
});
/// <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)
{
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);
// ========================================================================
// 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)
{
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);
// ========================================================================
// 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)
{
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);
// ========================================================================
// 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))]
public static TDestination? CloneTo<TDestination>(this object? src, JsonSerializerSettings? options = null) where TDestination : class
=> src?.ToJson(options).JsonTo<TDestination>(options);
/// <summary>
/// Using JSON
/// </summary>
public static void CopyTo(this object? src, object target, JsonSerializerSettings? options = null) => src?.ToJson(options).JsonTo(target, options);
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, 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, MessagePackSerializerOptions options) => MessagePackSerializer.Deserialize<T>(message, options);
}
@ -163,55 +565,41 @@ public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContract
public void IgnoreProperty(Type type, params string[] jsonPropertyNames)
{
if (!_ignores.ContainsKey(type)) _ignores[type] = [];
foreach (var prop in jsonPropertyNames) _ignores[type].Add(prop);
if (!_ignores.TryGetValue(type, out var set)) { set = new HashSet<string>(StringComparer.Ordinal); _ignores[type] = set; }
foreach (var prop in jsonPropertyNames) set.Add(prop);
}
public void IncludesProperty(Type type, params string[] jsonPropertyNames)
{
if (!_includes.ContainsKey(type)) _includes[type] = [];
foreach (var prop in jsonPropertyNames) _includes[type].Add(prop);
if (!_includes.TryGetValue(type, out var set)) { set = new HashSet<string>(StringComparer.Ordinal); _includes[type] = set; }
foreach (var prop in jsonPropertyNames) set.Add(prop);
}
public void RenameProperty(Type type, string propertyName, string newJsonPropertyName)
{
if (!_renames.ContainsKey(type)) _renames[type] = new Dictionary<string, string>();
_renames[type][propertyName] = newJsonPropertyName;
if (!_renames.TryGetValue(type, out var dict)) { dict = new Dictionary<string, string>(StringComparer.Ordinal); _renames[type] = dict; }
dict[propertyName] = newJsonPropertyName;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (IsIgnored(property.DeclaringType, property.PropertyName) || !IsIncluded(property.DeclaringType, property.PropertyName))
{
property.ShouldSerialize = i => false;
property.Ignored = true;
}
if (IsRenamed(property.DeclaringType, property.PropertyName, out var newJsonPropertyName))
property.PropertyName = newJsonPropertyName;
{ property.ShouldSerialize = _ => false; property.Ignored = true; }
if (IsRenamed(property.DeclaringType, property.PropertyName, out var newName)) property.PropertyName = newName;
return property;
}
private bool IsIgnored(Type type, string jsonPropertyName)
{
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));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsIgnored(Type? type, string? name) => type != null && name != null && _ignores.TryGetValue(type, out var set) && set.Contains(name);
private bool IsRenamed(Type type, string jsonPropertyName, out string? newJsonPropertyName)
{
if (_renames.TryGetValue(type, out var renames) && renames.TryGetValue(jsonPropertyName, out newJsonPropertyName)) return true;
[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));
newJsonPropertyName = null;
return false;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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;
}
}

View File

@ -2,6 +2,8 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using AyCode.Core.Extensions;
using System.Threading;
namespace AyCode.Core.Helpers
{
@ -11,6 +13,39 @@ namespace AyCode.Core.Helpers
public void Replace(IEnumerable other);
public void RemoveRange(IEnumerable other);
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
@ -20,81 +55,298 @@ namespace AyCode.Core.Helpers
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>
{
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(List<T> list) : base(list)
{ }
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)
{
_suppressChangedEvent = true;
Clear();
AddRange(other);
BeginUpdate();
try
{
lock (_syncRoot)
{
base.Clear();
foreach (var item in other) base.Add(item);
}
}
finally
{
EndUpdate();
}
}
public void Replace(IEnumerable other)
{
_suppressChangedEvent = true;
Clear();
foreach (T item in other) Add(item);
_suppressChangedEvent = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
BeginUpdate();
try
{
lock (_syncRoot)
{
base.Clear();
foreach (T item in other) base.Add(item);
}
}
finally
{
EndUpdate();
}
}
public void AddRange(IEnumerable other)
{
_suppressChangedEvent = true;
BeginUpdate();
try
{
lock (_syncRoot)
{
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)
{
_suppressChangedEvent = true;
BeginUpdate();
try
{
lock (_syncRoot)
{
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));
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
public void PopulateFromJson(string json, bool clearAll = false)
{
BeginUpdate();
try
{
lock (_syncRoot)
{
if (clearAll) base.Clear();
json.JsonTo(this.Items);
}
}
finally
{
EndUpdate();
}
}
public void SortAndReplace(IEnumerable<T> other, IComparer<T> comparer)
{
List<T> values = new(other);
var values = new List<T>(other);
values.Sort(comparer);
Replace(values);
}
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);
Replace(values);
}
@ -122,7 +374,7 @@ namespace AyCode.Core.Helpers
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (_suppressChangedEvent)
if (IsUpdating)
return;
base.OnPropertyChanged(e);
@ -130,31 +382,10 @@ namespace AyCode.Core.Helpers
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (_suppressChangedEvent)
if (IsUpdating)
return;
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

View File

@ -133,6 +133,9 @@ public sealed class SignalResponseJsonMessage : ISignalResponseMessage<string>
[Key(2)] public string? ResponseData { get; set; } = null;
[IgnoreMember]
public string? ResponseDataJson => ResponseData;
public SignalResponseJsonMessage(){}
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]
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; }
[Key(1)] public SignalResponseStatus Status { get; set; } = status;
[Key(2)] public TResponseData? ResponseData { get; set; } = responseData;
[IgnoreMember]
private TResponseData? _responseData;
[IgnoreMember]
private bool _isDeserialized;
[Key(0)]
public int MessageTag { get; set; }
[Key(1)]
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 sealed class SignalResponseStatusMessage(SignalResponseStatus status) : ISignalRMessage
public SignalResponseMessage()
{
public SignalResponseStatus Status { get; set; } = status;
}
//[MessagePackObject]
//public sealed class SignalResponseMessage(SignalResponseStatus status) : ISignalResponseMessage
//{
// [Key(0)]
// public SignalResponseStatus Status { get; set; } = status;
//}
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
{
/// <summary>
/// Deserialized response data. May trigger lazy deserialization.
/// </summary>
TResponseData? ResponseData { get; set; }
/// <summary>
/// Raw JSON string for direct access without deserialization.
/// </summary>
string? ResponseDataJson { get; }
}
public interface ISignalResponseMessage : ISignalRMessage

View File

@ -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>

View File

@ -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}");
}
}
}
}

View File

@ -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
}