diff --git a/AyCode.Core/Extensions/AcJsonDeserializer.cs b/AyCode.Core/Extensions/AcJsonDeserializer.cs index a502b45..d0bf33a 100644 --- a/AyCode.Core/Extensions/AcJsonDeserializer.cs +++ b/AyCode.Core/Extensions/AcJsonDeserializer.cs @@ -1,9 +1,12 @@ +using System.Buffers; using System.Collections; using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Globalization; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; using AyCode.Core.Helpers; using AyCode.Core.Interfaces; @@ -30,23 +33,28 @@ public class AcJsonDeserializationException : Exception /// /// High-performance custom JSON deserializer optimized for IId<T> reference handling. -/// Supports MaxDepth and UseReferenceHandling options. +/// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach). /// public static class AcJsonDeserializer { private static readonly ConcurrentDictionary TypeMetadataCache = new(); + + // Pre-computed JSON property names for fast lookup (UTF-8 bytes) + private static readonly byte[] RefPropertyUtf8 = "$ref"u8.ToArray(); + private static readonly byte[] IdPropertyUtf8 = "$id"u8.ToArray(); #region Public API /// /// Deserialize JSON string to a new object of type T with default options. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? Deserialize(string json) => Deserialize(json, AcJsonSerializerOptions.Default); /// /// Deserialize JSON string to a new object of type T with specified options. /// - public static T? Deserialize(string json, AcJsonSerializerOptions options) + public static T? Deserialize(string json, in AcJsonSerializerOptions options) { if (string.IsNullOrEmpty(json) || json == "null") return default; @@ -54,18 +62,30 @@ public static class AcJsonDeserializer try { - ValidateJson(json, targetType); - - if (TryDeserializePrimitive(json, targetType, out var primitiveResult)) + if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult)) return (T?)primitiveResult; - var context = new DeserializationContext(options); + ValidateJson(json, targetType); + + // Fast path for no reference handling - use Utf8JsonReader (streaming, no DOM) + if (!options.UseReferenceHandling) + { + return DeserializeWithUtf8Reader(json, options.MaxDepth); + } + + // Reference handling requires DOM for forward references using var doc = JsonDocument.Parse(json); - - var result = ReadValue(doc.RootElement, targetType, context, 0); - context.ResolveReferences(); - - return (T?)result; + var context = DeserializationContextPool.Get(options); + try + { + var result = ReadValue(doc.RootElement, targetType, context, 0); + context.ResolveReferences(); + return (T?)result; + } + finally + { + DeserializationContextPool.Return(context); + } } catch (AcJsonDeserializationException) { throw; } catch (System.Text.Json.JsonException ex) @@ -81,37 +101,48 @@ public static class AcJsonDeserializer /// /// Deserialize JSON string to specified type with default options. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default); /// /// Deserialize JSON string to specified type with specified options. /// - public static object? Deserialize(string json, Type targetType, AcJsonSerializerOptions options) + public static object? Deserialize(string json, in Type targetType, in AcJsonSerializerOptions options) { if (string.IsNullOrEmpty(json) || json == "null") return null; try { - ValidateJson(json, targetType); + var firstChar = json[0]; + var isArrayJson = firstChar == '['; + var isObjectJson = firstChar == '{'; - var isArrayJson = json.Length > 0 && json[0] == '['; - var isObjectJson = json.Length > 0 && json[0] == '{'; - var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType); - var isDictType = IsDictionaryType(targetType, out _, out _); - - if (!isArrayJson && !isCollectionType && !(isObjectJson && isDictType)) + if (!isArrayJson && !isObjectJson) { - if (TryDeserializePrimitive(json, targetType, out var primitiveResult)) + if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult)) return primitiveResult; } - var context = new DeserializationContext(options); + ValidateJson(json, targetType); + + // Fast path for no reference handling + if (!options.UseReferenceHandling) + { + return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth); + } + using var doc = JsonDocument.Parse(json); - - var result = ReadValue(doc.RootElement, targetType, context, 0); - context.ResolveReferences(); - - return result; + var context = DeserializationContextPool.Get(options); + try + { + var result = ReadValue(doc.RootElement, targetType, context, 0); + context.ResolveReferences(); + return result; + } + finally + { + DeserializationContextPool.Return(context); + } } catch (AcJsonDeserializationException) { throw; } catch (System.Text.Json.JsonException ex) @@ -127,74 +158,115 @@ public static class AcJsonDeserializer /// /// Populate existing object with JSON data (merge mode) with default options. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Populate(string json, T target) where T : class => Populate(json, target, AcJsonSerializerOptions.Default); /// /// Populate existing object with JSON data (merge mode) with specified options. /// - public static void Populate(string json, T target, AcJsonSerializerOptions options) where T : class + public static void Populate(string json, T target, in AcJsonSerializerOptions options) where T : class { ArgumentNullException.ThrowIfNull(target); if (string.IsNullOrEmpty(json) || json == "null") return; - Populate(json, (object)target, target.GetType(), options); + PopulateInternal(json, target, target.GetType(), options); } /// /// Populate existing object with JSON data with default options. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Populate(string json, object target) => Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default); /// /// Populate existing object with JSON data with specified options. /// - public static void Populate(string json, object target, AcJsonSerializerOptions options) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Populate(string json, object target, in AcJsonSerializerOptions options) => Populate(json, target, target.GetType(), options); /// /// Populate existing object with JSON data with default options. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Populate(string json, object target, Type targetType) => Populate(json, target, targetType, AcJsonSerializerOptions.Default); /// /// Populate existing object with JSON data with specified options. /// - public static void Populate(string json, object target, Type targetType, AcJsonSerializerOptions options) + public static void Populate(string json, object target, in Type targetType, in AcJsonSerializerOptions options) { ArgumentNullException.ThrowIfNull(target); if (string.IsNullOrEmpty(json) || json == "null") return; - + PopulateInternal(json, target, targetType, options); + } + + private static void PopulateInternal(string json, object target, in Type targetType, in AcJsonSerializerOptions options) + { try { ValidateJson(json, targetType); - var context = new DeserializationContext(options) { IsMergeMode = true }; - using var doc = JsonDocument.Parse(json); + // Fast path for no reference handling - use Utf8JsonReader streaming + if (!options.UseReferenceHandling) + { + var firstChar = json[0]; + + if (firstChar == '[') + { + if (target is IList targetList) + PopulateListWithUtf8Reader(json, targetList, targetType, options.MaxDepth); + else + throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType); + return; + } + + if (firstChar == '{') + { + var metadata = GetTypeMetadata(targetType); + PopulateObjectWithUtf8Reader(json, target, metadata, options.MaxDepth); + return; + } + + throw new AcJsonDeserializationException($"Cannot populate object with JSON starting with '{firstChar}'", json, targetType); + } + // Reference handling requires DOM for forward references + using var doc = JsonDocument.Parse(json); var rootElement = doc.RootElement; - if (rootElement.ValueKind == JsonValueKind.Array) + var context = DeserializationContextPool.Get(options); + context.IsMergeMode = true; + + try { - if (target is IList targetList) - PopulateList(rootElement, targetList, targetType, context, 0); + if (rootElement.ValueKind == JsonValueKind.Array) + { + if (target is IList targetList) + PopulateList(rootElement, targetList, targetType, context, 0); + else + throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType); + + context.ResolveReferences(); + return; + } + + if (rootElement.ValueKind == JsonValueKind.Object) + { + var metadata = GetTypeMetadata(targetType); + PopulateObjectInternalMerge(rootElement, target, metadata, context, 0); + } else - throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType); + throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType); context.ResolveReferences(); - return; } - - if (rootElement.ValueKind == JsonValueKind.Object) + finally { - var metadata = GetTypeMetadata(targetType); - PopulateObjectInternalMerge(rootElement, target, metadata, context, 0); + DeserializationContextPool.Return(context); } - else - throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType); - - context.ResolveReferences(); } catch (AcJsonDeserializationException) { throw; } catch (System.Text.Json.JsonException ex) @@ -209,71 +281,202 @@ public static class AcJsonDeserializer #endregion - #region Validation + #region Utf8JsonReader Fast Path (STJ-style streaming) - private static void ValidateJson(string json, Type targetType) + /// + /// Deserialize using Utf8JsonReader - streaming without DOM allocation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T? DeserializeWithUtf8Reader(string json, byte maxDepth) { - if (string.IsNullOrEmpty(json)) return; - - if (json.Length > 2 && json[0] == '"' && json[^1] == '"') + var (buffer, length) = GetUtf8Bytes(json); + try { - var inner = json[1..^1]; - if (inner.Contains("\\\"") && (inner.Contains("{") || inner.Contains("["))) - throw new AcJsonDeserializationException( - $"Detected double-serialized JSON string. Target type: {targetType.Name}.", - json, targetType); + var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth }); + if (!reader.Read()) return default; + return (T?)ReadValueFromReader(ref reader, typeof(T), maxDepth, 0); + } + finally + { + ReturnUtf8Buffer(buffer); } - - var isArrayJson = json.Length > 0 && json[0] == '['; - var isObjectJson = json.Length > 0 && json[0] == '{'; - var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType); - var isDictType = IsDictionaryType(targetType, out _, out _); - - if (isArrayJson && !isCollectionType && !isDictType && targetType != typeof(object)) - throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType); - - if (isObjectJson && isCollectionType && !isDictType) - throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType); } - #endregion - - #region Core Reading Methods - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadValue(JsonElement element, Type targetType, DeserializationContext context, int depth) + private static object? DeserializeWithUtf8ReaderNonGeneric(string json, Type targetType, byte maxDepth) { - return element.ValueKind switch + var (buffer, length) = GetUtf8Bytes(json); + try { - JsonValueKind.Object => ReadObject(element, targetType, context, depth), - JsonValueKind.Array => ReadArray(element, targetType, context, depth), - JsonValueKind.Null => null, - _ => ReadPrimitive(element, targetType, element.ValueKind) + var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth }); + if (!reader.Read()) return null; + return ReadValueFromReader(ref reader, targetType, maxDepth, 0); + } + finally + { + ReturnUtf8Buffer(buffer); + } + } + + private static object? ReadValueFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth) + { + switch (reader.TokenType) + { + case JsonTokenType.Null: + return null; + case JsonTokenType.True: + return true; + case JsonTokenType.False: + return false; + case JsonTokenType.Number: + return ReadNumberFromReader(ref reader, targetType); + case JsonTokenType.String: + return ReadStringFromReader(ref reader, targetType); + case JsonTokenType.StartObject: + return ReadObjectFromReader(ref reader, targetType, maxDepth, depth); + case JsonTokenType.StartArray: + return ReadArrayFromReader(ref reader, targetType, maxDepth, depth); + default: + return null; + } + } + + /// + /// Read number value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType + Type.GetTypeCode calls). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadNumberFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo) + { + return propInfo.PropertyTypeCode switch + { + TypeCode.Int32 => reader.GetInt32(), + TypeCode.Int64 => reader.GetInt64(), + TypeCode.Double => reader.GetDouble(), + TypeCode.Decimal => reader.GetDecimal(), + TypeCode.Single => reader.GetSingle(), + TypeCode.Byte => reader.GetByte(), + TypeCode.Int16 => reader.GetInt16(), + TypeCode.UInt16 => reader.GetUInt16(), + TypeCode.UInt32 => reader.GetUInt32(), + TypeCode.UInt64 => reader.GetUInt64(), + TypeCode.SByte => reader.GetSByte(), + _ => propInfo.UnderlyingType.IsEnum ? Enum.ToObject(propInfo.UnderlyingType, reader.GetInt32()) : reader.GetDouble() + }; + } + + /// + /// Read string value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType calls). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadStringFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo) + { + var type = propInfo.UnderlyingType; + + if (ReferenceEquals(type, StringType)) return reader.GetString(); + if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime(); + if (ReferenceEquals(type, GuidType)) return reader.GetGuid(); + if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset(); + if (ReferenceEquals(type, TimeSpanType)) + { + var str = reader.GetString(); + return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan); + } + if (type.IsEnum) + { + var str = reader.GetString(); + return str != null ? Enum.Parse(type, str) : null; + } + + return reader.GetString(); + } + + /// + /// Read value from reader using cached PropertySetterInfo for faster type resolution. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadValueFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo, byte maxDepth, int depth) + { + switch (reader.TokenType) + { + case JsonTokenType.Null: + return null; + case JsonTokenType.True: + return true; + case JsonTokenType.False: + return false; + case JsonTokenType.Number: + return ReadNumberFromReaderCached(ref reader, propInfo); + case JsonTokenType.String: + return ReadStringFromReaderCached(ref reader, propInfo); + case JsonTokenType.StartObject: + return ReadObjectFromReader(ref reader, propInfo.PropertyType, maxDepth, depth); + case JsonTokenType.StartArray: + return ReadArrayFromReader(ref reader, propInfo.PropertyType, maxDepth, depth); + default: + return null; + } + } + + private static object? ReadNumberFromReader(ref Utf8JsonReader reader, Type targetType) + { + var type = Nullable.GetUnderlyingType(targetType) ?? targetType; + var typeCode = Type.GetTypeCode(type); + + return typeCode switch + { + TypeCode.Int32 => reader.GetInt32(), + TypeCode.Int64 => reader.GetInt64(), + TypeCode.Double => reader.GetDouble(), + TypeCode.Decimal => reader.GetDecimal(), + TypeCode.Single => reader.GetSingle(), + TypeCode.Byte => reader.GetByte(), + TypeCode.Int16 => reader.GetInt16(), + TypeCode.UInt16 => reader.GetUInt16(), + TypeCode.UInt32 => reader.GetUInt32(), + TypeCode.UInt64 => reader.GetUInt64(), + TypeCode.SByte => reader.GetSByte(), + _ => type.IsEnum ? Enum.ToObject(type, reader.GetInt32()) : reader.GetDouble() }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadObject(JsonElement element, Type targetType, DeserializationContext context, int depth) + private static object? ReadStringFromReader(ref Utf8JsonReader reader, Type targetType) { - // Handle $ref even if reference handling is disabled (for compatibility) - if (context.UseReferenceHandling && element.TryGetProperty("$ref", out var refElement)) + var type = Nullable.GetUnderlyingType(targetType) ?? targetType; + + if (ReferenceEquals(type, StringType)) return reader.GetString(); + if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime(); + if (ReferenceEquals(type, GuidType)) return reader.GetGuid(); + if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset(); + if (ReferenceEquals(type, TimeSpanType)) { - var refId = refElement.GetString()!; - return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType); + var str = reader.GetString(); + return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan); + } + if (type.IsEnum) + { + var str = reader.GetString(); + return str != null ? Enum.Parse(type, str) : null; + } + + return reader.GetString(); + } + + private static object? ReadObjectFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth) + { + if (depth > maxDepth) + { + reader.Skip(); + return null; } - // Check depth limit - if (depth > context.MaxDepth) return null; - if (IsDictionaryType(targetType, out var keyType, out var valueType)) - return ReadDictionary(element, keyType!, valueType!, context, depth); + return ReadDictionaryFromReader(ref reader, keyType!, valueType!, maxDepth, depth); var metadata = GetTypeMetadata(targetType); - object? instance; - if (metadata.CompiledConstructor != null) - instance = metadata.CompiledConstructor.Invoke(); - else + var instance = metadata.CompiledConstructor?.Invoke(); + if (instance == null) { try { instance = Activator.CreateInstance(targetType); } catch (MissingMethodException ex) @@ -286,172 +489,310 @@ public static class AcJsonDeserializer if (instance == null) return null; - if (context.UseReferenceHandling && element.TryGetProperty("$id", out var idElement)) - context.RegisterObject(idElement.GetString()!, instance); - - PopulateObjectInternal(element, instance, metadata, context, depth); + var propsDict = metadata.PropertySettersFrozen; + var nextDepth = depth + 1; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + break; + + if (reader.TokenType != JsonTokenType.PropertyName) + continue; + + var propName = reader.GetString(); + if (propName == null || !reader.Read()) + continue; + + if (!propsDict.TryGetValue(propName, out var propInfo)) + { + reader.Skip(); + continue; + } + + // Use cached version for faster type resolution + var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth); + propInfo.SetValue(instance, value); + } return instance; } - private static void PopulateObjectInternal(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth) + /// + /// Read object from reader when we're already at StartObject. + /// + private static object? ReadObjectFromReaderAtStart(ref Utf8JsonReader reader, Type targetType, DeserializeTypeMetadata metadata, byte maxDepth, int depth) { - foreach (var jsonProp in element.EnumerateObject()) + if (depth > maxDepth) { - var propName = jsonProp.Name; - - if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref")) - continue; - - if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue; - - var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, depth + 1); - - if (value is DeferredReference deferred) - context.AddPropertyToResolve(target, propInfo, deferred.RefId); - else - propInfo.SetValue(target, value); + reader.Skip(); + return null; } - } - private static void PopulateObjectInternalMerge(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth) - { - foreach (var jsonProp in element.EnumerateObject()) + var instance = metadata.CompiledConstructor?.Invoke(); + if (instance == null) { - var propName = jsonProp.Name; - - if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref")) - continue; - - if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue; - - var propValue = jsonProp.Value; - var propValueKind = propValue.ValueKind; - - // Check depth limit for nested objects/collections - if (depth + 1 > context.MaxDepth) + try { instance = Activator.CreateInstance(targetType); } + catch (MissingMethodException ex) { - // At max depth, only set primitives - if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array) - { - var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind); - propInfo.SetValue(target, primitiveValue); - } - continue; + throw new AcJsonDeserializationException( + $"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.", + null, targetType, ex); } - - if (propInfo is { IsCollection: true, ElementIsIId: true } && propValueKind == JsonValueKind.Array) - { - var existingCollection = propInfo.GetValue(target); - if (existingCollection != null) - { - MergeIIdCollection(propValue, existingCollection, propInfo, context, depth); - continue; - } - } - - if (propValueKind == JsonValueKind.Object) - { - if (context.UseReferenceHandling && propValue.TryGetProperty("$ref", out _)) - { - var value = ReadValue(propValue, propInfo.PropertyType, context, depth + 1); - if (value is DeferredReference deferred) - context.AddPropertyToResolve(target, propInfo, deferred.RefId); - else - propInfo.SetValue(target, value); - continue; - } - - if (!propInfo.PropertyType.IsPrimitive && propInfo.PropertyType != StringType) - { - var existingObj = propInfo.GetValue(target); - if (existingObj != null) - { - var nestedMetadata = GetTypeMetadata(propInfo.PropertyType); - PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, depth + 1); - continue; - } - } - } - - var value2 = ReadValue(propValue, propInfo.PropertyType, context, depth + 1); - - if (value2 is DeferredReference deferred2) - context.AddPropertyToResolve(target, propInfo, deferred2.RefId); - else - propInfo.SetValue(target, value2); } - } - - private static void PopulateList(JsonElement arrayElement, IList targetList, Type listType, DeserializationContext context, int depth) - { - // Check depth limit - if (depth > context.MaxDepth) return; - var elementType = GetCollectionElementType(listType); - if (elementType == null) return; - - var acObservable = targetList as IAcObservableCollection; - acObservable?.BeginUpdate(); + if (instance == null) return null; - try + var propsDict = metadata.PropertySettersFrozen; + var nextDepth = depth + 1; + + while (reader.Read()) { - targetList.Clear(); + if (reader.TokenType == JsonTokenType.EndObject) + break; - foreach (var item in arrayElement.EnumerateArray()) + if (reader.TokenType != JsonTokenType.PropertyName) + continue; + + var propName = reader.GetString(); + if (propName == null || !reader.Read()) + continue; + + if (!propsDict.TryGetValue(propName, out var propInfo)) { - var value = ReadValue(item, elementType, context, depth + 1); - if (value != null) - targetList.Add(value); + reader.Skip(); + continue; } + + // Use cached version for faster type resolution + var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth); + propInfo.SetValue(instance, value); } - finally { acObservable?.EndUpdate(); } + + return instance; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadArray(JsonElement element, Type targetType, DeserializationContext context, int depth) + private static object? ReadArrayFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth) { - // Check depth limit - if (depth > context.MaxDepth) return null; + if (depth > maxDepth) + { + reader.Skip(); + return null; + } var elementType = GetCollectionElementType(targetType); if (elementType == null) return null; + var nextDepth = depth + 1; + var list = GetOrCreateListFactory(elementType)(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + break; + + var value = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth); + if (value != null) + list.Add(value); + } + if (targetType.IsArray) { - var list = GetOrCreateListFactory(elementType)(); - foreach (var item in element.EnumerateArray()) - list.Add(ReadValue(item, elementType, context, depth + 1)); - var array = Array.CreateInstance(elementType, list.Count); list.CopyTo(array, 0); return array; } - IList? targetList = null; - try - { - var instance = Activator.CreateInstance(targetType); - if (instance is IList list) targetList = list; - } - catch { /* Fallback to List */ } - - targetList ??= GetOrCreateListFactory(elementType)(); - - var acObservable = targetList as IAcObservableCollection; - acObservable?.BeginUpdate(); - - try - { - foreach (var item in element.EnumerateArray()) - targetList.Add(ReadValue(item, elementType, context, depth + 1)); - } - finally { acObservable?.EndUpdate(); } - - return targetList; + return list; } - private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth) + private static object ReadDictionaryFromReader(ref Utf8JsonReader reader, Type keyType, Type valueType, byte maxDepth, int depth) + { + var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType); + var dict = (IDictionary)Activator.CreateInstance(dictType)!; + var nextDepth = depth + 1; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + break; + + if (reader.TokenType != JsonTokenType.PropertyName) + continue; + + var keyStr = reader.GetString(); + if (keyStr == null || !reader.Read()) + continue; + + object key; + if (ReferenceEquals(keyType, StringType)) key = keyStr; + else if (ReferenceEquals(keyType, IntType)) key = int.Parse(keyStr, CultureInfo.InvariantCulture); + else if (ReferenceEquals(keyType, LongType)) key = long.Parse(keyStr, CultureInfo.InvariantCulture); + else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(keyStr); + else if (keyType.IsEnum) key = Enum.Parse(keyType, keyStr); + else key = Convert.ChangeType(keyStr, keyType, CultureInfo.InvariantCulture); + + var value = ReadValueFromReader(ref reader, valueType, maxDepth, nextDepth); + dict.Add(key, value); + } + + return dict; + } + + /// + /// Populate object using Utf8JsonReader streaming (no DOM allocation). + /// + private static void PopulateObjectWithUtf8Reader(string json, object target, DeserializeTypeMetadata metadata, byte maxDepth) + { + var (buffer, length) = GetUtf8Bytes(json); + try + { + var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth }); + + if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) + return; + + PopulateObjectMergeFromReader(ref reader, target, metadata, maxDepth, 0); + } + finally + { + ReturnUtf8Buffer(buffer); + } + } + + /// + /// Populate list using Utf8JsonReader streaming (no DOM allocation). + /// + private static void PopulateListWithUtf8Reader(string json, IList targetList, in Type listType, byte maxDepth) + { + var (buffer, length) = GetUtf8Bytes(json); + try + { + var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth }); + + if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray) + return; + + var elementType = GetCollectionElementType(listType); + if (elementType == null) return; + + var acObservable = targetList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + targetList.Clear(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + break; + + var value = ReadValueFromReader(ref reader, elementType, maxDepth, 1); + if (value != null) + targetList.Add(value); + } + } + finally { acObservable?.EndUpdate(); } + } + finally + { + ReturnUtf8Buffer(buffer); + } + } + + /// + /// Populate object with merge semantics from Utf8JsonReader. + /// + private static void PopulateObjectMergeFromReader(ref Utf8JsonReader reader, object target, DeserializeTypeMetadata metadata, byte maxDepth, int depth) + { + var propsDict = metadata.PropertySettersFrozen; + var nextDepth = depth + 1; + var maxDepthReached = nextDepth > maxDepth; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + break; + + if (reader.TokenType != JsonTokenType.PropertyName) + continue; + + var propName = reader.GetString(); + if (propName == null || !reader.Read()) + continue; + + if (!propsDict.TryGetValue(propName, out var propInfo)) + { + reader.Skip(); + continue; + } + + var tokenType = reader.TokenType; + + if (maxDepthReached) + { + if (tokenType != JsonTokenType.StartObject && tokenType != JsonTokenType.StartArray) + { + // Use cached version for faster primitive reading + var primitiveValue = ReadPrimitiveFromReaderCached(ref reader, propInfo); + propInfo.SetValue(target, primitiveValue); + } + else + { + reader.Skip(); + } + continue; + } + + // Handle IId collection merge + if (propInfo.IsIIdCollection && tokenType == JsonTokenType.StartArray) + { + var existingCollection = propInfo.GetValue(target); + if (existingCollection != null) + { + MergeIIdCollectionFromReader(ref reader, existingCollection, propInfo, maxDepth, depth); + continue; + } + } + + // Handle nested objects - merge into existing + if (tokenType == JsonTokenType.StartObject && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType)) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) + { + var nestedMetadata = GetTypeMetadata(propInfo.PropertyType); + PopulateObjectMergeFromReader(ref reader, existingObj, nestedMetadata, maxDepth, nextDepth); + continue; + } + } + + // Use cached version for faster type resolution + var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth); + propInfo.SetValue(target, value); + } + } + + /// + /// Read primitive value from Utf8JsonReader using cached PropertySetterInfo. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadPrimitiveFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo) + { + var tokenType = reader.TokenType; + if (tokenType == JsonTokenType.Null) return null; + if (tokenType == JsonTokenType.True) return true; + if (tokenType == JsonTokenType.False) return false; + if (tokenType == JsonTokenType.Number) return ReadNumberFromReaderCached(ref reader, propInfo); + if (tokenType == JsonTokenType.String) return ReadStringFromReaderCached(ref reader, propInfo); + return null; + } + + /// + /// Merge IId collection from Utf8JsonReader streaming. + /// + private static void MergeIIdCollectionFromReader(ref Utf8JsonReader reader, object existingCollection, PropertySetterInfo propInfo, byte maxDepth, int depth) { var elementType = propInfo.ElementType!; var idGetter = propInfo.ElementIdGetter!; @@ -481,6 +822,364 @@ public static class AcJsonDeserializer } } + var nextDepth = depth + 1; + var elementMetadata = GetTypeMetadata(elementType); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + break; + + if (reader.TokenType != JsonTokenType.StartObject) + { + var primitiveValue = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth); + if (primitiveValue != null) existingList.Add(primitiveValue); + continue; + } + + // For objects, we need to read the full object to check the Id + var newItem = ReadObjectFromReaderAtStart(ref reader, elementType, elementMetadata, maxDepth, nextDepth); + if (newItem == null) continue; + + // Check if this item already exists by Id + var itemId = propInfo.ElementIdGetter?.Invoke(newItem); + if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null) + { + if (existingById.TryGetValue(itemId, out var existingItem)) + { + // Copy properties from newItem to existingItem + CopyProperties(newItem, existingItem, elementMetadata); + continue; + } + } + + existingList.Add(newItem); + } + } + finally { acObservable?.EndUpdate(); } + } + + /// + /// Copy properties from source to target using metadata. + /// + private static void CopyProperties(object source, object target, DeserializeTypeMetadata metadata) + { + foreach (var prop in metadata.PropertySettersFrozen.Values) + { + var value = prop.GetValue(source); + if (value != null) + prop.SetValue(target, value); + } + } + + /// + /// Read primitive value from Utf8JsonReader. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadPrimitiveFromReader(ref Utf8JsonReader reader, Type targetType) + { + var tokenType = reader.TokenType; + if (tokenType == JsonTokenType.Null) return null; + if (tokenType == JsonTokenType.True) return true; + if (tokenType == JsonTokenType.False) return false; + if (tokenType == JsonTokenType.Number) return ReadNumberFromReader(ref reader, targetType); + if (tokenType == JsonTokenType.String) return ReadStringFromReader(ref reader, targetType); + return null; + } + + #endregion + + #region Validation + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ValidateJson(string json, in Type targetType) + { + if (json.Length < 2) return; + + var firstChar = json[0]; + + // Quick check for double-serialized JSON + if (firstChar == '"' && json[^1] == '"') + { + var inner = json.AsSpan(1, json.Length - 2); + if (inner.Contains("\\\"", StringComparison.Ordinal) && + (inner.Contains("{", StringComparison.Ordinal) || inner.Contains("[", StringComparison.Ordinal))) + throw new AcJsonDeserializationException( + $"Detected double-serialized JSON string. Target type: {targetType.Name}.", + json, targetType); + } + + var isArrayJson = firstChar == '['; + var isObjectJson = firstChar == '{'; + + if (isArrayJson) + { + var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType); + var isDictType = IsDictionaryType(targetType, out _, out _); + if (!isCollectionType && !isDictType && targetType != typeof(object)) + throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType); + } + else if (isObjectJson) + { + var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType); + var isDictType = IsDictionaryType(targetType, out _, out _); + if (isCollectionType && !isDictType) + throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType); + } + } + + #endregion + + #region With Reference Handling (JsonElement Path) + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadValue(in JsonElement element, in Type targetType, DeserializationContext context, int depth) + { + var kind = element.ValueKind; + if (kind == JsonValueKind.Object) return ReadObject(element, targetType, context, depth); + if (kind == JsonValueKind.Array) return ReadArray(element, targetType, context, depth); + if (kind == JsonValueKind.Null || kind == JsonValueKind.Undefined) return null; + return ReadPrimitive(element, targetType, kind); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadObject(in JsonElement element, in Type targetType, DeserializationContext context, int depth) + { + // Check for $ref first + if (element.TryGetProperty(RefPropertyUtf8, out var refElement)) + { + var refId = refElement.GetString()!; + return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType); + } + + if (depth > context.MaxDepth) return null; + + if (IsDictionaryType(targetType, out var keyType, out var valueType)) + return ReadDictionary(element, keyType!, valueType!, context, depth); + + var metadata = GetTypeMetadata(targetType); + + var instance = metadata.CompiledConstructor?.Invoke(); + if (instance == null) + { + try { instance = Activator.CreateInstance(targetType); } + catch (MissingMethodException ex) + { + throw new AcJsonDeserializationException( + $"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.", + null, targetType, ex); + } + } + + if (instance == null) return null; + + // Check for $id and register + if (element.TryGetProperty(IdPropertyUtf8, out var idElement)) + context.RegisterObject(idElement.GetString()!, instance); + + PopulateObjectInternal(element, instance, metadata, context, depth); + + return instance; + } + + private static void PopulateObjectInternal(in JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth) + { + var propsDict = metadata.PropertySettersFrozen; + var nextDepth = depth + 1; + + foreach (var jsonProp in element.EnumerateObject()) + { + var propName = jsonProp.Name; + + // Skip $ properties + if (propName.Length > 0 && propName[0] == '$') continue; + + if (!propsDict.TryGetValue(propName, out var propInfo)) continue; + + var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, nextDepth); + + if (value is DeferredReference deferred) + context.AddPropertyToResolve(target, propInfo, deferred.RefId); + else + propInfo.SetValue(target, value); + } + } + + private static void PopulateObjectInternalMerge(in JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth) + { + var propsDict = metadata.PropertySettersFrozen; + var nextDepth = depth + 1; + var maxDepthReached = nextDepth > context.MaxDepth; + + foreach (var jsonProp in element.EnumerateObject()) + { + var propName = jsonProp.Name; + + // Skip $ properties + if (propName.Length > 0 && propName[0] == '$') continue; + + if (!propsDict.TryGetValue(propName, out var propInfo)) continue; + + var propValue = jsonProp.Value; + var propValueKind = propValue.ValueKind; + + if (maxDepthReached) + { + if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array) + { + var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind); + propInfo.SetValue(target, primitiveValue); + } + continue; + } + + // Handle IId collection merge + if (propInfo.IsIIdCollection && propValueKind == JsonValueKind.Array) + { + var existingCollection = propInfo.GetValue(target); + if (existingCollection != null) + { + MergeIIdCollection(propValue, existingCollection, propInfo, context, depth); + continue; + } + } + + // Handle nested objects + if (propValueKind == JsonValueKind.Object) + { + // Check for $ref + if (propValue.TryGetProperty(RefPropertyUtf8, out _)) + { + var value = ReadValue(propValue, propInfo.PropertyType, context, nextDepth); + if (value is DeferredReference deferred) + context.AddPropertyToResolve(target, propInfo, deferred.RefId); + else + propInfo.SetValue(target, value); + continue; + } + + // Merge into existing object + if (!propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType)) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) + { + var nestedMetadata = GetTypeMetadata(propInfo.PropertyType); + PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, nextDepth); + continue; + } + } + } + + var value2 = ReadValue(propValue, propInfo.PropertyType, context, nextDepth); + + if (value2 is DeferredReference deferred2) + context.AddPropertyToResolve(target, propInfo, deferred2.RefId); + else + propInfo.SetValue(target, value2); + } + } + + private static void PopulateList(in JsonElement arrayElement, IList targetList, in Type listType, DeserializationContext context, int depth) + { + if (depth > context.MaxDepth) return; + + var elementType = GetCollectionElementType(listType); + if (elementType == null) return; + + var acObservable = targetList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + targetList.Clear(); + var nextDepth = depth + 1; + + foreach (var item in arrayElement.EnumerateArray()) + { + var value = ReadValue(item, elementType, context, nextDepth); + if (value != null) + targetList.Add(value); + } + } + finally { acObservable?.EndUpdate(); } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadArray(in JsonElement element, in Type targetType, DeserializationContext context, int depth) + { + if (depth > context.MaxDepth) return null; + + var elementType = GetCollectionElementType(targetType); + if (elementType == null) return null; + + var nextDepth = depth + 1; + + if (targetType.IsArray) + { + var list = GetOrCreateListFactory(elementType)(); + foreach (var item in element.EnumerateArray()) + list.Add(ReadValue(item, elementType, context, nextDepth)); + + var array = Array.CreateInstance(elementType, list.Count); + list.CopyTo(array, 0); + return array; + } + + IList? targetList = null; + try + { + var instance = Activator.CreateInstance(targetType); + if (instance is IList list) targetList = list; + } + catch { /* Fallback to List */ } + + targetList ??= GetOrCreateListFactory(elementType)(); + + var acObservable = targetList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + foreach (var item in element.EnumerateArray()) + targetList.Add(ReadValue(item, elementType, context, nextDepth)); + } + finally { acObservable?.EndUpdate(); } + + return targetList; + } + + private static void MergeIIdCollection(in JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth) + { + var elementType = propInfo.ElementType!; + var idGetter = propInfo.ElementIdGetter!; + var idType = propInfo.ElementIdType!; + + var existingList = (IList)existingCollection; + var count = existingList.Count; + + var acObservable = existingList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + Dictionary? existingById = null; + if (count > 0) + { + existingById = new Dictionary(count); + for (var i = 0; i < count; i++) + { + var item = existingList[i]; + if (item != null) + { + var id = idGetter(item); + if (id != null && !IsDefaultValue(id, idType)) + existingById[id] = item; + } + } + } + + var nextDepth = depth + 1; foreach (var jsonItem in arrayElement.EnumerateArray()) { if (jsonItem.ValueKind != JsonValueKind.Object) continue; @@ -494,38 +1193,70 @@ public static class AcJsonDeserializer if (existingById.TryGetValue(itemId, out var existingItem)) { var itemMetadata = GetTypeMetadata(elementType); - PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, depth + 1); + PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, nextDepth); continue; } } - var newItem = ReadValue(jsonItem, elementType, context, depth + 1); + var newItem = ReadValue(jsonItem, elementType, context, nextDepth); if (newItem != null) existingList.Add(newItem); } } finally { acObservable?.EndUpdate(); } } + private static object ReadDictionary(in JsonElement element, in Type keyType, in Type valueType, DeserializationContext context, int depth) + { + var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType); + var dict = (IDictionary)Activator.CreateInstance(dictType)!; + var nextDepth = depth + 1; + + foreach (var prop in element.EnumerateObject()) + { + var name = prop.Name; + if (name.Length > 0 && name[0] == '$') continue; + + object key; + if (ReferenceEquals(keyType, StringType)) key = name; + else if (ReferenceEquals(keyType, IntType)) key = int.Parse(name, CultureInfo.InvariantCulture); + else if (ReferenceEquals(keyType, LongType)) key = long.Parse(name, CultureInfo.InvariantCulture); + else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(name); + else if (keyType.IsEnum) key = Enum.Parse(keyType, name); + else key = Convert.ChangeType(name, keyType, CultureInfo.InvariantCulture); + + dict.Add(key, ReadValue(prop.Value, valueType, context, nextDepth)); + } + + return dict; + } + + #endregion + + #region Primitive Reading + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadPrimitive(JsonElement element, Type targetType, JsonValueKind valueKind) + private static object? ReadPrimitive(in JsonElement element, in Type targetType, JsonValueKind valueKind) { var type = Nullable.GetUnderlyingType(targetType) ?? targetType; if (valueKind == JsonValueKind.Number) { - if (ReferenceEquals(type, IntType)) return element.GetInt32(); - if (ReferenceEquals(type, LongType)) return element.GetInt64(); - if (ReferenceEquals(type, DoubleType)) return element.GetDouble(); - if (ReferenceEquals(type, DecimalType)) return element.GetDecimal(); - if (ReferenceEquals(type, FloatType)) return element.GetSingle(); - if (type == ByteType) return element.GetByte(); - if (type == ShortType) return element.GetInt16(); - if (type == UShortType) return element.GetUInt16(); - if (type == UIntType) return element.GetUInt32(); - if (type == ULongType) return element.GetUInt64(); - if (type == SByteType) return element.GetSByte(); - if (type.IsEnum) return Enum.ToObject(type, element.GetInt32()); - return null; + var typeCode = Type.GetTypeCode(type); + return typeCode switch + { + TypeCode.Int32 => element.GetInt32(), + TypeCode.Int64 => element.GetInt64(), + TypeCode.Double => element.GetDouble(), + TypeCode.Decimal => element.GetDecimal(), + TypeCode.Single => element.GetSingle(), + TypeCode.Byte => element.GetByte(), + TypeCode.Int16 => element.GetInt16(), + TypeCode.UInt16 => element.GetUInt16(), + TypeCode.UInt32 => element.GetUInt32(), + TypeCode.UInt64 => element.GetUInt64(), + TypeCode.SByte => element.GetSByte(), + _ => type.IsEnum ? Enum.ToObject(type, element.GetInt32()) : null + }; } if (valueKind == JsonValueKind.String) @@ -533,8 +1264,8 @@ public static class AcJsonDeserializer if (ReferenceEquals(type, StringType)) return element.GetString(); if (ReferenceEquals(type, DateTimeType)) return element.GetDateTime(); if (ReferenceEquals(type, GuidType)) return element.GetGuid(); - if (type == DateTimeOffsetType) return element.GetDateTimeOffset(); - if (type == TimeSpanType) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture); + if (ReferenceEquals(type, DateTimeOffsetType)) return element.GetDateTimeOffset(); + if (ReferenceEquals(type, TimeSpanType)) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture); if (type.IsEnum) return Enum.Parse(type, element.GetString()!); return null; } @@ -545,56 +1276,62 @@ public static class AcJsonDeserializer return null; } - private static object ReadDictionary(JsonElement element, Type keyType, Type valueType, DeserializationContext context, int depth) - { - var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType); - var dict = (IDictionary)Activator.CreateInstance(dictType)!; - - foreach (var prop in element.EnumerateObject()) - { - if (prop.Name.StartsWith('$')) continue; - - object key; - if (keyType == StringType) key = prop.Name; - else if (keyType == IntType) key = int.Parse(prop.Name, CultureInfo.InvariantCulture); - else if (keyType == LongType) key = long.Parse(prop.Name, CultureInfo.InvariantCulture); - else if (keyType == GuidType) key = Guid.Parse(prop.Name); - else if (keyType.IsEnum) key = Enum.Parse(keyType, prop.Name); - else key = Convert.ChangeType(prop.Name, keyType, CultureInfo.InvariantCulture); - - dict.Add(key, ReadValue(prop.Value, valueType, context, depth + 1)); - } - - return dict; - } - #endregion #region Primitive Deserialization - private static bool TryDeserializePrimitive(string json, Type targetType, out object? result) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryDeserializePrimitiveFast(string json, in Type targetType, out object? result) { var type = Nullable.GetUnderlyingType(targetType) ?? targetType; - if (ReferenceEquals(type, StringType)) + // Handle enums first + if (type.IsEnum) { - using var doc = JsonDocument.Parse(json); - result = doc.RootElement.GetString(); + if (json.Length > 0 && json[0] == '"') + { + using var doc = JsonDocument.Parse(json); + result = Enum.Parse(type, doc.RootElement.GetString()!); + } + else result = Enum.ToObject(type, int.Parse(json, CultureInfo.InvariantCulture)); return true; } - if (ReferenceEquals(type, IntType)) { result = int.Parse(json, CultureInfo.InvariantCulture); return true; } - if (ReferenceEquals(type, LongType)) { result = long.Parse(json, CultureInfo.InvariantCulture); return true; } - if (ReferenceEquals(type, BoolType)) { result = json == "true"; return true; } - if (ReferenceEquals(type, DoubleType)) { result = double.Parse(json, CultureInfo.InvariantCulture); return true; } - if (ReferenceEquals(type, DecimalType)) { result = decimal.Parse(json, CultureInfo.InvariantCulture); return true; } - if (ReferenceEquals(type, FloatType)) { result = float.Parse(json, CultureInfo.InvariantCulture); return true; } + var typeCode = Type.GetTypeCode(type); - if (ReferenceEquals(type, DateTimeType)) + switch (typeCode) { - using var doc = JsonDocument.Parse(json); - result = doc.RootElement.GetDateTime(); - return true; + case TypeCode.Int32: result = int.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.Int64: result = long.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.Boolean: result = json == "true"; return true; + case TypeCode.Double: result = double.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.Decimal: result = decimal.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.Single: result = float.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.String: + using (var doc = JsonDocument.Parse(json)) + { + result = doc.RootElement.GetString(); + return true; + } + case TypeCode.DateTime: + using (var doc = JsonDocument.Parse(json)) + { + result = doc.RootElement.GetDateTime(); + return true; + } + case TypeCode.Byte: result = byte.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.Int16: result = short.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.UInt16: result = ushort.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.UInt32: result = uint.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.UInt64: result = ulong.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.SByte: result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true; + case TypeCode.Char: + using (var doc = JsonDocument.Parse(json)) + { + var s = doc.RootElement.GetString(); + result = s?.Length > 0 ? s[0] : '\0'; + return true; + } } if (ReferenceEquals(type, GuidType)) @@ -604,32 +1341,17 @@ public static class AcJsonDeserializer return true; } - if (type == DateTimeOffsetType) { using var doc = JsonDocument.Parse(json); result = doc.RootElement.GetDateTimeOffset(); return true; } - if (type == TimeSpanType) { using var doc = JsonDocument.Parse(json); result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture); return true; } - - if (type.IsEnum) + if (ReferenceEquals(type, DateTimeOffsetType)) { - if (json.StartsWith('"')) - { - using var doc = JsonDocument.Parse(json); - result = Enum.Parse(type, doc.RootElement.GetString()!); - } - else result = Enum.ToObject(type, int.Parse(json, CultureInfo.InvariantCulture)); + using var doc = JsonDocument.Parse(json); + result = doc.RootElement.GetDateTimeOffset(); return true; } - if (type == ByteType) { result = byte.Parse(json, CultureInfo.InvariantCulture); return true; } - if (type == ShortType) { result = short.Parse(json, CultureInfo.InvariantCulture); return true; } - if (type == UShortType) { result = ushort.Parse(json, CultureInfo.InvariantCulture); return true; } - if (type == UIntType) { result = uint.Parse(json, CultureInfo.InvariantCulture); return true; } - if (type == ULongType) { result = ulong.Parse(json, CultureInfo.InvariantCulture); return true; } - if (type == SByteType) { result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true; } - - if (type == CharType) + if (ReferenceEquals(type, TimeSpanType)) { using var doc = JsonDocument.Parse(json); - var s = doc.RootElement.GetString(); - result = s?.Length > 0 ? s[0] : '\0'; + result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture); return true; } @@ -639,15 +1361,46 @@ public static class AcJsonDeserializer #endregion + #region Context Pool + + private static class DeserializationContextPool + { + private static readonly ConcurrentQueue Pool = new(); + private const int MaxPoolSize = 16; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DeserializationContext Get(in AcJsonSerializerOptions options) + { + if (Pool.TryDequeue(out var context)) + { + context.Reset(options); + return context; + } + return new DeserializationContext(options); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(DeserializationContext context) + { + if (Pool.Count < MaxPoolSize) + { + context.Clear(); + Pool.Enqueue(context); + } + } + } + + #endregion + #region Type Metadata [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static DeserializeTypeMetadata GetTypeMetadata(Type type) + private static DeserializeTypeMetadata GetTypeMetadata(in Type type) => TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t)); private sealed class DeserializeTypeMetadata { - public Dictionary PropertySetters { get; } + public FrozenDictionary PropertySettersFrozen { get; } public Func? CompiledConstructor { get; } public DeserializeTypeMetadata(Type type) @@ -670,20 +1423,28 @@ public static class AcJsonDeserializer propsList.Add(p); } - PropertySetters = new Dictionary(propsList.Count, StringComparer.OrdinalIgnoreCase); + var propertySetters = new Dictionary(propsList.Count, StringComparer.OrdinalIgnoreCase); + foreach (var prop in propsList) - PropertySetters[prop.Name] = new PropertySetterInfo(prop, type); + { + propertySetters[prop.Name] = new PropertySetterInfo(prop, type); + } + + // Create frozen dictionary for faster lookup in hot paths + PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } } private sealed class PropertySetterInfo { - public Type PropertyType { get; } - public bool IsCollection { get; } - public bool ElementIsIId { get; } - public Type? ElementType { get; } - public Type? ElementIdType { get; } - public Func? ElementIdGetter { get; } + public readonly Type PropertyType; + public readonly Type UnderlyingType; // Cached: Nullable.GetUnderlyingType(PropertyType) ?? PropertyType + public readonly TypeCode PropertyTypeCode; // Cached TypeCode for fast primitive reading + public readonly bool IsNullable; + public readonly bool IsIIdCollection; + public readonly Type? ElementType; + public readonly Type? ElementIdType; + public readonly Func? ElementIdGetter; private readonly Action _setter; private readonly Func _getter; @@ -691,21 +1452,26 @@ public static class AcJsonDeserializer public PropertySetterInfo(PropertyInfo prop, Type declaringType) { PropertyType = prop.PropertyType; + var underlying = Nullable.GetUnderlyingType(PropertyType); + IsNullable = underlying != null; + UnderlyingType = underlying ?? PropertyType; + PropertyTypeCode = Type.GetTypeCode(UnderlyingType); + _setter = CreateCompiledSetter(declaringType, prop); _getter = CreateCompiledGetter(declaringType, prop); ElementType = GetCollectionElementType(PropertyType); - IsCollection = ElementType != null && ElementType != typeof(object) && + var isCollection = ElementType != null && ElementType != typeof(object) && typeof(IEnumerable).IsAssignableFrom(PropertyType) && - PropertyType != StringType; + !ReferenceEquals(PropertyType, StringType); - if (IsCollection && ElementType != null) + if (isCollection && ElementType != null) { - var (isId, idType) = GetIdInfo(ElementType); - if (isId) + var idInfo = GetIdInfo(ElementType); + if (idInfo.IsId) { - ElementIsIId = true; - ElementIdType = idType; + IsIIdCollection = true; + ElementIdType = idInfo.IdType; var idProp = ElementType.GetProperty("Id"); if (idProp != null) ElementIdGetter = CreateCompiledGetter(ElementType, idProp); @@ -762,16 +1528,26 @@ public static class AcJsonDeserializer private Dictionary? _idToObject; private List? _propertiesToResolve; - public bool IsMergeMode { get; init; } - public bool UseReferenceHandling { get; } - public byte MaxDepth { get; } + public bool IsMergeMode { get; set; } + public bool UseReferenceHandling { get; private set; } + public byte MaxDepth { get; private set; } - public DeserializationContext() : this(AcJsonSerializerOptions.Default) { } + public DeserializationContext(in AcJsonSerializerOptions options) + { + Reset(options); + } - public DeserializationContext(AcJsonSerializerOptions options) + public void Reset(in AcJsonSerializerOptions options) { UseReferenceHandling = options.UseReferenceHandling; MaxDepth = options.MaxDepth; + IsMergeMode = false; + } + + public void Clear() + { + _idToObject?.Clear(); + _propertiesToResolve?.Clear(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/AyCode.Core/Extensions/AcJsonSerializer.cs b/AyCode.Core/Extensions/AcJsonSerializer.cs index b72e200..b6fa408 100644 --- a/AyCode.Core/Extensions/AcJsonSerializer.cs +++ b/AyCode.Core/Extensions/AcJsonSerializer.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; +using System.Text.Json; using AyCode.Core.Interfaces; using Newtonsoft.Json; using static AyCode.Core.Extensions.JsonUtilities; @@ -14,92 +15,95 @@ namespace AyCode.Core.Extensions; /// /// High-performance custom JSON serializer optimized for IId<T> reference handling. -/// Features: -/// - Single-pass serialization with inline $id/$ref emission -/// - StringBuilder-based output (no intermediate string allocations) -/// - Compiled expression tree property accessors -/// - Smart reference tracking: only emits $id when object is actually referenced later -/// - MaxDepth support for controlling serialization depth +/// Uses Utf8JsonWriter for high-performance UTF-8 output (STJ approach). /// public static class AcJsonSerializer { private static readonly ConcurrentDictionary TypeMetadataCache = new(); + // Pre-encoded property names for $id/$ref (STJ optimization) + private static readonly JsonEncodedText IdPropertyEncoded = JsonEncodedText.Encode("$id"); + private static readonly JsonEncodedText RefPropertyEncoded = JsonEncodedText.Encode("$ref"); + /// /// Serialize object to JSON string with default options. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string Serialize(T value) => Serialize(value, AcJsonSerializerOptions.Default); /// /// Serialize object to JSON string with specified options. /// - public static string Serialize(T value, AcJsonSerializerOptions options) + public static string Serialize(T value, in AcJsonSerializerOptions options) { if (value == null) return "null"; - var type = typeof(T); + var type = value.GetType(); - if (TrySerializePrimitive(value, type, out var primitiveJson)) + if (TrySerializePrimitiveRuntime(value, type, out var primitiveJson)) return primitiveJson; - var context = new SerializationContext(options); - - if (options.UseReferenceHandling) - ScanReferences(value, context, 0); - - context.StartWriting(); - WriteValue(value, context, 0); - - return context.GetResult(); + var context = SerializationContextPool.Get(options); + try + { + if (options.UseReferenceHandling) + ScanReferences(value, context, 0); + + WriteValue(value, context, 0); + return context.GetResult(); + } + finally + { + SerializationContextPool.Return(context); + } } - private static bool TrySerializePrimitive(T value, Type type, out string json) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TrySerializePrimitiveRuntime(object value, in Type type, out string json) { var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + var typeCode = Type.GetTypeCode(underlyingType); - if (underlyingType == StringType) { json = SerializeString((string)(object)value!); return true; } - if (underlyingType == IntType) { json = ((int)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == LongType) { json = ((long)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == BoolType) { json = (bool)(object)value! ? "true" : "false"; return true; } - - if (underlyingType == DoubleType) + switch (typeCode) { - var d = (double)(object)value!; - json = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture); - return true; + case TypeCode.String: json = SerializeString((string)value); return true; + case TypeCode.Int32: json = ((int)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.Int64: json = ((long)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.Boolean: json = (bool)value ? "true" : "false"; return true; + case TypeCode.Double: + var d = (double)value; + json = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture); + return true; + case TypeCode.Decimal: json = ((decimal)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.Single: + var f = (float)value; + json = float.IsNaN(f) || float.IsInfinity(f) ? "null" : f.ToString("G9", CultureInfo.InvariantCulture); + return true; + case TypeCode.DateTime: json = $"\"{((DateTime)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true; + case TypeCode.Byte: json = ((byte)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.Int16: json = ((short)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.UInt16: json = ((ushort)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.UInt32: json = ((uint)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.UInt64: json = ((ulong)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.SByte: json = ((sbyte)value).ToString(CultureInfo.InvariantCulture); return true; + case TypeCode.Char: json = SerializeString(value.ToString()!); return true; } - if (underlyingType == DecimalType) { json = ((decimal)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - - if (underlyingType == FloatType) - { - var f = (float)(object)value!; - json = float.IsNaN(f) || float.IsInfinity(f) ? "null" : f.ToString("G9", CultureInfo.InvariantCulture); - return true; - } - - if (underlyingType == DateTimeType) { json = $"\"{((DateTime)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; return true; } - if (underlyingType == DateTimeOffsetType) { json = $"\"{((DateTimeOffset)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; return true; } - if (underlyingType == GuidType) { json = $"\"{((Guid)(object)value!).ToString("D")}\""; return true; } - if (underlyingType == TimeSpanType) { json = $"\"{((TimeSpan)(object)value!).ToString("c", CultureInfo.InvariantCulture)}\""; return true; } + if (ReferenceEquals(underlyingType, GuidType)) { json = $"\"{((Guid)value).ToString("D")}\""; return true; } + if (ReferenceEquals(underlyingType, DateTimeOffsetType)) { json = $"\"{((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true; } + if (ReferenceEquals(underlyingType, TimeSpanType)) { json = $"\"{((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)}\""; return true; } if (underlyingType.IsEnum) { json = Convert.ToInt32(value).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == ByteType) { json = ((byte)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == ShortType) { json = ((short)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == UShortType) { json = ((ushort)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == UIntType) { json = ((uint)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == ULongType) { json = ((ulong)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == SByteType) { json = ((sbyte)(object)value!).ToString(CultureInfo.InvariantCulture); return true; } - if (underlyingType == CharType) { json = SerializeString(value!.ToString()!); return true; } json = ""; return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string SerializeString(string value) { - if (!NeedsEscaping(value)) return $"\"{value}\""; + if (!NeedsEscaping(value)) return string.Concat("\"", value, "\""); - var sb = new StringBuilder(value.Length + 2); + var sb = new StringBuilder(value.Length + 8); sb.Append('"'); WriteEscapedString(sb, value); sb.Append('"'); @@ -110,16 +114,13 @@ public static class AcJsonSerializer private static void ScanReferences(object? value, SerializationContext context, int depth) { - if (value == null) return; - if (depth > context.MaxDepth) return; + if (value == null || depth > context.MaxDepth) return; var type = value.GetType(); - if (IsPrimitiveOrStringFast(type)) return; - if (!context.TrackForScanning(value)) return; - if (value is IEnumerable enumerable && type != StringType) + if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { foreach (var item in enumerable) if (item != null) ScanReferences(item, context, depth + 1); @@ -127,9 +128,11 @@ public static class AcJsonSerializer } var metadata = GetTypeMetadata(type); - foreach (var prop in metadata.Properties) + var props = metadata.Properties; + var propCount = props.Length; + for (var i = 0; i < propCount; i++) { - var propValue = prop.GetValue(value); + var propValue = props[i].GetValue(value); if (propValue != null) ScanReferences(propValue, context, depth + 1); } } @@ -140,133 +143,154 @@ public static class AcJsonSerializer private static void WriteValue(object? value, SerializationContext context, int depth) { - if (value == null) { context.WriteNull(); return; } + if (value == null) { context.Writer.WriteNullValue(); return; } var type = value.GetType(); - if (TryWritePrimitive(value, type, context)) return; + if (TryWritePrimitive(value, type, context.Writer)) return; - // Check depth limit for complex types - if (depth > context.MaxDepth) - { - context.WriteNull(); - return; - } + if (depth > context.MaxDepth) { context.Writer.WriteNullValue(); return; } if (value is IDictionary dictionary) { WriteDictionary(dictionary, context, depth); return; } - if (value is IEnumerable enumerable && type != StringType) { WriteArray(enumerable, context, depth); return; } + if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { WriteArray(enumerable, context, depth); return; } WriteObject(value, type, context, depth); } - private static void WriteObject(object value, Type type, SerializationContext context, int depth) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteObject(object value, in Type type, SerializationContext context, int depth) { + var writer = context.Writer; + if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId)) { - context.WriteRef(refId); + writer.WriteStartObject(); + writer.WriteString(RefPropertyEncoded, refId); + writer.WriteEndObject(); return; } - context.WriteObjectStart(); - var isFirst = true; + writer.WriteStartObject(); if (context.UseReferenceHandling && context.ShouldWriteId(value, out var id)) { - context.WritePropertyName("$id", ref isFirst); - context.WriteString(id); + writer.WriteString(IdPropertyEncoded, id); context.MarkAsWritten(value, id); } var metadata = GetTypeMetadata(type); - foreach (var prop in metadata.Properties) + var props = metadata.Properties; + var propCount = props.Length; + var nextDepth = depth + 1; + + for (var i = 0; i < propCount; i++) { + var prop = props[i]; var propValue = prop.GetValue(value); if (propValue == null) continue; - if (IsSerializerDefaultValue(propValue, prop.PropertyType)) continue; + if (IsDefaultValueFast(propValue, prop.PropertyTypeCode, prop.PropertyType)) continue; - context.WritePropertyName(prop.JsonName, ref isFirst); - WriteValue(propValue, context, depth + 1); + writer.WritePropertyName(prop.JsonNameEncoded); + WriteValue(propValue, context, nextDepth); } - context.WriteObjectEnd(); + writer.WriteEndObject(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteArray(IEnumerable enumerable, SerializationContext context, int depth) { - context.WriteArrayStart(); - var isFirst = true; + var writer = context.Writer; + writer.WriteStartArray(); + var nextDepth = depth + 1; foreach (var item in enumerable) { - if (!isFirst) context.WriteComma(); - isFirst = false; - WriteValue(item, context, depth + 1); + WriteValue(item, context, nextDepth); } - context.WriteArrayEnd(); + writer.WriteEndArray(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteDictionary(IDictionary dictionary, SerializationContext context, int depth) { - context.WriteObjectStart(); - var isFirst = true; + var writer = context.Writer; + writer.WriteStartObject(); + var nextDepth = depth + 1; foreach (DictionaryEntry entry in dictionary) { - context.WritePropertyName(entry.Key?.ToString() ?? "", ref isFirst); - WriteValue(entry.Value, context, depth + 1); + writer.WritePropertyName(entry.Key?.ToString() ?? ""); + WriteValue(entry.Value, context, nextDepth); } - context.WriteObjectEnd(); + writer.WriteEndObject(); } - private static bool TryWritePrimitive(object value, Type type, SerializationContext context) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryWritePrimitive(object value, in Type type, Utf8JsonWriter writer) { var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + var typeCode = Type.GetTypeCode(underlyingType); - if (underlyingType == StringType) { context.WriteString((string)value); return true; } - if (underlyingType == IntType) { context.WriteInt((int)value); return true; } - if (underlyingType == LongType) { context.WriteLong((long)value); return true; } - if (underlyingType == BoolType) { context.WriteBool((bool)value); return true; } - if (underlyingType == DoubleType) { context.WriteDouble((double)value); return true; } - if (underlyingType == DecimalType) { context.WriteDecimal((decimal)value); return true; } - if (underlyingType == FloatType) { context.WriteFloat((float)value); return true; } - if (underlyingType == DateTimeType) { context.WriteDateTime((DateTime)value); return true; } - if (underlyingType == DateTimeOffsetType) { context.WriteDateTimeOffset((DateTimeOffset)value); return true; } - if (underlyingType == GuidType) { context.WriteGuid((Guid)value); return true; } - if (underlyingType == TimeSpanType) { context.WriteTimeSpan((TimeSpan)value); return true; } - if (underlyingType.IsEnum) { context.WriteInt(Convert.ToInt32(value)); return true; } - if (underlyingType == ByteType) { context.WriteInt((byte)value); return true; } - if (underlyingType == ShortType) { context.WriteInt((short)value); return true; } - if (underlyingType == UShortType) { context.WriteInt((ushort)value); return true; } - if (underlyingType == UIntType) { context.WriteLong((uint)value); return true; } - if (underlyingType == ULongType) { context.WriteULong((ulong)value); return true; } - if (underlyingType == SByteType) { context.WriteInt((sbyte)value); return true; } - if (underlyingType == CharType) { context.WriteString(value.ToString()!); return true; } + switch (typeCode) + { + case TypeCode.String: writer.WriteStringValue((string)value); return true; + case TypeCode.Int32: writer.WriteNumberValue((int)value); return true; + case TypeCode.Int64: writer.WriteNumberValue((long)value); return true; + case TypeCode.Boolean: writer.WriteBooleanValue((bool)value); return true; + case TypeCode.Double: + var d = (double)value; + if (double.IsNaN(d) || double.IsInfinity(d)) writer.WriteNullValue(); + else writer.WriteNumberValue(d); + return true; + case TypeCode.Decimal: writer.WriteNumberValue((decimal)value); return true; + case TypeCode.Single: + var f = (float)value; + if (float.IsNaN(f) || float.IsInfinity(f)) writer.WriteNullValue(); + else writer.WriteNumberValue(f); + return true; + case TypeCode.DateTime: writer.WriteStringValue((DateTime)value); return true; + case TypeCode.Byte: writer.WriteNumberValue((byte)value); return true; + case TypeCode.Int16: writer.WriteNumberValue((short)value); return true; + case TypeCode.UInt16: writer.WriteNumberValue((ushort)value); return true; + case TypeCode.UInt32: writer.WriteNumberValue((uint)value); return true; + case TypeCode.UInt64: writer.WriteNumberValue((ulong)value); return true; + case TypeCode.SByte: writer.WriteNumberValue((sbyte)value); return true; + case TypeCode.Char: writer.WriteStringValue(value.ToString()); return true; + } + + if (ReferenceEquals(underlyingType, GuidType)) { writer.WriteStringValue((Guid)value); return true; } + if (ReferenceEquals(underlyingType, DateTimeOffsetType)) { writer.WriteStringValue((DateTimeOffset)value); return true; } + if (ReferenceEquals(underlyingType, TimeSpanType)) { writer.WriteStringValue(((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)); return true; } + if (underlyingType.IsEnum) { writer.WriteNumberValue(Convert.ToInt32(value)); return true; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsSerializerDefaultValue(object value, Type propertyType) + private static bool IsDefaultValueFast(object value, TypeCode typeCode, in Type propertyType) { - var type = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + switch (typeCode) + { + case TypeCode.Int32: return (int)value == 0; + case TypeCode.Int64: return (long)value == 0L; + case TypeCode.Double: return (double)value == 0.0; + case TypeCode.Decimal: return (decimal)value == 0m; + case TypeCode.Single: return (float)value == 0f; + case TypeCode.Byte: return (byte)value == 0; + case TypeCode.Int16: return (short)value == 0; + case TypeCode.UInt16: return (ushort)value == 0; + case TypeCode.UInt32: return (uint)value == 0; + case TypeCode.UInt64: return (ulong)value == 0; + case TypeCode.SByte: return (sbyte)value == 0; + case TypeCode.Boolean: return (bool)value == false; + case TypeCode.String: return string.IsNullOrEmpty((string)value); + } - if (type == IntType) return (int)value == 0; - if (type == LongType) return (long)value == 0L; - if (type == DoubleType) return (double)value == 0.0; - if (type == DecimalType) return (decimal)value == 0m; - if (type == FloatType) return (float)value == 0f; - if (type == ByteType) return (byte)value == 0; - if (type == ShortType) return (short)value == 0; - if (type == UShortType) return (ushort)value == 0; - if (type == UIntType) return (uint)value == 0; - if (type == ULongType) return (ulong)value == 0; - if (type == SByteType) return (sbyte)value == 0; - if (type == BoolType) return (bool)value == false; - if (type == StringType) return string.IsNullOrEmpty((string)value); - if (type.IsEnum) return Convert.ToInt32(value) == 0; - if (type == GuidType) return (Guid)value == Guid.Empty; + if (propertyType.IsEnum) return Convert.ToInt32(value) == 0; + if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty; return false; } @@ -275,7 +299,8 @@ public static class AcJsonSerializer #region Type Metadata - private static TypeMetadata GetTypeMetadata(Type type) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeMetadata GetTypeMetadata(in Type type) => TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t)); private sealed class TypeMetadata @@ -295,14 +320,18 @@ public static class AcJsonSerializer private sealed class PropertyAccessor { - public string JsonName { get; } - public Type PropertyType { get; } + public readonly string JsonName; + public readonly JsonEncodedText JsonNameEncoded; // STJ optimization - pre-encoded property name + public readonly Type PropertyType; + public readonly TypeCode PropertyTypeCode; private readonly Func _getter; public PropertyAccessor(PropertyInfo prop) { JsonName = prop.Name; - PropertyType = prop.PropertyType; + JsonNameEncoded = JsonEncodedText.Encode(prop.Name); + PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + PropertyTypeCode = Type.GetTypeCode(PropertyType); _getter = CreateCompiledGetter(prop.DeclaringType!, prop); } @@ -321,35 +350,89 @@ public static class AcJsonSerializer #endregion + #region Context Pool + + private static class SerializationContextPool + { + private static readonly ConcurrentQueue Pool = new(); + private const int MaxPoolSize = 16; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SerializationContext Get(in AcJsonSerializerOptions options) + { + if (Pool.TryDequeue(out var context)) + { + context.Reset(options); + return context; + } + return new SerializationContext(options); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(SerializationContext context) + { + if (Pool.Count < MaxPoolSize) + { + context.Clear(); + Pool.Enqueue(context); + } + } + } + + #endregion + #region Serialization Context - private sealed class SerializationContext + private sealed class SerializationContext : IDisposable { - private readonly StringBuilder _sb = new(4096); - private readonly Dictionary? _scanOccurrences; - private readonly Dictionary? _writtenRefs; - private readonly HashSet? _multiReferenced; - private int _nextId = 1; + private readonly ArrayBufferWriter _buffer; + public Utf8JsonWriter Writer { get; private set; } - private static readonly ArrayPool CharPool = ArrayPool.Shared; - private readonly char[] _numberBuffer = CharPool.Rent(64); + private Dictionary? _scanOccurrences; + private Dictionary? _writtenRefs; + private HashSet? _multiReferenced; + private int _nextId; - public bool UseReferenceHandling { get; } - public byte MaxDepth { get; } + public bool UseReferenceHandling { get; private set; } + public byte MaxDepth { get; private set; } - public SerializationContext(AcJsonSerializerOptions options) + private static readonly JsonWriterOptions WriterOptions = new() + { + Indented = false, + SkipValidation = true // Skip validation for performance + }; + + public SerializationContext(in AcJsonSerializerOptions options) + { + _buffer = new ArrayBufferWriter(4096); + Writer = new Utf8JsonWriter(_buffer, WriterOptions); + Reset(options); + } + + public void Reset(in AcJsonSerializerOptions options) { UseReferenceHandling = options.UseReferenceHandling; MaxDepth = options.MaxDepth; + _nextId = 1; if (UseReferenceHandling) { - _scanOccurrences = new Dictionary(64, ReferenceEqualityComparer.Instance); - _writtenRefs = new Dictionary(32, ReferenceEqualityComparer.Instance); - _multiReferenced = new HashSet(32, ReferenceEqualityComparer.Instance); + _scanOccurrences ??= new Dictionary(64, ReferenceEqualityComparer.Instance); + _writtenRefs ??= new Dictionary(32, ReferenceEqualityComparer.Instance); + _multiReferenced ??= new HashSet(32, ReferenceEqualityComparer.Instance); } } + public void Clear() + { + Writer.Reset(); + _buffer.Clear(); + _scanOccurrences?.Clear(); + _writtenRefs?.Clear(); + _multiReferenced?.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TrackForScanning(object obj) { if (_scanOccurrences == null) return true; @@ -360,8 +443,7 @@ public static class AcJsonSerializer return true; } - public void StartWriting() { } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ShouldWriteId(object obj, out string id) { if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj)) @@ -373,122 +455,26 @@ public static class AcJsonSerializer return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void MarkAsWritten(object obj, string id) => _writtenRefs![obj] = id; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetExistingRef(object obj, out string refId) { if (_writtenRefs != null) return _writtenRefs.TryGetValue(obj, out refId!); refId = ""; return false; } - - public void WriteRef(string refId) { _sb.Append("{\"$ref\":\""); _sb.Append(refId); _sb.Append("\"}"); } - public string GetResult() { CharPool.Return(_numberBuffer); return _sb.ToString(); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteNull() => _sb.Append("null"); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteObjectStart() => _sb.Append('{'); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteObjectEnd() => _sb.Append('}'); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayStart() => _sb.Append('['); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayEnd() => _sb.Append(']'); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteComma() => _sb.Append(','); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WritePropertyName(string name, ref bool isFirst) + public string GetResult() { - if (!isFirst) _sb.Append(','); - isFirst = false; - _sb.Append('"'); _sb.Append(name); _sb.Append("\":"); + Writer.Flush(); + return Encoding.UTF8.GetString(_buffer.WrittenSpan); } - public void WriteString(string value) + public void Dispose() { - _sb.Append('"'); - if (!NeedsEscaping(value)) _sb.Append(value); - else WriteEscapedString(_sb, value); - _sb.Append('"'); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteInt(int value) - { - if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written); - else _sb.Append(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteLong(long value) - { - if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written); - else _sb.Append(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteULong(ulong value) - { - if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written); - else _sb.Append(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteBool(bool value) => _sb.Append(value ? "true" : "false"); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteDouble(double value) - { - if (double.IsNaN(value) || double.IsInfinity(value)) _sb.Append("null"); - else if (value.TryFormat(_numberBuffer, out var written, "G17", CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written); - else _sb.Append(value.ToString("G17", CultureInfo.InvariantCulture)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteFloat(float value) - { - if (float.IsNaN(value) || float.IsInfinity(value)) _sb.Append("null"); - else if (value.TryFormat(_numberBuffer, out var written, "G9", CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written); - else _sb.Append(value.ToString("G9", CultureInfo.InvariantCulture)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteDecimal(decimal value) - { - if (value.TryFormat(_numberBuffer, out var written, provider: CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written); - else _sb.Append(value.ToString(CultureInfo.InvariantCulture)); - } - - public void WriteDateTime(DateTime value) - { - _sb.Append('"'); - Span buffer = stackalloc char[33]; - if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]); - else _sb.Append(value.ToString("O", CultureInfo.InvariantCulture)); - _sb.Append('"'); - } - - public void WriteDateTimeOffset(DateTimeOffset value) - { - _sb.Append('"'); - Span buffer = stackalloc char[33]; - if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]); - else _sb.Append(value.ToString("O", CultureInfo.InvariantCulture)); - _sb.Append('"'); - } - - public void WriteGuid(Guid value) - { - _sb.Append('"'); - Span buffer = stackalloc char[36]; - if (value.TryFormat(buffer, out var written, "D")) _sb.Append(buffer[..written]); - else _sb.Append(value.ToString("D")); - _sb.Append('"'); - } - - public void WriteTimeSpan(TimeSpan value) - { - _sb.Append('"'); - Span buffer = stackalloc char[26]; - if (value.TryFormat(buffer, out var written, "c", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]); - else _sb.Append(value.ToString("c", CultureInfo.InvariantCulture)); - _sb.Append('"'); + Writer.Dispose(); } } diff --git a/AyCode.Core/Extensions/JsonUtilities.cs b/AyCode.Core/Extensions/JsonUtilities.cs index c0772f9..1d0e122 100644 --- a/AyCode.Core/Extensions/JsonUtilities.cs +++ b/AyCode.Core/Extensions/JsonUtilities.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Collections; using System.Collections.Concurrent; using System.Collections.Frozen; @@ -51,6 +52,27 @@ public sealed class AcJsonSerializerOptions public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false }; } +/// +/// Cached result for IId type info lookup. +/// +public readonly struct IdTypeInfo +{ + public readonly bool IsId; + public readonly Type? IdType; + + public IdTypeInfo(bool isId, Type? idType) + { + IsId = isId; + IdType = idType; + } + + public void Deconstruct(out bool isId, out Type? idType) + { + isId = IsId; + idType = IdType; + } +} + /// /// Central utilities for JSON serialization/deserialization. /// Contains shared type caches, primitive type checks, and string utilities. @@ -109,7 +131,7 @@ public static class JsonUtilities #region Type Caches - private static readonly ConcurrentDictionary IdInfoCache = new(); + private static readonly ConcurrentDictionary IdInfoCache = new(); private static readonly ConcurrentDictionary CollectionElementCache = new(); private static readonly ConcurrentDictionary IsPrimitiveCache = new(); private static readonly ConcurrentDictionary IsCollectionCache = new(); @@ -119,6 +141,37 @@ public static class JsonUtilities #endregion + #region UTF8 Buffer Pool + + /// + /// Rents a UTF8 byte buffer from the shared pool. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] RentUtf8Buffer(int minLength) + => ArrayPool.Shared.Rent(minLength); + + /// + /// Returns a UTF8 byte buffer to the shared pool. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReturnUtf8Buffer(byte[] buffer) + => ArrayPool.Shared.Return(buffer); + + /// + /// Converts a JSON string to UTF8 bytes using pooled buffer. + /// Returns the actual byte count written. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (byte[] buffer, int length) GetUtf8Bytes(string json) + { + var maxByteCount = Encoding.UTF8.GetMaxByteCount(json.Length); + var buffer = RentUtf8Buffer(maxByteCount); + var actualLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0); + return (buffer, actualLength); + } + + #endregion + #region String Utilities /// @@ -211,6 +264,20 @@ public static class JsonUtilities return false; } + /// + /// Checks if a span needs JSON escaping. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool NeedsEscaping(ReadOnlySpan value) + { + foreach (var c in value) + { + if (c < 32 || c == '"' || c == '\\') + return true; + } + return false; + } + /// /// Escapes a string for JSON output. /// @@ -238,6 +305,34 @@ public static class JsonUtilities } } } + + /// + /// Escapes a span for JSON output. + /// + public static void WriteEscapedString(StringBuilder sb, ReadOnlySpan 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 @@ -263,7 +358,7 @@ public static class JsonUtilities /// Faster primitive check using TypeCode for hot paths. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsPrimitiveOrStringFast(Type type) + public static bool IsPrimitiveOrStringFast(in Type type) { var typeCode = Type.GetTypeCode(type); return typeCode switch @@ -272,7 +367,7 @@ public static class JsonUtilities 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 + _ => ReferenceEquals(type, GuidType) || ReferenceEquals(type, TimeSpanType) || ReferenceEquals(type, DateTimeOffsetType) || type.IsEnum }; } @@ -280,28 +375,28 @@ public static class JsonUtilities /// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsGenericCollectionType(Type type) + public static bool IsGenericCollectionType(in Type type) { return IsCollectionCache.GetOrAdd(type, static t => { - if (t == StringType || t.IsPrimitive) return false; + if (ReferenceEquals(t, StringType) || t.IsPrimitive) return false; if (t.IsArray) return true; if (t.IsGenericType) { var genericDef = t.GetGenericTypeDefinition(); - if (genericDef == ListGenericType || - genericDef == IListGenericType || + if (ReferenceEquals(genericDef, ListGenericType) || + ReferenceEquals(genericDef, IListGenericType) || genericDef == typeof(ICollection<>) || - genericDef == IEnumerableGenericType || - genericDef == ObservableCollectionType || - genericDef == CollectionType) + ReferenceEquals(genericDef, IEnumerableGenericType) || + ReferenceEquals(genericDef, ObservableCollectionType) || + ReferenceEquals(genericDef, CollectionType)) return true; } foreach (var iface in t.GetInterfaces()) { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType) + if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType)) return true; } @@ -313,7 +408,7 @@ public static class JsonUtilities /// Checks if type is a dictionary type. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType) + public static bool IsDictionaryType(in Type type, out Type? keyType, out Type? valueType) { keyType = null; valueType = null; @@ -321,7 +416,7 @@ public static class JsonUtilities if (!type.IsGenericType) return false; var genericDef = type.GetGenericTypeDefinition(); - if (genericDef == DictionaryGenericType || genericDef == IDictionaryGenericType) + if (ReferenceEquals(genericDef, DictionaryGenericType) || ReferenceEquals(genericDef, IDictionaryGenericType)) { var args = type.GetGenericArguments(); keyType = args[0]; @@ -331,7 +426,7 @@ public static class JsonUtilities foreach (var iface in type.GetInterfaces()) { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IDictionaryGenericType) + if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IDictionaryGenericType)) { var args = iface.GetGenericArguments(); keyType = args[0]; @@ -347,7 +442,7 @@ public static class JsonUtilities /// Gets the element type of a collection. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Type? GetCollectionElementType(Type collectionType) + public static Type? GetCollectionElementType(in Type collectionType) { return CollectionElementCache.GetOrAdd(collectionType, static type => { @@ -357,14 +452,14 @@ public static class JsonUtilities if (type.IsGenericType) { var genericDef = type.GetGenericTypeDefinition(); - if (genericDef == ListGenericType || genericDef == IListGenericType || - genericDef == typeof(ICollection<>) || genericDef == IEnumerableGenericType) + if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) || + genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType)) return type.GetGenericArguments()[0]; } foreach (var iface in type.GetInterfaces()) { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType) + if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType)) return iface.GetGenericArguments()[0]; } @@ -373,21 +468,21 @@ public static class JsonUtilities } /// - /// Gets IId info for a type. + /// Gets IId info for a type. Returns struct to avoid allocation. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (bool IsId, Type? IdType) GetIdInfo(Type type) + public static IdTypeInfo GetIdInfo(in Type type) { return IdInfoCache.GetOrAdd(type, static t => { foreach (var iface in t.GetInterfaces()) { if (!iface.IsGenericType) continue; - if (iface.GetGenericTypeDefinition() != IIdGenericType) continue; + if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue; var idType = iface.GetGenericArguments()[0]; - return (idType.IsValueType, idType); + return new IdTypeInfo(idType.IsValueType, idType); } - return (false, null); + return new IdTypeInfo(false, null); }); } @@ -406,11 +501,11 @@ public static class JsonUtilities /// Checks if collection contains primitive elements. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsPrimitiveElementCollection(Type type) + public static bool IsPrimitiveElementCollection(in Type type) { return IsPrimitiveCollectionCache.GetOrAdd(type, static t => { - if (t == StringType) return false; + if (ReferenceEquals(t, StringType)) return false; Type? elementType = null; if (t.IsArray) @@ -430,7 +525,7 @@ public static class JsonUtilities /// Gets or creates a list factory for a given element type. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Func GetOrCreateListFactory(Type elementType) + public static Func GetOrCreateListFactory(in Type elementType) { return ListFactoryCache.GetOrAdd(elementType, static t => { @@ -445,7 +540,7 @@ public static class JsonUtilities /// Checks if value is the default value for its type. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsDefaultValue(object id, Type idType) + public static bool IsDefaultValue(in object id, in Type idType) { if (ReferenceEquals(idType, IntType)) return (int)id == 0; if (ReferenceEquals(idType, LongType)) return (long)id == 0; diff --git a/AyCode.Core/Extensions/MergeContractResolver.cs b/AyCode.Core/Extensions/MergeContractResolver.cs index 601a620..959a5fa 100644 --- a/AyCode.Core/Extensions/MergeContractResolver.cs +++ b/AyCode.Core/Extensions/MergeContractResolver.cs @@ -40,18 +40,18 @@ public sealed class CachedPropertyInfo if (!ShouldSkip) { - var (isId, idType) = GetIdInfo(PropertyType); - IsIId = isId; - IdType = idType; + var idInfo = GetIdInfo(PropertyType); + IsIId = idInfo.IsId; + IdType = idInfo.IdType; - if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != StringType) + if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && !ReferenceEquals(PropertyType, StringType)) { CollectionElementType = GetCollectionElementType(PropertyType); if (CollectionElementType != null) { - var (elemIsId, elemIdType) = GetIdInfo(CollectionElementType); - IsIIdCollection = elemIsId; - CollectionElementIdType = elemIdType; + var elemIdInfo = GetIdInfo(CollectionElementType); + IsIIdCollection = elemIdInfo.IsId; + CollectionElementIdType = elemIdInfo.IdType; } } } @@ -315,7 +315,7 @@ public class UnifiedMergeContractResolver : DefaultContractResolver var t = property.PropertyType; if (t == null) return property; - var config = GetOrCreatePropertyConfig(member, t); + var config = GetOrCreatePropertyConfigRef(member, t); if (config.IsPrimitiveElementCollection) { @@ -343,7 +343,7 @@ public class UnifiedMergeContractResolver : DefaultContractResolver } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static CachedPropertyConfig GetOrCreatePropertyConfig(MemberInfo member, Type propertyType) + private static CachedPropertyConfig GetOrCreatePropertyConfigRef(MemberInfo member, Type propertyType) => PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType)); private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType) @@ -360,11 +360,11 @@ public class UnifiedMergeContractResolver : DefaultContractResolver config.ElementType = GetCollectionElementType(propertyType); if (config.ElementType != null) { - var (hasId, elemIdType) = GetIdInfo(config.ElementType); - if (hasId && elemIdType != null) + var idInfo = GetIdInfo(config.ElementType); + if (idInfo.IsId && idInfo.IdType != null) { config.IsIdCollection = true; - config.IdType = elemIdType; + config.IdType = idInfo.IdType; } config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType); } diff --git a/BenchmarkSuite1/SerializationBenchmarks.cs b/BenchmarkSuite1/SerializationBenchmarks.cs index 97a9006..aabd647 100644 --- a/BenchmarkSuite1/SerializationBenchmarks.cs +++ b/BenchmarkSuite1/SerializationBenchmarks.cs @@ -1,137 +1,339 @@ using AyCode.Core.Extensions; using AyCode.Core.Tests.TestModels; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Reports; using Newtonsoft.Json; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace AyCode.Core.Benchmarks; /// -/// Serialization benchmarks comparing AcJsonSerializer/Deserializer with Newtonsoft.Json. -/// Uses shared TestModels from AyCode.Core.Tests for consistency. +/// Serialization benchmarks comparing AyCode, Newtonsoft.Json, and System.Text.Json. +/// Tests small, medium, and large data with and without reference handling. /// [MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] public class SerializationBenchmarks { - // Test data - uses shared TestModels - private TestOrder _testOrder = null!; + // Test data - small, medium, large + private TestOrder _smallOrder = null!; + private TestOrder _mediumOrder = null!; + private TestOrder _largeOrder = null!; // Pre-serialized JSON for deserialization benchmarks - private string _newtonsoftJson = null!; - private string _ayCodeJson = null!; + private string _smallAyCodeJson = null!; + private string _smallAyCodeNoRefJson = null!; + private string _smallStjJson = null!; + private string _smallStjNoRefJson = null!; + private string _smallNewtonsoftJson = null!; + private string _mediumAyCodeJson = null!; + private string _mediumAyCodeNoRefJson = null!; + private string _mediumStjJson = null!; + private string _mediumStjNoRefJson = null!; + private string _mediumNewtonsoftJson = null!; + private string _largeAyCodeJson = null!; + private string _largeAyCodeNoRefJson = null!; + private string _largeStjJson = null!; + private string _largeStjNoRefJson = null!; + private string _largeNewtonsoftJson = null!; - // Target objects for Populate benchmarks - private TestOrder _populateTarget = null!; + // STJ options + private JsonSerializerOptions _stjWithRefs = null!; + private JsonSerializerOptions _stjNoRefs = null!; - // Settings - private JsonSerializerSettings _newtonsoftNoRefSettings = null!; + // AyCode options + private AcJsonSerializerOptions _ayCodeWithRefs = null!; + private AcJsonSerializerOptions _ayCodeNoRefs = null!; + + // Newtonsoft settings + private JsonSerializerSettings _newtonsoftSettings = null!; [GlobalSetup] public void Setup() { - // Newtonsoft WITHOUT reference handling (baseline) - _newtonsoftNoRefSettings = new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, - Formatting = Formatting.None - }; + // Small: ~20 objects (1 item × 1 pallet × 2 measurements × 3 points) + _smallOrder = TestDataFactory.CreateBenchmarkOrder( + itemCount: 1, + palletsPerItem: 1, + measurementsPerPallet: 2, + pointsPerMeasurement: 3); - // Create benchmark data using shared factory - // ~1500 objects: 5 items × 4 pallets × 3 measurements × 5 points = 300 points + containers - _testOrder = TestDataFactory.CreateBenchmarkOrder( + // Medium: ~300 objects (3 items × 2 pallets × 2 measurements × 5 points) + _mediumOrder = TestDataFactory.CreateBenchmarkOrder( + itemCount: 3, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 5); + + // Large: ~1500 objects (5 items × 4 pallets × 3 measurements × 5 points) + _largeOrder = TestDataFactory.CreateBenchmarkOrder( itemCount: 5, palletsPerItem: 4, measurementsPerPallet: 3, pointsPerMeasurement: 5); - // Pre-serialize for deserialization benchmarks - _newtonsoftJson = JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings); - _ayCodeJson = _testOrder.ToJson(); + // STJ options with reference handling + _stjWithRefs = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = null, + WriteIndented = false, + ReferenceHandler = ReferenceHandler.Preserve, + MaxDepth = 256 + }; - // Create target for populate benchmarks - _populateTarget = new TestOrder(); + // STJ options without reference handling + _stjNoRefs = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = null, + WriteIndented = false, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + MaxDepth = 256 + }; + + // AyCode options + _ayCodeWithRefs = AcJsonSerializerOptions.Default; + _ayCodeNoRefs = AcJsonSerializerOptions.WithoutReferenceHandling(); + + // Newtonsoft settings + _newtonsoftSettings = new JsonSerializerSettings + { + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None + }; + + // Pre-serialize for deserialization benchmarks + _smallAyCodeJson = AcJsonSerializer.Serialize(_smallOrder, _ayCodeWithRefs); + _smallAyCodeNoRefJson = AcJsonSerializer.Serialize(_smallOrder, _ayCodeNoRefs); + _smallStjJson = JsonSerializer.Serialize(_smallOrder, _stjWithRefs); + _smallStjNoRefJson = JsonSerializer.Serialize(_smallOrder, _stjNoRefs); + _smallNewtonsoftJson = JsonConvert.SerializeObject(_smallOrder, _newtonsoftSettings); + + _mediumAyCodeJson = AcJsonSerializer.Serialize(_mediumOrder, _ayCodeWithRefs); + _mediumAyCodeNoRefJson = AcJsonSerializer.Serialize(_mediumOrder, _ayCodeNoRefs); + _mediumStjJson = JsonSerializer.Serialize(_mediumOrder, _stjWithRefs); + _mediumStjNoRefJson = JsonSerializer.Serialize(_mediumOrder, _stjNoRefs); + _mediumNewtonsoftJson = JsonConvert.SerializeObject(_mediumOrder, _newtonsoftSettings); + + _largeAyCodeJson = AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs); + _largeAyCodeNoRefJson = AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs); + _largeStjJson = JsonSerializer.Serialize(_largeOrder, _stjWithRefs); + _largeStjNoRefJson = JsonSerializer.Serialize(_largeOrder, _stjNoRefs); + _largeNewtonsoftJson = JsonConvert.SerializeObject(_largeOrder, _newtonsoftSettings); // Output sizes for comparison - var newtonsoftBytes = Encoding.UTF8.GetByteCount(_newtonsoftJson); - var ayCodeBytes = Encoding.UTF8.GetByteCount(_ayCodeJson); - Console.WriteLine("=== JSON Size Comparison ==="); - Console.WriteLine($"Newtonsoft (skip defaults): {_newtonsoftJson.Length:N0} chars ({newtonsoftBytes:N0} bytes)"); - Console.WriteLine($"AyCode (refs+skip): {_ayCodeJson.Length:N0} chars ({ayCodeBytes:N0} bytes)"); - Console.WriteLine($"Size reduction: {(1.0 - (double)ayCodeBytes / newtonsoftBytes) * 100:F1}%"); + Console.WriteLine($"Small: AyCode(refs)={_smallAyCodeJson.Length:N0}, AyCode(noRef)={_smallAyCodeNoRefJson.Length:N0}, STJ(refs)={_smallStjJson.Length:N0}, STJ(noRef)={_smallStjNoRefJson.Length:N0}"); + Console.WriteLine($"Medium: AyCode(refs)={_mediumAyCodeJson.Length:N0}, AyCode(noRef)={_mediumAyCodeNoRefJson.Length:N0}, STJ(refs)={_mediumStjJson.Length:N0}, STJ(noRef)={_mediumStjNoRefJson.Length:N0}"); + Console.WriteLine($"Large: AyCode(refs)={_largeAyCodeJson.Length:N0}, AyCode(noRef)={_largeAyCodeNoRefJson.Length:N0}, STJ(refs)={_largeStjJson.Length:N0}, STJ(noRef)={_largeStjNoRefJson.Length:N0}"); } - #region Serialization Benchmarks + #region Serialize Large - With Refs - [Benchmark(Description = "Newtonsoft (no refs)")] - [BenchmarkCategory("Serialize")] - public string Serialize_Newtonsoft_NoRefs() - => JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings); + [Benchmark(Description = "AyCode Serialize")] + [BenchmarkCategory("Serialize-Large-WithRefs")] + public string Serialize_Large_AyCode_WithRefs() + => AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs); - [Benchmark(Description = "AyCode (with refs)")] - [BenchmarkCategory("Serialize")] - public string Serialize_AyCode_WithRefs() - => _testOrder.ToJson(); - - [Benchmark(Description = "AcJsonSerializer (custom)")] - [BenchmarkCategory("Serialize")] - public string Serialize_AcJsonSerializer() - => AcJsonSerializer.Serialize(_testOrder); + [Benchmark(Description = "STJ Serialize", Baseline = true)] + [BenchmarkCategory("Serialize-Large-WithRefs")] + public string Serialize_Large_STJ_WithRefs() + => JsonSerializer.Serialize(_largeOrder, _stjWithRefs); #endregion - #region Deserialization Benchmarks + #region Serialize Large - No Refs - [Benchmark(Description = "Newtonsoft (no refs)")] - [BenchmarkCategory("Deserialize")] - public TestOrder? Deserialize_Newtonsoft_NoRefs() - => JsonConvert.DeserializeObject(_newtonsoftJson, _newtonsoftNoRefSettings); + [Benchmark(Description = "AyCode Serialize")] + [BenchmarkCategory("Serialize-Large-NoRefs")] + public string Serialize_Large_AyCode_NoRefs() + => AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs); - [Benchmark(Description = "AyCode (with refs)")] - [BenchmarkCategory("Deserialize")] - public TestOrder? Deserialize_AyCode_WithRefs() - => _ayCodeJson.JsonTo(); - - [Benchmark(Description = "AcJsonDeserializer (custom)")] - [BenchmarkCategory("Deserialize")] - public TestOrder? Deserialize_AcJsonDeserializer() - => AcJsonDeserializer.Deserialize(_ayCodeJson); + [Benchmark(Description = "STJ Serialize", Baseline = true)] + [BenchmarkCategory("Serialize-Large-NoRefs")] + public string Serialize_Large_STJ_NoRefs() + => JsonSerializer.Serialize(_largeOrder, _stjNoRefs); #endregion - #region Populate Benchmarks + #region Serialize Medium - With Refs - [Benchmark(Description = "AcJsonDeserializer.Populate")] - [BenchmarkCategory("Populate")] - public void Populate_AcJsonDeserializer() + [Benchmark(Description = "AyCode Serialize")] + [BenchmarkCategory("Serialize-Medium-WithRefs")] + public string Serialize_Medium_AyCode_WithRefs() + => AcJsonSerializer.Serialize(_mediumOrder, _ayCodeWithRefs); + + [Benchmark(Description = "STJ Serialize", Baseline = true)] + [BenchmarkCategory("Serialize-Medium-WithRefs")] + public string Serialize_Medium_STJ_WithRefs() + => JsonSerializer.Serialize(_mediumOrder, _stjWithRefs); + + #endregion + + #region Serialize Medium - No Refs + + [Benchmark(Description = "AyCode Serialize")] + [BenchmarkCategory("Serialize-Medium-NoRefs")] + public string Serialize_Medium_AyCode_NoRefs() + => AcJsonSerializer.Serialize(_mediumOrder, _ayCodeNoRefs); + + [Benchmark(Description = "STJ Serialize", Baseline = true)] + [BenchmarkCategory("Serialize-Medium-NoRefs")] + public string Serialize_Medium_STJ_NoRefs() + => JsonSerializer.Serialize(_mediumOrder, _stjNoRefs); + + #endregion + + #region Small Data Deserialization - With Refs + + [Benchmark(Description = "AyCode WithRefs")] + [BenchmarkCategory("Deserialize-Small-WithRefs")] + public TestOrder? Deserialize_Small_AyCode_WithRefs() + => AcJsonDeserializer.Deserialize(_smallAyCodeJson, _ayCodeWithRefs); + + [Benchmark(Description = "STJ WithRefs", Baseline = true)] + [BenchmarkCategory("Deserialize-Small-WithRefs")] + public TestOrder? Deserialize_Small_STJ_WithRefs() + => JsonSerializer.Deserialize(_smallStjJson, _stjWithRefs); + + #endregion + + #region Small Data Deserialization - No Refs + + [Benchmark(Description = "AyCode NoRefs")] + [BenchmarkCategory("Deserialize-Small-NoRefs")] + public TestOrder? Deserialize_Small_AyCode_NoRefs() + => AcJsonDeserializer.Deserialize(_smallAyCodeNoRefJson, _ayCodeNoRefs); + + [Benchmark(Description = "STJ NoRefs", Baseline = true)] + [BenchmarkCategory("Deserialize-Small-NoRefs")] + public TestOrder? Deserialize_Small_STJ_NoRefs() + => JsonSerializer.Deserialize(_smallStjNoRefJson, _stjNoRefs); + + #endregion + + #region Medium Data Deserialization - With Refs + + [Benchmark(Description = "AyCode WithRefs")] + [BenchmarkCategory("Deserialize-Medium-WithRefs")] + public TestOrder? Deserialize_Medium_AyCode_WithRefs() + => AcJsonDeserializer.Deserialize(_mediumAyCodeJson, _ayCodeWithRefs); + + [Benchmark(Description = "STJ WithRefs", Baseline = true)] + [BenchmarkCategory("Deserialize-Medium-WithRefs")] + public TestOrder? Deserialize_Medium_STJ_WithRefs() + => JsonSerializer.Deserialize(_mediumStjJson, _stjWithRefs); + + #endregion + + #region Medium Data Deserialization - No Refs + + [Benchmark(Description = "AyCode NoRefs")] + [BenchmarkCategory("Deserialize-Medium-NoRefs")] + public TestOrder? Deserialize_Medium_AyCode_NoRefs() + => AcJsonDeserializer.Deserialize(_mediumAyCodeNoRefJson, _ayCodeNoRefs); + + [Benchmark(Description = "STJ NoRefs", Baseline = true)] + [BenchmarkCategory("Deserialize-Medium-NoRefs")] + public TestOrder? Deserialize_Medium_STJ_NoRefs() + => JsonSerializer.Deserialize(_mediumStjNoRefJson, _stjNoRefs); + + #endregion + + #region Large Data Deserialization - With Refs + + [Benchmark(Description = "AyCode WithRefs")] + [BenchmarkCategory("Deserialize-Large-WithRefs")] + public TestOrder? Deserialize_Large_AyCode_WithRefs() + => AcJsonDeserializer.Deserialize(_largeAyCodeJson, _ayCodeWithRefs); + + [Benchmark(Description = "STJ WithRefs", Baseline = true)] + [BenchmarkCategory("Deserialize-Large-WithRefs")] + public TestOrder? Deserialize_Large_STJ_WithRefs() + => JsonSerializer.Deserialize(_largeStjJson, _stjWithRefs); + + #endregion + + #region Large Data Deserialization - No Refs + + [Benchmark(Description = "AyCode NoRefs")] + [BenchmarkCategory("Deserialize-Large-NoRefs")] + public TestOrder? Deserialize_Large_AyCode_NoRefs() + => AcJsonDeserializer.Deserialize(_largeAyCodeNoRefJson, _ayCodeNoRefs); + + [Benchmark(Description = "STJ NoRefs", Baseline = true)] + [BenchmarkCategory("Deserialize-Large-NoRefs")] + public TestOrder? Deserialize_Large_STJ_NoRefs() + => JsonSerializer.Deserialize(_largeStjNoRefJson, _stjNoRefs); + + #endregion + + #region Populate Benchmarks - Small + + [Benchmark(Description = "AyCode Populate")] + [BenchmarkCategory("Populate-Small")] + public void Populate_Small_AyCode() { - // Create fresh target for each iteration to avoid state pollution var target = new TestOrder(); - AcJsonDeserializer.Populate(_ayCodeJson, target); + AcJsonDeserializer.Populate(_smallAyCodeJson, target); } - [Benchmark(Description = "Newtonsoft PopulateObject")] - [BenchmarkCategory("Populate")] - public void Populate_Newtonsoft() + [Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)] + [BenchmarkCategory("Populate-Small")] + public void Populate_Small_Newtonsoft() { - // Create fresh target for each iteration to match the other benchmark var target = new TestOrder(); - JsonConvert.PopulateObject(_newtonsoftJson, target, _newtonsoftNoRefSettings); + JsonConvert.PopulateObject(_smallNewtonsoftJson, target, _newtonsoftSettings); } #endregion - #region JSON Size Comparison + #region Populate Benchmarks - Medium - [Benchmark(Description = "JSON Size - Newtonsoft")] - [BenchmarkCategory("Size")] - public int JsonSize_Newtonsoft() => _newtonsoftJson.Length; + [Benchmark(Description = "AyCode Populate")] + [BenchmarkCategory("Populate-Medium")] + public void Populate_Medium_AyCode() + { + var target = new TestOrder(); + AcJsonDeserializer.Populate(_mediumAyCodeJson, target); + } - [Benchmark(Description = "JSON Size - AyCode")] - [BenchmarkCategory("Size")] - public int JsonSize_AyCode() => _ayCodeJson.Length; + [Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)] + [BenchmarkCategory("Populate-Medium")] + public void Populate_Medium_Newtonsoft() + { + var target = new TestOrder(); + JsonConvert.PopulateObject(_mediumNewtonsoftJson, target, _newtonsoftSettings); + } + + #endregion + + #region Populate Benchmarks - Large + + [Benchmark(Description = "AyCode Populate")] + [BenchmarkCategory("Populate-Large")] + public void Populate_Large_AyCode() + { + var target = new TestOrder(); + AcJsonDeserializer.Populate(_largeAyCodeJson, target); + } + + [Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)] + [BenchmarkCategory("Populate-Large")] + public void Populate_Large_Newtonsoft() + { + var target = new TestOrder(); + JsonConvert.PopulateObject(_largeNewtonsoftJson, target, _newtonsoftSettings); + } #endregion } \ No newline at end of file