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
}

File diff suppressed because it is too large Load Diff

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,18 +248,12 @@ internal static class JsonReferencePostProcessor
{
valueStart++;
var valueEnd = valueStart;
while (valueEnd < json.Length && json[valueEnd] != '"')
valueEnd++;
if (valueEnd > valueStart)
result.Add(json.Substring(valueStart, valueEnd - valueStart));
while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++;
if (valueEnd > valueStart) result.Add(json.Substring(valueStart, valueEnd - valueStart));
searchStart = valueEnd + 1;
}
else
{
searchStart = valueStart;
}
}
return result;
@ -332,20 +279,12 @@ internal sealed class PooledStringWriter : StringWriter
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
StringBuilderPool.Return(_pooledBuilder);
}
if (!_disposed) { _disposed = true; StringBuilderPool.Return(_pooledBuilder); }
base.Dispose(disposing);
}
}
internal interface ObjectPool<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)
{
// 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 T source, AcJsonSerializerOptions options)
=> AcJsonSerializer.Serialize(source, options);
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 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, 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) where T : class, IAcSerializableToJson
=> AcJsonSerializer.Serialize(source);
public static T? JsonTo<T>(this string json, JsonSerializerSettings? options = null)
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)
{
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);
// Use runtime type instead of compile-time type for Populate
json = UnwrapJsonString(json);
return AcJsonDeserializer.Deserialize(json, toType, options);
}
/// <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

@ -110,13 +110,13 @@ namespace AyCode.Services.SignalRs
/// <summary>
/// Gets the current connection state. Override in tests.
/// </summary>
protected virtual HubConnectionState GetConnectionState()
protected virtual HubConnectionState GetConnectionState()
=> HubConnection?.State ?? HubConnectionState.Disconnected;
/// <summary>
/// Checks if the connection is connected. Override in tests.
/// </summary>
protected virtual bool IsConnected()
protected virtual bool IsConnected()
=> GetConnectionState() == HubConnectionState.Connected;
/// <summary>
@ -162,13 +162,13 @@ namespace AyCode.Services.SignalRs
/// <summary>
/// Gets the pending requests dictionary for testing.
/// </summary>
protected ConcurrentDictionary<int, SignalRRequestModel> GetPendingRequests()
protected ConcurrentDictionary<int, SignalRRequestModel> GetPendingRequests()
=> _responseByRequestId;
/// <summary>
/// Clears all pending requests.
/// </summary>
protected void ClearPendingRequests()
protected void ClearPendingRequests()
=> _responseByRequestId.Clear();
/// <summary>
@ -214,7 +214,7 @@ namespace AyCode.Services.SignalRs
Logger.DebugConditional($"Client SendMessageToServerAsync sending; {nameof(requestId)}: {requestId}; ConnectionState: {GetConnectionState()}; {ConstHelper.NameByValue(TagsName, messageTag)}");
await StartConnection();
var msgp = message?.ToMessagePack(ContractlessStandardResolver.Options);
if (!IsConnected())
@ -273,13 +273,13 @@ namespace AyCode.Services.SignalRs
private static ISignalRMessage CreatePostMessage<TPostData>(TPostData postData)
{
var type = typeof(TPostData);
// Primitives, strings, enums, and value types should use IdMessage format
if (IsPrimitiveOrStringOrEnum(type))
{
return new SignalPostJsonDataMessage<IdMessage>(new IdMessage(postData!));
}
// Complex objects use direct serialization
return new SignalPostJsonDataMessage<TPostData>(postData);
}
@ -291,9 +291,9 @@ namespace AyCode.Services.SignalRs
/// </summary>
private static bool IsPrimitiveOrStringOrEnum(Type type)
{
return type == typeof(string) ||
type.IsEnum ||
type.IsValueType ||
return type == typeof(string) ||
type.IsEnum ||
type.IsValueType ||
type == typeof(DateTime);
}
@ -340,8 +340,9 @@ namespace AyCode.Services.SignalRs
if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, TransportSendTimeout, MsDelay, MsFirstDelay) &&
_responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage<string> responseMessage)
{
startTime = obj.RequestDateTime;
SignalRRequestModelPool.Return(obj);
if (responseMessage.Status == SignalResponseStatus.Error || responseMessage.ResponseData == null)
{
var errorText = $"Client SendMessageToServerAsync<TResponseData> response error; await; tag: {messageTag}; Status: {responseMessage.Status}; ConnectionState: {GetConnectionState()}; requestId: {requestId}";
@ -355,14 +356,16 @@ namespace AyCode.Services.SignalRs
//return default;
}
return responseMessage.ResponseData.JsonTo<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))
@ -375,7 +378,7 @@ namespace AyCode.Services.SignalRs
public virtual Task SendMessageToServerAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback)
=> SendMessageToServerAsync(messageTag, null, responseCallback);
public virtual Task SendMessageToServerAsync<TResponseData>(int messageTag, ISignalRMessage? message, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback)
public virtual Task SendMessageToServerAsync<TResponseData>(int messageTag, ISignalRMessage? message, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback)
{
if (messageTag == 0) Logger.Error($"SendMessageToServerAsync; messageTag == 0");
@ -444,7 +447,7 @@ namespace AyCode.Services.SignalRs
{
SignalRRequestModelPool.Return(removedModel);
}
// Request-response hibás eset - ne hívjuk meg a MessageReceived-et
return Task.CompletedTask;
}

View File

@ -215,17 +215,19 @@ public sealed class SignalResponseMessage<TResponseData> : ISignalResponseMessag
{
if (!_isDeserialized)
{
_responseData = ResponseDataJson != null
? ResponseDataJson.JsonTo<TResponseData>()
: default;
_isDeserialized = true;
_responseData = ResponseDataJson != null
? ResponseDataJson.JsonTo<TResponseData>()
: default;
}
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")]