Refactor JSON (de)serialization: options, depth, utilities

Major overhaul of JSON serialization/deserialization:
- Introduce AcJsonSerializerOptions for reference handling and max depth
- Centralize type checks, primitive/collection logic in JsonUtilities
- Add depth limiting to serializer/deserializer (MaxDepth support)
- Make $id/$ref reference handling optional via options
- Unify and simplify public API (ToJson, JsonTo, CloneTo, CopyTo, etc.)
- Improve primitive, enum, and collection handling and caching
- Refactor contract resolver and merge logic to use new utilities
- Remove redundant code, centralize string escaping/unescaping
- Update all tests and benchmarks to use new API and options
- Fix minor bugs and improve error handling and validation

This modernizes and unifies the JSON infrastructure for better performance, flexibility, and maintainability.
This commit is contained in:
Loretta 2025-12-12 11:30:55 +01:00
parent 8e7869b3da
commit ad426feba4
10 changed files with 1460 additions and 2256 deletions

View File

@ -17,8 +17,6 @@ public sealed class JsonExtensionTests
TestDataFactory.ResetIdCounter();
}
private static JsonSerializerSettings GetMergeSettings() => SerializeObjectExtensions.Options;
#region Deep Hierarchy Tests (5 Levels)
[TestMethod]
@ -56,7 +54,7 @@ public sealed class JsonExtensionTests
}}";
// Act
updateJson.JsonTo(order, GetMergeSettings());
updateJson.JsonTo(order);
// Assert: All references preserved
Assert.AreSame(originalItem, order.Items[0], "Level 2: Item reference must be preserved");
@ -101,7 +99,7 @@ public sealed class JsonExtensionTests
}}";
// Act
updateJson.JsonTo(order, GetMergeSettings());
updateJson.JsonTo(order);
// Assert
Assert.AreEqual(originalItemCount, order.Items.Count, "Items count should be preserved (KEEP logic)");
@ -120,11 +118,8 @@ public sealed class JsonExtensionTests
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var order = TestDataFactory.CreateOrder(itemCount: 2, sharedTag: sharedTag);
var settings = GetMergeSettings();
settings.Formatting = Formatting.Indented;
// Act
var json = order.ToJson(settings);
var json = order.ToJson();
Console.WriteLine($"Semantic Reference JSON:\n{json}");
// Assert
@ -155,7 +150,7 @@ public sealed class JsonExtensionTests
}";
// Act
updateJson.JsonTo(order, GetMergeSettings());
updateJson.JsonTo(order);
// Assert
Assert.AreEqual("ORD-UPDATED", order.OrderNumber);
@ -174,11 +169,8 @@ public sealed class JsonExtensionTests
var sharedMeta = TestDataFactory.CreateMetadata(withChild: true);
var order = TestDataFactory.CreateOrder(itemCount: 2, sharedMetadata: sharedMeta);
var settings = GetMergeSettings();
settings.Formatting = Formatting.Indented;
// Act
var json = order.ToJson(settings);
var json = order.ToJson();
Console.WriteLine($"Newtonsoft Reference JSON:\n{json}");
// Assert
@ -210,11 +202,8 @@ public sealed class JsonExtensionTests
AuditMetadata = rootMeta
};
var settings = GetMergeSettings();
settings.Formatting = Formatting.Indented;
// Act
var json = order.ToJson(settings);
var json = order.ToJson();
// Assert
Assert.IsTrue(json.Contains("Root"));
@ -247,8 +236,7 @@ public sealed class JsonExtensionTests
Items = [new TestOrderItem { Id = 10, ProductName = "A", Tag = sharedTag, ItemMetadata = sharedMeta }]
};
var settings = GetMergeSettings();
var json = order.ToJson(settings);
var json = order.ToJson();
// Assert
var refCount = json.Split("\"$ref\"").Length - 1;
@ -279,7 +267,7 @@ public sealed class JsonExtensionTests
}}";
// Act
order.DeepPopulateWithMerge(updateJson, GetMergeSettings());
order.DeepPopulateWithMerge(updateJson);
// Assert
Assert.AreNotSame(originalRef, order.NoMergeItems);
@ -314,7 +302,7 @@ public sealed class JsonExtensionTests
}";
// Act
order.DeepPopulateWithMerge(updateJson, GetMergeSettings());
order.DeepPopulateWithMerge(updateJson);
// Assert
Assert.AreEqual(2, order.MetadataList.Count);
@ -350,9 +338,9 @@ public sealed class JsonExtensionTests
new { Id = appleId, Name = "Apple", Qty = 7 },
new { Id = Guid.NewGuid(), Name = "Banana", Qty = 4 }
}
}.ToJson(GetMergeSettings());
}.ToJson();
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// List reference preserved
Assert.AreSame(originalItemsRef, order.Items, "List reference must be preserved");
@ -377,8 +365,8 @@ public sealed class JsonExtensionTests
var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 2, sharedTag: sharedTag, sharedMetadata: sharedMeta);
// Act
var json = order.ToJson(GetMergeSettings());
var deserialized = json.JsonTo<TestOrder>(GetMergeSettings());
var json = order.ToJson();
var deserialized = json.JsonTo<TestOrder>();
// Assert
Assert.IsNotNull(deserialized);
@ -394,10 +382,9 @@ public sealed class JsonExtensionTests
[TestMethod]
public void PrimitiveArray_BooleanTrue_RoundTrips()
{
var settings = GetMergeSettings();
var jsonString = (new[] { true }).ToJson(settings);
var jsonString = (new[] { true }).ToJson();
var result = jsonString.JsonTo(typeof(bool[]), settings) as bool[];
var result = jsonString.JsonTo(typeof(bool[])) as bool[];
Assert.IsNotNull(result);
Assert.IsTrue(result[0], "Boolean true should deserialize as true!");
@ -406,7 +393,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void PrimitiveArray_AllTypes_RoundTrip()
{
var settings = GetMergeSettings();
var testCases = new (Type type, object value)[]
{
(typeof(bool), true),
@ -424,9 +410,9 @@ public sealed class JsonExtensionTests
{
var wrapped = Array.CreateInstance(type, 1);
wrapped.SetValue(value, 0);
var json = wrapped.ToJson(settings);
var json = wrapped.ToJson();
var result = json.JsonTo(type.MakeArrayType(), settings) as Array;
var result = json.JsonTo(type.MakeArrayType()) as Array;
Assert.IsNotNull(result, $"Failed for {type.Name}");
Assert.AreEqual(value, result.GetValue(0), $"Value mismatch for {type.Name}");
@ -436,15 +422,14 @@ public sealed class JsonExtensionTests
[TestMethod]
public void IdMessage_MultipleParameters_SimulateSignalR()
{
var settings = GetMergeSettings();
var @params = new (Type t, object v)[] { (typeof(bool), true), (typeof(string), "filter"), (typeof(int), 100) };
foreach (var (type, value) in @params)
{
var wrapped = Array.CreateInstance(type, 1);
wrapped.SetValue(value, 0);
var json = wrapped.ToJson(settings);
var arr = json.JsonTo(type.MakeArrayType(), settings) as Array;
var json = wrapped.ToJson();
var arr = json.JsonTo(type.MakeArrayType()) as Array;
Assert.AreEqual(value, arr?.GetValue(0));
}
}
@ -574,7 +559,7 @@ public sealed class JsonExtensionTests
};
// Act - Serialize with AyCode
var json = order.ToJson(GetMergeSettings());
var json = order.ToJson();
// Deserialize with native Newtonsoft
var nativeSettings = new JsonSerializerSettings
@ -600,8 +585,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Populate_RefNode_ShouldSetPropertyToReferencedObject()
{
// Arrange: Create JSON with $id and $ref
// This simulates a scenario where the same object is referenced multiple times
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -612,15 +595,13 @@ public sealed class JsonExtensionTests
var order = new TestOrder { Id = 1, OrderNumber = "OLD" };
// Act
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from $ref");
Assert.AreEqual(100, order.PrimaryTag.Id);
Assert.AreEqual("SharedTag", order.PrimaryTag.Name);
// The key assertion: SecondaryTag should be the SAME object as PrimaryTag (via $ref)
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
"SecondaryTag should reference the same object as PrimaryTag via $ref");
}
@ -628,7 +609,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Populate_RefNodeInCollection_ShouldSetPropertyToReferencedObject()
{
// Arrange: Create JSON with shared reference in collection
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -642,17 +622,13 @@ public sealed class JsonExtensionTests
var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List<SharedTag>() };
// Act
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
Assert.AreEqual(2, order.Tags.Count, "Tags should have 2 items");
// First tag should be same as PrimaryTag via $ref
Assert.AreSame(order.PrimaryTag, order.Tags[0],
"Tags[0] should reference the same object as PrimaryTag via $ref");
// Second tag should be different
Assert.AreEqual(200, order.Tags[1].Id);
Assert.AreNotSame(order.PrimaryTag, order.Tags[1]);
}
@ -660,7 +636,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Populate_NestedRefNode_ShouldResolveCorrectly()
{
// Arrange: Create JSON with nested $ref
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -680,13 +655,11 @@ public sealed class JsonExtensionTests
};
// Act
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.Items[0].Tag, "Item's Tag should be set");
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set from $ref");
// PrimaryTag should be same as Items[0].Tag via $ref
Assert.AreSame(order.Items[0].Tag, order.PrimaryTag,
"PrimaryTag should reference the same object as Items[0].Tag via $ref");
}
@ -694,7 +667,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Populate_ForwardRef_ShouldResolveDeferredReference()
{
// Arrange: $ref appears BEFORE $id (forward reference)
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -705,9 +677,9 @@ public sealed class JsonExtensionTests
var order = new TestOrder { Id = 1, OrderNumber = "OLD" };
// Act
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// Assert - forward reference should be resolved
// Assert
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from forward $ref");
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
@ -717,7 +689,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Populate_MultipleRefsToSameId_AllShouldResolveToSameObject()
{
// Arrange: Create JSON with multiple $refs pointing to the same $id
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -733,9 +704,9 @@ public sealed class JsonExtensionTests
var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List<SharedTag>() };
// Act
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// Assert - all refs should point to the same object
// Assert
Assert.IsNotNull(order.PrimaryTag);
Assert.AreSame(order.PrimaryTag, order.SecondaryTag);
Assert.AreEqual(3, order.Tags.Count);
@ -747,7 +718,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Populate_DeeplyNestedRef_ShouldResolveAcrossLevels()
{
// Arrange: Create JSON with $id at deep level (Item.Tag), $ref at root level
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -766,7 +736,7 @@ public sealed class JsonExtensionTests
};
// Act
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// Assert
var deepTag = order.Items[0].Tag;
@ -779,7 +749,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Populate_RefInNestedObject_ShouldResolveFromParentContext()
{
// Arrange: $id at root, $ref in nested child
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -798,7 +767,7 @@ public sealed class JsonExtensionTests
};
// Act
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag);
@ -810,7 +779,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Deserialize_RefOnly_ShouldCreateDeferredAndResolve()
{
// Arrange: JSON where only $ref exists (forward reference scenario in deserialize)
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -831,7 +799,6 @@ public sealed class JsonExtensionTests
[TestMethod]
public void Deserialize_MultipleIdRefs_ComplexGraph()
{
// Arrange: Complex object graph with multiple $id/$ref pairs
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
@ -855,23 +822,16 @@ public sealed class JsonExtensionTests
// Assert
Assert.IsNotNull(order);
Assert.AreEqual(3, order.Tags.Count);
// Verify tag1 references
Assert.AreSame(order.PrimaryTag, order.Tags[0]);
Assert.AreSame(order.PrimaryTag, order.Tags[2]);
// Verify tag2 references
Assert.AreSame(order.SecondaryTag, order.Tags[1]);
Assert.AreSame(order.SecondaryTag, order.Items[0].Tag);
// Verify they are different
Assert.AreNotSame(order.PrimaryTag, order.SecondaryTag);
}
[TestMethod]
public void Populate_RefWithExistingTarget_ShouldOverwriteWithReference()
{
// Arrange: Target has existing value, should be overwritten by $ref
var json = @"{
""Id"": 1,
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""NewTag"" },
@ -882,13 +842,13 @@ public sealed class JsonExtensionTests
var order = new TestOrder
{
Id = 1,
SecondaryTag = existingTag // Pre-existing value
SecondaryTag = existingTag
};
// Act
json.JsonTo(order, GetMergeSettings());
json.JsonTo(order);
// Assert - SecondaryTag should be overwritten with the $ref reference
// Assert
Assert.IsNotNull(order.PrimaryTag);
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
"SecondaryTag should be overwritten with $ref reference");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,126 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using AyCode.Core.Interfaces;
using Newtonsoft.Json;
namespace AyCode.Core.Extensions;
internal static class JsonUtilities
/// <summary>
/// Options for AcJsonSerializer and AcJsonDeserializer.
/// </summary>
public sealed class AcJsonSerializerOptions
{
/// <summary>
/// Default options instance with reference handling enabled and max depth.
/// </summary>
public static readonly AcJsonSerializerOptions Default = new();
/// <summary>
/// Options for shallow serialization (root level only, no references).
/// </summary>
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false };
/// <summary>
/// Whether to use $id/$ref reference handling for circular references.
/// Default: true
/// </summary>
public bool UseReferenceHandling { get; init; } = true;
/// <summary>
/// Maximum depth for serialization/deserialization.
/// 0 = root level only (primitives of root object)
/// 1 = root + first level of nested objects/collections
/// byte.MaxValue (255) = effectively unlimited
/// Default: byte.MaxValue
/// </summary>
public byte MaxDepth { get; init; } = byte.MaxValue;
/// <summary>
/// Creates options with specified max depth.
/// </summary>
public static AcJsonSerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
/// <summary>
/// Creates options without reference handling.
/// </summary>
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
}
/// <summary>
/// Central utilities for JSON serialization/deserialization.
/// Contains shared type caches, primitive type checks, and string utilities.
/// </summary>
public static class JsonUtilities
{
#region Pre-computed Type Handles
public static readonly Type IntType = typeof(int);
public static readonly Type LongType = typeof(long);
public static readonly Type DoubleType = typeof(double);
public static readonly Type DecimalType = typeof(decimal);
public static readonly Type FloatType = typeof(float);
public static readonly Type StringType = typeof(string);
public static readonly Type DateTimeType = typeof(DateTime);
public static readonly Type GuidType = typeof(Guid);
public static readonly Type BoolType = typeof(bool);
public static readonly Type DateTimeOffsetType = typeof(DateTimeOffset);
public static readonly Type TimeSpanType = typeof(TimeSpan);
public static readonly Type ByteType = typeof(byte);
public static readonly Type ShortType = typeof(short);
public static readonly Type UShortType = typeof(ushort);
public static readonly Type UIntType = typeof(uint);
public static readonly Type ULongType = typeof(ulong);
public static readonly Type SByteType = typeof(sbyte);
public static readonly Type CharType = typeof(char);
#endregion
#region Cached Generic Type Definitions
internal static readonly Type IEnumerableGenericType = typeof(IEnumerable<>);
internal static readonly Type IIdGenericType = typeof(IId<>);
internal static readonly Type NullableGenericType = typeof(Nullable<>);
internal static readonly Type IListGenericType = typeof(IList<>);
internal static readonly Type ListGenericType = typeof(List<>);
internal static readonly Type DictionaryGenericType = typeof(Dictionary<,>);
internal static readonly Type IDictionaryGenericType = typeof(IDictionary<,>);
internal static readonly Type ObservableCollectionType = typeof(System.Collections.ObjectModel.ObservableCollection<>);
internal static readonly Type CollectionType = typeof(System.Collections.ObjectModel.Collection<>);
#endregion
#region Primitive Type Set
private static readonly FrozenSet<Type> PrimitiveTypes = new HashSet<Type>
{
typeof(string), typeof(decimal), typeof(DateTime),
typeof(DateTimeOffset), typeof(Guid), typeof(TimeSpan),
typeof(bool), typeof(byte), typeof(sbyte), typeof(short),
typeof(ushort), typeof(int), typeof(uint), typeof(long),
typeof(ulong), typeof(float), typeof(double), typeof(char)
}.ToFrozenSet();
#endregion
#region Type Caches
private static readonly ConcurrentDictionary<Type, (bool IsId, Type? IdType)> IdInfoCache = new();
private static readonly ConcurrentDictionary<Type, Type?> CollectionElementCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCollectionCache = new();
private static readonly ConcurrentDictionary<PropertyInfo, bool> JsonIgnoreCache = new();
private static readonly ConcurrentDictionary<Type, Func<IList>> ListFactoryCache = new();
#endregion
#region String Utilities
/// <summary>
/// Unwraps a JSON string that may be double-quoted (e.g., "\"...\"" -> ...).
/// Optimized to avoid Regex.Unescape allocation when no escape sequences exist.
@ -18,23 +134,14 @@ internal static class JsonUtilities
if (span.Length < 2 || span[0] != '"' || span[^1] != '"')
return json;
// Extract inner content (without outer quotes)
var inner = span[1..^1];
// Fast path: check if any escape sequences exist
if (!inner.Contains('\\'))
{
// No escapes - just return substring (single allocation)
return json.Substring(1, json.Length - 2);
}
// Slow path: unescape the string
return UnescapeJsonString(inner);
}
/// <summary>
/// Manual JSON string unescaping - avoids Regex.Unescape overhead.
/// </summary>
private static string UnescapeJsonString(ReadOnlySpan<char> input)
{
var sb = new StringBuilder(input.Length);
@ -51,54 +158,24 @@ internal static class JsonUtilities
var next = input[i + 1];
switch (next)
{
case '"':
sb.Append('"');
i++;
break;
case '\\':
sb.Append('\\');
i++;
break;
case '/':
sb.Append('/');
i++;
break;
case 'b':
sb.Append('\b');
i++;
break;
case 'f':
sb.Append('\f');
i++;
break;
case 'n':
sb.Append('\n');
i++;
break;
case 'r':
sb.Append('\r');
i++;
break;
case 't':
sb.Append('\t');
i++;
break;
case '"': sb.Append('"'); i++; break;
case '\\': sb.Append('\\'); i++; break;
case '/': sb.Append('/'); i++; break;
case 'b': sb.Append('\b'); i++; break;
case 'f': sb.Append('\f'); i++; break;
case 'n': sb.Append('\n'); i++; break;
case 'r': sb.Append('\r'); i++; break;
case 't': sb.Append('\t'); i++; break;
case 'u' when i + 5 < input.Length:
// Unicode escape: \uXXXX
var hex = input.Slice(i + 2, 4);
if (TryParseHex(hex, out var unicode))
{
sb.Append((char)unicode);
i += 5;
}
else
{
sb.Append(c);
}
break;
default:
sb.Append(c);
else sb.Append(c);
break;
default: sb.Append(c); break;
}
}
@ -112,15 +189,275 @@ internal static class JsonUtilities
foreach (var c in hex)
{
value <<= 4;
if (c >= '0' && c <= '9')
value |= c - '0';
else if (c >= 'a' && c <= 'f')
value |= c - 'a' + 10;
else if (c >= 'A' && c <= 'F')
value |= c - 'A' + 10;
else
return false;
if (c >= '0' && c <= '9') value |= c - '0';
else if (c >= 'a' && c <= 'f') value |= c - 'a' + 10;
else if (c >= 'A' && c <= 'F') value |= c - 'A' + 10;
else return false;
}
return true;
}
/// <summary>
/// Checks if a string needs JSON escaping.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool NeedsEscaping(string value)
{
foreach (var c in value)
{
if (c < 32 || c == '"' || c == '\\')
return true;
}
return false;
}
/// <summary>
/// Escapes a string for JSON output.
/// </summary>
public static void WriteEscapedString(StringBuilder sb, string value)
{
foreach (var c in value)
{
switch (c)
{
case '"': sb.Append("\\\""); break;
case '\\': sb.Append("\\\\"); break;
case '\b': sb.Append("\\b"); break;
case '\f': sb.Append("\\f"); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default:
if (c < 32)
{
sb.Append("\\u");
sb.Append(((int)c).ToString("X4"));
}
else sb.Append(c);
break;
}
}
}
#endregion
#region Type Checking Methods
/// <summary>
/// Fast primitive check using type code.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveOrString(Type type)
{
return IsPrimitiveCache.GetOrAdd(type, static t =>
{
if (t.IsPrimitive || PrimitiveTypes.Contains(t)) return true;
if (t.IsGenericType && t.GetGenericTypeDefinition() == NullableGenericType)
return IsPrimitiveOrString(t.GetGenericArguments()[0]);
if (t.IsEnum) return true;
return false;
});
}
/// <summary>
/// Faster primitive check using TypeCode for hot paths.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveOrStringFast(Type type)
{
var typeCode = Type.GetTypeCode(type);
return typeCode switch
{
TypeCode.Boolean or TypeCode.Char or TypeCode.SByte or TypeCode.Byte or
TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or
TypeCode.Int64 or TypeCode.UInt64 or TypeCode.Single or TypeCode.Double or
TypeCode.Decimal or TypeCode.DateTime or TypeCode.String => true,
_ => type == GuidType || type == TimeSpanType || type == DateTimeOffsetType || type.IsEnum
};
}
/// <summary>
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsGenericCollectionType(Type type)
{
return IsCollectionCache.GetOrAdd(type, static t =>
{
if (t == StringType || t.IsPrimitive) return false;
if (t.IsArray) return true;
if (t.IsGenericType)
{
var genericDef = t.GetGenericTypeDefinition();
if (genericDef == ListGenericType ||
genericDef == IListGenericType ||
genericDef == typeof(ICollection<>) ||
genericDef == IEnumerableGenericType ||
genericDef == ObservableCollectionType ||
genericDef == CollectionType)
return true;
}
foreach (var iface in t.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType)
return true;
}
return typeof(IEnumerable).IsAssignableFrom(t);
});
}
/// <summary>
/// Checks if type is a dictionary type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType)
{
keyType = null;
valueType = null;
if (!type.IsGenericType) return false;
var genericDef = type.GetGenericTypeDefinition();
if (genericDef == DictionaryGenericType || genericDef == IDictionaryGenericType)
{
var args = type.GetGenericArguments();
keyType = args[0];
valueType = args[1];
return true;
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IDictionaryGenericType)
{
var args = iface.GetGenericArguments();
keyType = args[0];
valueType = args[1];
return true;
}
}
return false;
}
/// <summary>
/// Gets the element type of a collection.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Type? GetCollectionElementType(Type collectionType)
{
return CollectionElementCache.GetOrAdd(collectionType, static type =>
{
if (type.IsArray)
return type.GetElementType();
if (type.IsGenericType)
{
var genericDef = type.GetGenericTypeDefinition();
if (genericDef == ListGenericType || genericDef == IListGenericType ||
genericDef == typeof(ICollection<>) || genericDef == IEnumerableGenericType)
return type.GetGenericArguments()[0];
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType)
return iface.GetGenericArguments()[0];
}
return typeof(object);
});
}
/// <summary>
/// Gets IId info for a type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (bool IsId, Type? IdType) GetIdInfo(Type type)
{
return IdInfoCache.GetOrAdd(type, static t =>
{
foreach (var iface in t.GetInterfaces())
{
if (!iface.IsGenericType) continue;
if (iface.GetGenericTypeDefinition() != IIdGenericType) continue;
var idType = iface.GetGenericArguments()[0];
return (idType.IsValueType, idType);
}
return (false, null);
});
}
/// <summary>
/// Checks if property has JsonIgnore attribute.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasJsonIgnoreAttribute(PropertyInfo prop)
{
return JsonIgnoreCache.GetOrAdd(prop, static p =>
Attribute.IsDefined(p, typeof(JsonIgnoreAttribute)) ||
Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)));
}
/// <summary>
/// Checks if collection contains primitive elements.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveElementCollection(Type type)
{
return IsPrimitiveCollectionCache.GetOrAdd(type, static t =>
{
if (t == StringType) return false;
Type? elementType = null;
if (t.IsArray)
elementType = t.GetElementType();
else if (t.IsGenericType && typeof(IEnumerable).IsAssignableFrom(t))
{
var genericArgs = t.GetGenericArguments();
if (genericArgs.Length == 1) elementType = genericArgs[0];
}
if (elementType == null) return false;
return IsPrimitiveOrString(elementType) || elementType.IsEnum;
});
}
/// <summary>
/// Gets or creates a list factory for a given element type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Func<IList> GetOrCreateListFactory(Type elementType)
{
return ListFactoryCache.GetOrAdd(elementType, static t =>
{
var listType = ListGenericType.MakeGenericType(t);
var newExpr = System.Linq.Expressions.Expression.New(listType);
var castExpr = System.Linq.Expressions.Expression.Convert(newExpr, typeof(IList));
return System.Linq.Expressions.Expression.Lambda<Func<IList>>(castExpr).Compile();
});
}
/// <summary>
/// Checks if value is the default value for its type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDefaultValue(object id, Type idType)
{
if (ReferenceEquals(idType, IntType)) return (int)id == 0;
if (ReferenceEquals(idType, LongType)) return (long)id == 0;
if (ReferenceEquals(idType, GuidType)) return (Guid)id == Guid.Empty;
if (ReferenceEquals(idType, ShortType)) return (short)id == 0;
if (ReferenceEquals(idType, ByteType)) return (byte)id == 0;
if (ReferenceEquals(idType, UIntType)) return (uint)id == 0;
if (ReferenceEquals(idType, ULongType)) return (ulong)id == 0;
if (ReferenceEquals(idType, UShortType)) return (ushort)id == 0;
if (ReferenceEquals(idType, SByteType)) return (sbyte)id == 0;
return false;
}
#endregion
}

View File

@ -1,23 +1,23 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Reflection;
using System.Runtime.CompilerServices;
using AyCode.Core.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using static AyCode.Core.Extensions.JsonUtilities;
namespace AyCode.Core.Extensions
namespace AyCode.Core.Extensions;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonNoMergeCollectionAttribute : Attribute { }
/// <summary>
/// Cached property metadata for faster JSON processing.
/// </summary>
public sealed class CachedPropertyInfo
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonNoMergeCollectionAttribute : Attribute { }
/// <summary>
/// Cached property metadata for faster JSON processing.
/// </summary>
public sealed class CachedPropertyInfo
{
public PropertyInfo Property { get; }
public string Name { get; }
public Type PropertyType { get; }
@ -27,110 +27,69 @@ namespace AyCode.Core.Extensions
public Type? CollectionElementType { get; }
public Type? CollectionElementIdType { get; }
public bool ShouldSkip { get; }
public bool CanRead { get; }
public bool HasIndexParameters { get; }
public CachedPropertyInfo(PropertyInfo prop)
{
Property = prop;
Name = prop.Name;
PropertyType = prop.PropertyType;
CanRead = prop.CanRead;
HasIndexParameters = prop.GetIndexParameters().Length > 0;
// Pre-compute skip condition
ShouldSkip = !CanRead || HasIndexParameters || TypeCache.HasJsonIgnoreAttribute(prop);
ShouldSkip = !prop.CanRead ||
prop.GetIndexParameters().Length > 0 ||
HasJsonIgnoreAttribute(prop);
if (!ShouldSkip)
{
var (isId, idType) = TypeCache.GetIdInfo(PropertyType);
var (isId, idType) = GetIdInfo(PropertyType);
IsIId = isId;
IdType = idType;
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != typeof(string))
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != StringType)
{
CollectionElementType = TypeCache.GetElementType(PropertyType);
CollectionElementType = GetCollectionElementType(PropertyType);
if (CollectionElementType != null)
{
var (elemIsId, elemIdType) = TypeCache.GetIdInfo(CollectionElementType);
var (elemIsId, elemIdType) = GetIdInfo(CollectionElementType);
IsIIdCollection = elemIsId;
CollectionElementIdType = elemIdType;
}
}
}
}
}
}
/// <summary>
/// Static type metadata cache - thread-safe because shared across all serialization operations
/// 🔑 OPTIMIZATION: Uses FrozenDictionary for hot-path lookups after warmup
/// </summary>
public static class TypeCache
{
private static readonly ConcurrentDictionary<Type, (bool IsId, Type? IdType)> _idCache = new();
private static readonly ConcurrentDictionary<Type, Type?> _collectionElemCache = new();
private static readonly ConcurrentDictionary<Type, string> _typeNameCache = new();
private static readonly ConcurrentDictionary<Type, CachedPropertyInfo[]> _cachedPropertyInfoCache = new();
private static readonly ConcurrentDictionary<PropertyInfo, bool> _jsonIgnoreCache = new();
private static readonly ConcurrentDictionary<Type, bool> _isPrimitiveCache = new();
private static readonly ConcurrentDictionary<Type, bool> _isPrimitiveElementCollectionCache = new();
private static readonly ConcurrentDictionary<Type, bool> _isCollectionTypeCache = new();
// 🔑 Type ID cache for long-based semantic IDs
private static readonly ConcurrentDictionary<Type, int> _typeIdCache = new();
/// <summary>
/// Static type metadata cache for semantic ID generation.
/// </summary>
public static class TypeCache
{
private static readonly ConcurrentDictionary<Type, string> TypeNameCache = new();
private static readonly ConcurrentDictionary<Type, CachedPropertyInfo[]> CachedPropertyInfoCache = new();
private static readonly ConcurrentDictionary<Type, int> TypeIdCache = new();
private static int _typeIdCounter;
// 🔑 OPTIMIZATION: Pre-computed primitive types set
private static readonly FrozenSet<Type> PrimitiveTypes = new HashSet<Type>
{
typeof(string), typeof(decimal), typeof(DateTime),
typeof(DateTimeOffset), typeof(Guid), typeof(TimeSpan),
typeof(bool), typeof(byte), typeof(sbyte), typeof(short),
typeof(ushort), typeof(int), typeof(uint), typeof(long),
typeof(ulong), typeof(float), typeof(double), typeof(char)
}.ToFrozenSet();
// 🔑 OPTIMIZATION: Cache generic type definitions to avoid repeated GetGenericTypeDefinition calls
private static readonly Type IEnumerableGenericType = typeof(IEnumerable<>);
private static readonly Type IIdGenericType = typeof(IId<>);
private static readonly Type NullableGenericType = typeof(Nullable<>);
/// <summary>
/// Gets a unique integer ID for a type (thread-safe, consistent within app lifetime)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetTypeId(Type t)
public static int GetTypeId(Type t) => TypeIdCache.GetOrAdd(t, _ => Interlocked.Increment(ref _typeIdCounter));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long CreateSemanticId(int typeId, long objectId) => ((long)typeId << 48) | (objectId & 0xFFFFFFFFFFFF);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetTypeName(Type t) => TypeNameCache.GetOrAdd(t, static type => type.Name);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static CachedPropertyInfo[] GetCachedProperties(Type t)
{
return _typeIdCache.GetOrAdd(t, _ => Interlocked.Increment(ref _typeIdCounter));
return CachedPropertyInfoCache.GetOrAdd(t, static type =>
{
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var cached = new CachedPropertyInfo[props.Length];
for (var i = 0; i < props.Length; i++)
cached[i] = new CachedPropertyInfo(props[i]);
return cached;
});
}
/// <summary>
/// Creates a long-based semantic ID from type ID and object ID.
/// Format: [16 bits typeId][48 bits objectId]
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long CreateSemanticId(int typeId, long objectId)
{
return ((long)typeId << 48) | (objectId & 0xFFFFFFFFFFFF);
}
/// <summary>
/// Extracts the type ID from a semantic ID
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ExtractTypeId(long semanticId) => (int)(semanticId >> 48);
/// <summary>
/// Extracts the object ID from a semantic ID
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long ExtractObjectId(long semanticId) => semanticId & 0xFFFFFFFFFFFF;
/// <summary>
/// Converts any ID value to a long for semantic ID creation.
/// Supports: int, long, Guid, short, byte, uint, ulong, ushort.
/// Throws for unsupported types to prevent cross-process hash inconsistencies.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long IdToLong(object idValue)
{
@ -145,16 +104,10 @@ namespace AyCode.Core.Extensions
ulong ul => (long)ul,
ushort us => us,
sbyte sb => sb,
_ => throw new NotSupportedException(
$"ID type '{idValue.GetType().Name}' is not supported for semantic ID generation. " +
$"Supported types: int, long, Guid, short, byte, uint, ulong, ushort, sbyte. " +
$"Using GetHashCode() would cause inconsistent IDs between client and server.")
_ => throw new NotSupportedException($"ID type '{idValue.GetType().Name}' is not supported for semantic ID generation.")
};
}
/// <summary>
/// 🔑 OPTIMIZATION: Generic ID to long conversion without boxing for common types
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long IdToLong<TId>(TId id) where TId : struct
{
@ -167,8 +120,6 @@ namespace AyCode.Core.Extensions
if (typeof(TId) == typeof(ulong)) return (long)Unsafe.As<TId, ulong>(ref id);
if (typeof(TId) == typeof(ushort)) return Unsafe.As<TId, ushort>(ref id);
if (typeof(TId) == typeof(sbyte)) return Unsafe.As<TId, sbyte>(ref id);
// Fallback with boxing for unknown types
return IdToLong((object)id);
}
@ -177,128 +128,16 @@ namespace AyCode.Core.Extensions
{
Span<byte> bytes = stackalloc byte[16];
guid.TryWriteBytes(bytes);
var high = BitConverter.ToInt64(bytes);
var low = BitConverter.ToInt64(bytes.Slice(8));
return high ^ low;
return BitConverter.ToInt64(bytes) ^ BitConverter.ToInt64(bytes.Slice(8));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetTypeName(Type t)
{
return _typeNameCache.GetOrAdd(t, static type => type.Name);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (bool IsId, Type? IdType) GetIdInfo(Type t)
{
return _idCache.GetOrAdd(t, static type =>
{
var interfaces = type.GetInterfaces();
for (var i = 0; i < interfaces.Length; i++)
{
var iface = interfaces[i];
if (!iface.IsGenericType) continue;
if (iface.GetGenericTypeDefinition() != IIdGenericType) continue;
var idType = iface.GetGenericArguments()[0];
return (idType.IsValueType, idType);
}
return (false, null);
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Type? GetElementType(Type t)
{
return _collectionElemCache.GetOrAdd(t, static type =>
{
if (type.IsArray) return type.GetElementType();
if (type.IsGenericType && type.GetGenericTypeDefinition() == IEnumerableGenericType)
return type.GetGenericArguments()[0];
var interfaces = type.GetInterfaces();
for (var i = 0; i < interfaces.Length; i++)
{
var iface = interfaces[i];
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IEnumerableGenericType)
return iface.GetGenericArguments()[0];
}
return null;
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static CachedPropertyInfo[] GetCachedProperties(Type t)
{
return _cachedPropertyInfoCache.GetOrAdd(t, static type =>
{
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var cached = new CachedPropertyInfo[props.Length];
for (var i = 0; i < props.Length; i++)
cached[i] = new CachedPropertyInfo(props[i]);
return cached;
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasJsonIgnoreAttribute(PropertyInfo prop)
{
return _jsonIgnoreCache.GetOrAdd(prop, static p =>
p.GetCustomAttribute<JsonIgnoreAttribute>() != null ||
p.GetCustomAttribute<System.Text.Json.Serialization.JsonIgnoreAttribute>() != null);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitive(Type t)
{
return _isPrimitiveCache.GetOrAdd(t, static type =>
{
if (type.IsPrimitive || PrimitiveTypes.Contains(type)) return true;
if (type.IsGenericType && type.GetGenericTypeDefinition() == NullableGenericType)
return IsPrimitive(type.GetGenericArguments()[0]);
return false;
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveElementCollection(Type type)
{
return _isPrimitiveElementCollectionCache.GetOrAdd(type, static t =>
{
if (t == typeof(string)) return false;
Type? elementType = null;
if (t.IsArray)
elementType = t.GetElementType();
else if (t.IsGenericType && typeof(IEnumerable).IsAssignableFrom(t))
{
var genericArgs = t.GetGenericArguments();
if (genericArgs.Length == 1) elementType = genericArgs[0];
}
if (elementType == null) return false;
return IsPrimitive(elementType) || elementType.IsEnum;
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsCollectionType(Type type)
{
return _isCollectionTypeCache.GetOrAdd(type, static t =>
{
if (t == typeof(string) || t.IsPrimitive) return false;
return t.IsArray || typeof(IEnumerable).IsAssignableFrom(t);
});
}
}
/// <summary>
/// Converter for IId collections that supports merging by ID (update existing, add new, keep unmentioned)
/// 🔑 OPTIMIZATION: Uses pre-computed type ID and EqualityComparer
/// </summary>
public class IdAwareCollectionMergeConverter<TItem, TId> : JsonConverter
/// <summary>
/// Converter for IId collections that supports merging by ID.
/// </summary>
public class IdAwareCollectionMergeConverter<TItem, TId> : JsonConverter
where TItem : class, IId<TId>, new() where TId : struct
{
{
private static readonly int CachedTypeId = TypeCache.GetTypeId(typeof(TItem));
private static readonly EqualityComparer<TId> IdComparer = EqualityComparer<TId>.Default;
@ -325,7 +164,6 @@ namespace AyCode.Core.Extensions
var jsonCount = jsonArray.Count;
var existingCount = targetList.Count;
// 🔑 OPTIMIZATION: Pre-sized dictionary with exact capacity
var existingItemsMap = new Dictionary<long, TItem>(existingCount);
for (var index = 0; index < existingCount; index++)
{
@ -333,9 +171,7 @@ namespace AyCode.Core.Extensions
existingItemsMap[GetSemanticId(item.Id)] = item;
}
// 🔑 OPTIMIZATION: Pre-sized collections
var estimatedCapacity = jsonCount + existingCount;
var finalItems = new List<TItem>(estimatedCapacity);
var finalItems = new List<TItem>(jsonCount + existingCount);
var processedIds = new HashSet<long>(jsonCount);
for (var i = 0; i < jsonCount; i++)
@ -358,32 +194,23 @@ namespace AyCode.Core.Extensions
itemResult = existingItem;
}
else
{
itemResult = jObj.ToObject<TItem>(serializer);
}
}
else
{
itemResult = itemToken.ToObject<TItem>(serializer);
}
if (itemResult == null) continue;
var currentId = itemResult.Id;
var isIdentifiable = !IdComparer.Equals(currentId, default);
if (isIdentifiable)
if (!IdComparer.Equals(currentId, default))
{
if (processedIds.Add(GetSemanticId(currentId)))
finalItems.Add(itemResult);
}
else
{
finalItems.Add(itemResult);
}
}
// KEEP logic - add items that weren't in the JSON
foreach (var kvp in existingItemsMap)
{
if (processedIds.Add(kvp.Key))
@ -412,17 +239,15 @@ namespace AyCode.Core.Extensions
return targetList;
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) =>
throw new InvalidOperationException("IdAwareCollectionMergeConverter is read-only.");
}
}
}
/// <summary>
/// 🔑 OPTIMIZATION: Static class with inlined ID extraction methods
/// </summary>
public static class IdExtractor
{
/// <summary>
/// Static class with inlined ID extraction methods.
/// </summary>
public static class IdExtractor
{
private static readonly string IdPropertyName = nameof(IId<int>.Id);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -431,7 +256,6 @@ namespace AyCode.Core.Extensions
var idPropToken = obj.GetValue(IdPropertyName, StringComparison.OrdinalIgnoreCase);
if (idPropToken == null || idPropToken.Type == JTokenType.Null) return default;
// 🔑 OPTIMIZATION: Fast path for common types - JIT eliminates dead branches
if (typeof(TId) == typeof(int))
{
if (idPropToken.Type == JTokenType.Integer)
@ -462,25 +286,22 @@ namespace AyCode.Core.Extensions
try { return idPropToken.Value<TId>(); }
catch { return default; }
}
}
}
public class UnifiedMergeContractResolver : DefaultContractResolver
{
public class UnifiedMergeContractResolver : DefaultContractResolver
{
private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> CollectionConverterCache = new();
private static readonly ConcurrentDictionary<MemberInfo, bool> NoMergeAttributeCache = new();
private static readonly ConcurrentDictionary<MemberInfo, CachedPropertyConfig> PropertyConfigCache = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool HasNoMergeAttribute(MemberInfo member)
{
return NoMergeAttributeCache.GetOrAdd(member, static m =>
m.GetCustomAttribute<JsonNoMergeCollectionAttribute>() != null);
}
private static bool HasNoMergeAttribute(MemberInfo member) =>
NoMergeAttributeCache.GetOrAdd(member, static m => m.GetCustomAttribute<JsonNoMergeCollectionAttribute>() != null);
protected override JsonArrayContract CreateArrayContract(Type objectType)
{
var contract = base.CreateArrayContract(objectType);
if (TypeCache.IsPrimitiveElementCollection(objectType))
if (IsPrimitiveElementCollection(objectType))
{
contract.ItemIsReference = false;
contract.IsReference = false;
@ -523,31 +344,29 @@ namespace AyCode.Core.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static CachedPropertyConfig GetOrCreatePropertyConfig(MemberInfo member, Type propertyType)
{
return PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType));
}
=> PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType));
private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType)
{
var config = new CachedPropertyConfig
{
IsPrimitiveElementCollection = TypeCache.IsPrimitiveElementCollection(propertyType),
IsPrimitiveElementCollection = IsPrimitiveElementCollection(propertyType),
IsExcludedFromMerge = HasNoMergeAttribute(member),
IsCollection = TypeCache.IsCollectionType(propertyType)
IsCollection = IsGenericCollectionType(propertyType)
};
if (config.IsCollection)
{
config.ElementType = TypeCache.GetElementType(propertyType);
config.ElementType = GetCollectionElementType(propertyType);
if (config.ElementType != null)
{
var (hasId, elemIdType) = TypeCache.GetIdInfo(config.ElementType);
var (hasId, elemIdType) = GetIdInfo(config.ElementType);
if (hasId && elemIdType != null)
{
config.IsIdCollection = true;
config.IdType = elemIdType;
}
config.IsPrimitiveElement = TypeCache.IsPrimitive(config.ElementType);
config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType);
}
}
@ -564,27 +383,24 @@ namespace AyCode.Core.Extensions
public Type? IdType { get; set; }
public bool IsPrimitiveElement { get; set; }
}
}
}
public static class JsonPopulateExtensions
{
public static class JsonPopulateExtensions
{
private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> RootConverterCache = new();
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
// Cached serializer for merge operations
private static readonly JsonSerializer CachedMergeSerializer = CreateMergeSerializer();
private static JsonSerializer CreateMergeSerializer()
{
var settings = new JsonSerializerSettings
return JsonSerializer.Create(new JsonSerializerSettings
{
ContractResolver = SharedContractResolver,
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
};
return JsonSerializer.Create(settings);
});
}
public static void DeepPopulateWithMerge<T>(this T target, string json, JsonSerializerSettings? settings = null) where T : notnull
@ -592,29 +408,18 @@ namespace AyCode.Core.Extensions
ArgumentNullException.ThrowIfNull(target);
ArgumentNullException.ThrowIfNull(json);
// Use centralized unwrap helper
json = JsonUtilities.UnwrapJsonString(json);
// Create a local resolver indicating this serializer is used for a MERGE operation
json = UnwrapJsonString(json);
var resolver = new HybridReferenceResolver(isForMerge: true, estimatedObjectCount: 32);
JsonSerializer serializer;
if (settings == null)
{
// Fast path: reuse cached serializer
serializer = CachedMergeSerializer;
}
else
{
settings.ContractResolver ??= SharedContractResolver;
var serializerSettings = new JsonSerializerSettings(settings)
{
ReferenceResolverProvider = () => resolver
};
serializer = JsonSerializer.Create(serializerSettings);
serializer = JsonSerializer.Create(new JsonSerializerSettings(settings) { ReferenceResolverProvider = () => resolver });
}
// Temporarily set the reference resolver for cached serializer
var originalResolver = serializer.ReferenceResolver;
serializer.ReferenceResolver = resolver;
@ -623,18 +428,17 @@ namespace AyCode.Core.Extensions
if (target is IList targetList)
{
var type = target.GetType();
var elemType = TypeCache.GetElementType(type);
var elemType = GetCollectionElementType(type);
if (elemType != null)
{
var (isId, idType) = TypeCache.GetIdInfo(elemType);
var (isId, idType) = GetIdInfo(elemType);
if (isId && idType != null)
{
var converterInstance = RootConverterCache.GetOrAdd((elemType, idType), static k =>
(JsonConverter)Activator.CreateInstance(
typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!);
// Use JToken for collection merge (needed for complex logic)
var token = JToken.Parse(json);
using var reader = token.CreateReader();
converterInstance.ReadJson(reader, target.GetType(), target, serializer);
@ -643,7 +447,6 @@ namespace AyCode.Core.Extensions
}
}
// For non-collection targets, use direct JsonTextReader for better performance
using var stringReader = new StringReader(json);
using var jsonReader = new JsonTextReader(stringReader);
serializer.Populate(jsonReader, target);
@ -653,5 +456,4 @@ namespace AyCode.Core.Extensions
serializer.ReferenceResolver = originalResolver;
}
}
}
}

View File

@ -1,14 +1,13 @@
using AyCode.Core.Interfaces;
using MessagePack;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System.Buffers;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text;
using AyCode.Core.Interfaces;
using MessagePack;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using static AyCode.Core.Extensions.JsonUtilities;
namespace AyCode.Core.Extensions;
@ -36,8 +35,7 @@ internal static class Base62
value /= 62;
}
if (isNegative)
buffer[--index] = '-';
if (isNegative) buffer[--index] = '-';
return new string(buffer[index..]);
}
@ -53,7 +51,7 @@ public class HybridReferenceResolver : IReferenceResolver
internal HashSet<string>? _referencedIds;
private int _nextNumericId = 1;
private static readonly ConcurrentDictionary<Type, Func<object, object?>> _idGetterCache = new();
private static readonly ConcurrentDictionary<Type, Func<object, object?>> IdGetterCache = new();
public bool IsForMerge { get; }
private readonly int _estimatedObjectCount;
@ -86,13 +84,12 @@ public class HybridReferenceResolver : IReferenceResolver
var objectToId = GetObjectToId();
if (objectToId.TryGetValue(value, out var existingId))
{
if (!IsForMerge)
ReferencedIds.Add(existingId);
if (!IsForMerge) ReferencedIds.Add(existingId);
return existingId;
}
var type = value.GetType();
var (isId, idType) = TypeCache.GetIdInfo(type);
var (isId, idType) = GetIdInfo(type);
string newRef;
if (isId && idType != null)
@ -108,14 +105,10 @@ public class HybridReferenceResolver : IReferenceResolver
newRef = Base62.Encode(semanticId);
}
else
{
newRef = Base62.Encode(-_nextNumericId++);
}
}
else
{
newRef = Base62.Encode(-_nextNumericId++);
}
GetIdToObject()[newRef] = value;
objectToId[value] = newRef;
@ -130,7 +123,7 @@ public class HybridReferenceResolver : IReferenceResolver
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Func<object, object?> GetOrCreateIdGetter(Type type) =>
_idGetterCache.GetOrAdd(type, static t =>
IdGetterCache.GetOrAdd(type, static t =>
{
var prop = t.GetProperty("Id");
if (prop == null) return static _ => null;
@ -138,44 +131,18 @@ public class HybridReferenceResolver : IReferenceResolver
if (getMethod == null) return static _ => null;
return obj => getMethod.Invoke(obj, null);
});
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDefaultValue(object value, Type type)
{
if (type == typeof(int)) return (int)value == 0;
if (type == typeof(long)) return (long)value == 0L;
if (type == typeof(Guid)) return (Guid)value == Guid.Empty;
if (type == typeof(short)) return (short)value == 0;
if (type == typeof(byte)) return (byte)value == 0;
if (type == typeof(uint)) return (uint)value == 0;
if (type == typeof(ulong)) return (ulong)value == 0;
if (type == typeof(ushort)) return (ushort)value == 0;
if (type == typeof(sbyte)) return (sbyte)value == 0;
return false;
}
}
internal sealed class ReferenceEqualityComparer : IEqualityComparer<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);
if (!json.Contains(IdMarker)) return json;
return referencedIds == null || referencedIds.Count == 0
? RemoveAllIdsSpan(json)
: RemoveUnreferencedIdsSpan(json, referencedIds);
}
private static string RemoveAllIdsSpan(string json)
@ -189,16 +156,14 @@ internal static class JsonReferencePostProcessor
var idIndex = json.IndexOf(IdMarker, searchStart, StringComparison.Ordinal);
if (idIndex < 0) break;
if (idIndex > lastCopyEnd)
sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
if (idIndex > lastCopyEnd) sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
var endIndex = SkipIdEntry(json, idIndex);
lastCopyEnd = endIndex;
searchStart = endIndex;
}
if (lastCopyEnd < json.Length)
sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
if (lastCopyEnd < json.Length) sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
return sb.Length == json.Length ? json : sb.ToString();
}
@ -225,9 +190,7 @@ internal static class JsonReferencePostProcessor
{
valueStart++;
valueEnd = valueStart;
while (valueEnd < json.Length && json[valueEnd] != '"')
valueEnd++;
while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++;
idValue = json.Substring(valueStart, valueEnd - valueStart);
valueEnd++;
}
@ -236,24 +199,17 @@ internal static class JsonReferencePostProcessor
valueEnd++;
if (idValue != null && referencedIds.Contains(idValue))
{
searchStart = valueEnd;
}
else
{
if (idIndex > lastCopyEnd)
sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
if (idIndex > lastCopyEnd) sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
lastCopyEnd = valueEnd;
searchStart = valueEnd;
}
}
if (lastCopyEnd == 0)
return json;
if (lastCopyEnd < json.Length)
sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
if (lastCopyEnd == 0) return json;
if (lastCopyEnd < json.Length) sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
return sb.ToString();
}
@ -262,32 +218,29 @@ internal static class JsonReferencePostProcessor
private static int SkipIdEntry(string json, int idIndex)
{
var pos = idIndex + IdMarker.Length;
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ':'))
pos++;
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ':')) pos++;
if (pos < json.Length && json[pos] == '"')
{
pos++;
while (pos < json.Length && json[pos] != '"')
pos++;
if (pos < json.Length)
pos++;
while (pos < json.Length && json[pos] != '"') pos++;
if (pos < json.Length) pos++;
}
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ','))
pos++;
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ',')) pos++;
return pos;
}
public static HashSet<string> CollectReferencedIds(string json)
{
const string refMarker = "\"$ref\"";
var result = new HashSet<string>(StringComparer.Ordinal);
var searchStart = 0;
while (searchStart < json.Length)
{
var refIndex = json.IndexOf(RefMarker, searchStart, StringComparison.Ordinal);
var refIndex = json.IndexOf(refMarker, searchStart, StringComparison.Ordinal);
if (refIndex < 0) break;
var valueStart = refIndex + RefMarker.Length;
var valueStart = refIndex + refMarker.Length;
while (valueStart < json.Length && (json[valueStart] == ' ' || json[valueStart] == ':'))
valueStart++;
@ -295,19 +248,13 @@ internal static class JsonReferencePostProcessor
{
valueStart++;
var valueEnd = valueStart;
while (valueEnd < json.Length && json[valueEnd] != '"')
valueEnd++;
if (valueEnd > valueStart)
result.Add(json.Substring(valueStart, valueEnd - valueStart));
while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++;
if (valueEnd > valueStart) result.Add(json.Substring(valueStart, valueEnd - valueStart));
searchStart = valueEnd + 1;
}
else
{
searchStart = valueStart;
}
}
return result;
}
@ -332,20 +279,12 @@ internal sealed class PooledStringWriter : StringWriter
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
StringBuilderPool.Return(_pooledBuilder);
}
if (!_disposed) { _disposed = true; StringBuilderPool.Return(_pooledBuilder); }
base.Dispose(disposing);
}
}
internal interface ObjectPool<T> where T : class
{
T Get();
void Return(T obj);
}
internal interface ObjectPool<T> where T : class { T Get(); void Return(T obj); }
internal sealed class DefaultObjectPool<T> : ObjectPool<T> where T : class
{
@ -385,7 +324,6 @@ public static class SerializeObjectExtensions
{
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
private static readonly Dictionary<object, object> EmptyContextDict = new();
private static readonly JsonSerializer CachedSerializer = CreateCachedSerializer();
public static JsonSerializerSettings Options => new()
{
@ -399,143 +337,113 @@ public static class SerializeObjectExtensions
Formatting = Formatting.None,
};
private static JsonSerializer CreateCachedSerializer() => JsonSerializer.Create(new JsonSerializerSettings
{
ContractResolver = SharedContractResolver,
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
Formatting = Formatting.None,
});
/// <summary>
/// Serialize object to JSON string with default options.
/// </summary>
public static string ToJson<T>(this T source)
=> AcJsonSerializer.Serialize(source);
/// <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.
/// Serialize object to JSON string with specified options.
/// </summary>
public static string ToJson<T>(this T source, JsonSerializerSettings? options = null)
public static string ToJson<T>(this T source, AcJsonSerializerOptions options)
=> AcJsonSerializer.Serialize(source, options);
public static string ToJson<T>(this IQueryable<T> source) where T : class, IAcSerializableToJson
=> AcJsonSerializer.Serialize(source);
public static string ToJson<T>(this IQueryable<T> source, AcJsonSerializerOptions options) where T : class, IAcSerializableToJson
=> AcJsonSerializer.Serialize(source, options);
public static string ToJson<T>(this IEnumerable<T> source) where T : class, IAcSerializableToJson
=> AcJsonSerializer.Serialize(source);
public static string ToJson<T>(this IEnumerable<T> source, AcJsonSerializerOptions options) where T : class, IAcSerializableToJson
=> AcJsonSerializer.Serialize(source, options);
/// <summary>
/// Deserialize JSON to object with default options.
/// </summary>
public static T? JsonTo<T>(this string json)
{
// If custom options are provided, use Newtonsoft for full compatibility
//if (options != null)
//{
// return JsonConvert.SerializeObject(source, options);
//}
// Use our high-performance custom serializer
return AcJsonSerializer.Serialize(source);
// ========================================================================
// OLD IMPLEMENTATION - Newtonsoft with HybridReferenceResolver
// Uncomment below and comment out the AcJsonSerializer.Serialize line above to rollback
// ========================================================================
// var resolver = new HybridReferenceResolver(estimatedObjectCount: 256);
// var serializer = CachedSerializer;
//
// string json;
// using (var sw = PooledStringWriter.Rent())
// {
// var originalResolver = serializer.ReferenceResolver;
// serializer.ReferenceResolver = resolver;
// try
// {
// serializer.Serialize(sw, source);
// json = sw.ToString();
// }
// finally
// {
// serializer.ReferenceResolver = originalResolver;
// }
// }
//
// // Skip post-processing if no $id in output
// if (!json.Contains("\"$id\""))
// return json;
//
// // If we tracked references, use them
// if (resolver._referencedIds?.Count > 0)
// return JsonReferencePostProcessor.RemoveUnreferencedIds(json, resolver._referencedIds);
//
// // No references and no $ref - remove all $id
// if (!json.Contains("\"$ref\""))
// return JsonReferencePostProcessor.RemoveUnreferencedIds(json, null);
//
// // Fallback: scan JSON for $ref values
// var referenced = JsonReferencePostProcessor.CollectReferencedIds(json);
// return JsonReferencePostProcessor.RemoveUnreferencedIds(json, referenced);
}
public static string ToJson<T>(this IQueryable<T> source, JsonSerializerSettings? options = null) where T : class, IAcSerializableToJson
//=> options != null ? JsonConvert.SerializeObject(source, options) : AcJsonSerializer.Serialize(source);
=> ((object)source).ToJson(options);
public static string ToJson<T>(this IEnumerable<T> source, JsonSerializerSettings? options = null) where T : class, IAcSerializableToJson
//=> options != null ? JsonConvert.SerializeObject(source, options) : AcJsonSerializer.Serialize(source);
=> ((object)source).ToJson(options);
public static T? JsonTo<T>(this string json, JsonSerializerSettings? options = null)
{
json = JsonUtilities.UnwrapJsonString(json);
json = UnwrapJsonString(json);
return AcJsonDeserializer.Deserialize<T>(json);
// Use our high-performance custom deserializer
// AcJsonDeserializer now supports primitives, enums, and complex types
//if (options == null)
//{
// try
// {
// return AcJsonDeserializer.Deserialize<T>(json);
// }
// catch
// {
// // Fallback to Newtonsoft if custom deserializer fails
// }
//}
//return JsonConvert.DeserializeObject<T>(json, options ?? Options);
}
public static object? JsonTo(this string json, Type toType, JsonSerializerSettings? options = null)
/// <summary>
/// Deserialize JSON to object with specified options.
/// </summary>
public static T? JsonTo<T>(this string json, AcJsonSerializerOptions options)
{
json = JsonUtilities.UnwrapJsonString(json);
json = UnwrapJsonString(json);
return AcJsonDeserializer.Deserialize<T>(json, options);
}
/// <summary>
/// Deserialize JSON to specified type with default options.
/// </summary>
public static object? JsonTo(this string json, Type toType)
{
json = UnwrapJsonString(json);
return AcJsonDeserializer.Deserialize(json, toType);
//// Use our high-performance custom deserializer
//// AcJsonDeserializer now supports primitives, enums, and complex types
//if (options == null)
//{
// try
// {
// return AcJsonDeserializer.Deserialize(json, toType);
// }
// catch
// {
// // Fallback to Newtonsoft if custom deserializer fails
// }
//}
//return JsonConvert.DeserializeObject(json, toType, options ?? Options);
}
public static void JsonTo(this string json, object target, JsonSerializerSettings? options = null)
/// <summary>
/// Deserialize JSON to specified type with specified options.
/// </summary>
public static object? JsonTo(this string json, Type toType, AcJsonSerializerOptions options)
{
json = JsonUtilities.UnwrapJsonString(json);
json = UnwrapJsonString(json);
return AcJsonDeserializer.Deserialize(json, toType, options);
}
// Use runtime type instead of compile-time type for Populate
/// <summary>
/// Populate existing object from JSON with default options.
/// </summary>
public static void JsonTo(this string json, object target)
{
json = UnwrapJsonString(json);
AcJsonDeserializer.Populate(json, target);
}
[return: NotNullIfNotNull(nameof(src))]
public static TDestination? CloneTo<TDestination>(this object? src, JsonSerializerSettings? options = null) where TDestination : class
/// <summary>
/// Populate existing object from JSON with specified options.
/// </summary>
public static void JsonTo(this string json, object target, AcJsonSerializerOptions options)
{
json = UnwrapJsonString(json);
AcJsonDeserializer.Populate(json, target, options);
}
/// <summary>
/// Clone object via JSON serialization with default options.
/// </summary>
public static TDestination? CloneTo<TDestination>(this object? src) where TDestination : class
=> src?.ToJson().JsonTo<TDestination>();
/// <summary>
/// Clone object via JSON serialization with specified options.
/// </summary>
public static TDestination? CloneTo<TDestination>(this object? src, AcJsonSerializerOptions options) where TDestination : class
=> src?.ToJson(options).JsonTo<TDestination>(options);
public static void CopyTo(this object? src, object target, JsonSerializerSettings? options = null)
/// <summary>
/// Copy object properties to target via JSON with default options.
/// </summary>
public static void CopyTo(this object? src, object target)
=> src?.ToJson().JsonTo(target);
/// <summary>
/// Copy object properties to target via JSON with specified options.
/// </summary>
public static void CopyTo(this object? src, object target, AcJsonSerializerOptions options)
=> src?.ToJson(options).JsonTo(target, options);
//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);
public static byte[] ToMessagePack(this object message, MessagePackSerializerOptions options)
=> MessagePackSerializer.Serialize(message, options);
public static T MessagePackTo<T>(this byte[] message, MessagePackSerializerOptions options)
=> MessagePackSerializer.Deserialize<T>(message, options);
}
public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContractResolver

View File

@ -340,6 +340,7 @@ namespace AyCode.Services.SignalRs
if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, TransportSendTimeout, MsDelay, MsFirstDelay) &&
_responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage<string> responseMessage)
{
startTime = obj.RequestDateTime;
SignalRRequestModelPool.Return(obj);
if (responseMessage.Status == SignalResponseStatus.Error || responseMessage.ResponseData == null)
@ -355,14 +356,16 @@ namespace AyCode.Services.SignalRs
//return default;
}
return responseMessage.ResponseData.JsonTo<TResponse>();
var responseData = responseMessage.ResponseData.JsonTo<TResponse>();
Logger.Info($"Client deserialized response json. Total: {(DateTime.UtcNow.Subtract(startTime)).TotalMilliseconds} ms! requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]");
return responseData;
}
Logger.Error($"Client timeout after: {(DateTime.Now - startTime).TotalSeconds} sec! ConnectionState: {GetConnectionState()}; requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]");
}
catch (Exception ex)
{
Logger.Error($"SendMessageToServerAsync; requestId: {requestId}; ConnectionState: {GetConnectionState()}; {ex.Message}; {ConstHelper.NameByValue(TagsName, messageTag)}", ex);
Logger.Error($"Client SendMessageToServerAsync; requestId: {requestId}; ConnectionState: {GetConnectionState()}; {ex.Message}; {ConstHelper.NameByValue(TagsName, messageTag)}", ex);
}
if (_responseByRequestId.TryRemove(requestId, out var removedModel))

View File

@ -215,17 +215,19 @@ public sealed class SignalResponseMessage<TResponseData> : ISignalResponseMessag
{
if (!_isDeserialized)
{
_isDeserialized = true;
_responseData = ResponseDataJson != null
? ResponseDataJson.JsonTo<TResponseData>()
: default;
_isDeserialized = true;
}
return _responseData;
}
set
{
_responseData = value;
_isDeserialized = true;
_responseData = value;
ResponseDataJson = value?.ToJson();
}
}

View File

@ -27,7 +27,7 @@ public class SignalRRequestModel : IResettable
/// </summary>
public bool TryReset()
{
RequestDateTime = DateTime.UtcNow;
RequestDateTime = default;
ResponseDateTime = default;
ResponseByRequestId = null;
return true;

View File

@ -25,7 +25,6 @@ public class SerializationBenchmarks
// Settings
private JsonSerializerSettings _newtonsoftNoRefSettings = null!;
private JsonSerializerSettings _ayCodeSettings = null!;
[GlobalSetup]
public void Setup()
@ -39,9 +38,6 @@ public class SerializationBenchmarks
Formatting = Formatting.None
};
// AyCode WITH reference handling
_ayCodeSettings = SerializeObjectExtensions.Options;
// Create benchmark data using shared factory
// ~1500 objects: 5 items × 4 pallets × 3 measurements × 5 points = 300 points + containers
_testOrder = TestDataFactory.CreateBenchmarkOrder(
@ -52,7 +48,7 @@ public class SerializationBenchmarks
// Pre-serialize for deserialization benchmarks
_newtonsoftJson = JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings);
_ayCodeJson = _testOrder.ToJson(_ayCodeSettings);
_ayCodeJson = _testOrder.ToJson();
// Create target for populate benchmarks
_populateTarget = new TestOrder();
@ -77,7 +73,7 @@ public class SerializationBenchmarks
[Benchmark(Description = "AyCode (with refs)")]
[BenchmarkCategory("Serialize")]
public string Serialize_AyCode_WithRefs()
=> _testOrder.ToJson(_ayCodeSettings);
=> _testOrder.ToJson();
[Benchmark(Description = "AcJsonSerializer (custom)")]
[BenchmarkCategory("Serialize")]
@ -96,7 +92,7 @@ public class SerializationBenchmarks
[Benchmark(Description = "AyCode (with refs)")]
[BenchmarkCategory("Deserialize")]
public TestOrder? Deserialize_AyCode_WithRefs()
=> _ayCodeJson.JsonTo<TestOrder>(_ayCodeSettings);
=> _ayCodeJson.JsonTo<TestOrder>();
[Benchmark(Description = "AcJsonDeserializer (custom)")]
[BenchmarkCategory("Deserialize")]