751 lines
28 KiB
C#
751 lines
28 KiB
C#
using System.Collections;
|
|
using System.Linq.Expressions;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using AyCode.Core.Helpers;
|
|
using AyCode.Core.Serializers.Expressions;
|
|
using static AyCode.Core.Helpers.JsonUtilities;
|
|
|
|
namespace AyCode.Core.Serializers.Jsons;
|
|
|
|
/// <summary>
|
|
/// Exception thrown when JSON deserialization fails.
|
|
/// </summary>
|
|
public class AcJsonDeserializationException : Exception
|
|
{
|
|
public string? Json { get; }
|
|
public Type? TargetType { get; }
|
|
|
|
public AcJsonDeserializationException(string message, string? json = null, Type? targetType = null, Exception? innerException = null)
|
|
: base(message, innerException)
|
|
{
|
|
Json = json?.Length > 500 ? json[..500] + "..." : json;
|
|
TargetType = targetType;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// High-performance custom JSON deserializer optimized for IId<T> reference handling.
|
|
/// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach).
|
|
/// </summary>
|
|
public static partial class AcJsonDeserializer
|
|
{
|
|
// 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();
|
|
private static readonly Type ExpressionBaseType = typeof(Expression);
|
|
|
|
#region Public API
|
|
|
|
/// <summary>
|
|
/// Deserialize JSON string to a new object of type T with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static T? Deserialize<T>(string json) => Deserialize<T>(json, AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Deserialize UTF-8 encoded JSON bytes to a new object of type T with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static T? Deserialize<T>(ReadOnlySpan<byte> utf8Json) => Deserialize<T>(utf8Json, AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Deserialize UTF-8 encoded JSON bytes to a new object of type T with specified options.
|
|
/// </summary>
|
|
public static T? Deserialize<T>(ReadOnlySpan<byte> utf8Json, in AcJsonSerializerOptions options)
|
|
{
|
|
if (utf8Json.IsEmpty) return default;
|
|
if (utf8Json.Length == 4 && utf8Json.SequenceEqual("null"u8)) return default;
|
|
|
|
var targetType = typeof(T);
|
|
|
|
// Handle Expression types
|
|
if (IsExpressionType(targetType))
|
|
{
|
|
return (T?)(object?)DeserializeExpression(utf8Json, targetType, options);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!options.UseReferenceHandling)
|
|
{
|
|
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
|
if (!reader.Read()) return default;
|
|
return (T?)ReadValueFromReader(ref reader, targetType, options.MaxDepth, 0);
|
|
}
|
|
|
|
var jsonBytes = utf8Json.ToArray();
|
|
using var doc = JsonDocument.Parse(jsonBytes);
|
|
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)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", null, targetType, ex);
|
|
}
|
|
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", null, targetType, ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize JSON string to a new object of type T with specified options.
|
|
/// </summary>
|
|
public static T? Deserialize<T>(string json, in AcJsonSerializerOptions options)
|
|
{
|
|
if (string.IsNullOrEmpty(json) || json == "null") return default;
|
|
|
|
var targetType = typeof(T);
|
|
|
|
// Handle Expression types
|
|
if (IsExpressionType(targetType))
|
|
{
|
|
var (buffer, length) = GetUtf8Bytes(json);
|
|
try
|
|
{
|
|
return (T?)(object?)DeserializeExpression(buffer.AsSpan(0, length), targetType, options);
|
|
}
|
|
finally
|
|
{
|
|
ReturnUtf8Buffer(buffer);
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
|
|
return (T?)primitiveResult;
|
|
|
|
ValidateJson(json, targetType);
|
|
|
|
if (!options.UseReferenceHandling)
|
|
{
|
|
return DeserializeWithUtf8Reader<T>(json, options.MaxDepth);
|
|
}
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
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)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", json, targetType, ex);
|
|
}
|
|
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", json, targetType, ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize JSON string to specified type with specified options.
|
|
/// </summary>
|
|
public static object? Deserialize(string json, in Type targetType, in AcJsonSerializerOptions options)
|
|
{
|
|
if (string.IsNullOrEmpty(json) || json == "null") return null;
|
|
|
|
// Handle Expression types - deserialize as AcExpressionNode and rebuild
|
|
if (AcSerializerCommon.IsExpressionType(targetType))
|
|
{
|
|
var (buffer, length) = GetUtf8Bytes(json);
|
|
try
|
|
{
|
|
return DeserializeExpression(buffer.AsSpan(0, length), targetType, options);
|
|
}
|
|
finally
|
|
{
|
|
ReturnUtf8Buffer(buffer);
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
var firstChar = json[0];
|
|
var isArrayJson = firstChar == '[';
|
|
var isObjectJson = firstChar == '{';
|
|
|
|
if (!isArrayJson && !isObjectJson)
|
|
{
|
|
if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
|
|
return primitiveResult;
|
|
}
|
|
|
|
ValidateJson(json, targetType);
|
|
|
|
// Fast path for no reference handling
|
|
if (!options.UseReferenceHandling)
|
|
{
|
|
return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth);
|
|
}
|
|
|
|
// Reference handling requires DOM - copy to array for JsonDocument.Parse
|
|
var jsonBytes = Encoding.UTF8.GetBytes(json);
|
|
using var doc = JsonDocument.Parse(jsonBytes);
|
|
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)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", json, targetType, ex);
|
|
}
|
|
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", json, targetType, ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize Expression from JSON.
|
|
/// Uses AcSerializerCommon for entity type extraction.
|
|
/// </summary>
|
|
private static Expression? DeserializeExpression(ReadOnlySpan<byte> utf8Json, Type targetExpressionType, in AcJsonSerializerOptions options)
|
|
{
|
|
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
|
if (!reader.Read()) return null;
|
|
|
|
var node = (AcExpressionNode?)ReadValueFromReader(ref reader, typeof(AcExpressionNode), options.MaxDepth, 0);
|
|
if (node == null) return null;
|
|
|
|
var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType);
|
|
return AcExpressionRebuilder.FromNode(node, entityType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a type is an Expression type.
|
|
/// Uses AcSerializerCommon for consistency.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsExpressionType(Type type)
|
|
{
|
|
return AcSerializerCommon.IsExpressionType(type);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize UTF-8 encoded JSON bytes to specified type with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static object? Deserialize(ReadOnlySpan<byte> utf8Json, Type targetType)
|
|
=> Deserialize(utf8Json, targetType, AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Deserialize UTF-8 encoded JSON bytes to specified type with specified options.
|
|
/// </summary>
|
|
public static object? Deserialize(ReadOnlySpan<byte> utf8Json, in Type targetType, in AcJsonSerializerOptions options)
|
|
{
|
|
if (utf8Json.IsEmpty) return null;
|
|
|
|
// Check for "null" literal
|
|
if (utf8Json.Length == 4 && utf8Json.SequenceEqual("null"u8)) return null;
|
|
|
|
// Handle Expression types
|
|
if (AcSerializerCommon.IsExpressionType(targetType))
|
|
{
|
|
return DeserializeExpression(utf8Json, targetType, options);
|
|
}
|
|
|
|
try
|
|
{
|
|
// Fast path for no reference handling
|
|
if (!options.UseReferenceHandling)
|
|
{
|
|
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
|
if (!reader.Read()) return null;
|
|
return ReadValueFromReader(ref reader, targetType, options.MaxDepth, 0);
|
|
}
|
|
|
|
// Reference handling requires DOM - copy to array for JsonDocument.Parse
|
|
var jsonBytes = utf8Json.ToArray();
|
|
using var doc = JsonDocument.Parse(jsonBytes);
|
|
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)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", null, targetType, ex);
|
|
}
|
|
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", null, targetType, ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize JSON string to specified type with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Populate existing object with JSON data (merge mode) with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Populate<T>(string json, T target) where T : class
|
|
=> Populate(json, target, AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Populate existing object with JSON data (merge mode) with specified options.
|
|
/// </summary>
|
|
public static void Populate<T>(string json, T target, in AcJsonSerializerOptions options) where T : class
|
|
{
|
|
ArgumentNullException.ThrowIfNull(target);
|
|
if (string.IsNullOrEmpty(json) || json == "null") return;
|
|
PopulateInternal(json, target, target.GetType(), options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate existing object with JSON data with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Populate(string json, object target)
|
|
=> Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Populate existing object with JSON data with specified options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Populate(string json, object target, in AcJsonSerializerOptions options)
|
|
=> Populate(json, target, target.GetType(), options);
|
|
|
|
/// <summary>
|
|
/// Populate existing object with JSON data with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Populate(string json, object target, Type targetType)
|
|
=> Populate(json, target, targetType, AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Populate existing object with JSON data with specified options.
|
|
/// </summary>
|
|
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);
|
|
|
|
// 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;
|
|
|
|
var context = DeserializationContextPool.Get(options);
|
|
context.IsMergeMode = true;
|
|
|
|
try
|
|
{
|
|
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 object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
|
|
|
|
context.ResolveReferences();
|
|
}
|
|
finally
|
|
{
|
|
DeserializationContextPool.Return(context);
|
|
}
|
|
}
|
|
catch (AcJsonDeserializationException) { throw; }
|
|
catch (System.Text.Json.JsonException ex)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to parse JSON for population of type '{targetType.Name}': {ex.Message}", json, targetType, ex);
|
|
}
|
|
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
|
|
{
|
|
throw new AcJsonDeserializationException($"Failed to convert JSON value during population of type '{targetType.Name}': {ex.Message}", json, targetType, ex);
|
|
}
|
|
}
|
|
|
|
#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 Chain API
|
|
|
|
/// <summary>
|
|
/// Create a deserialize chain that parses JSON once and allows multiple deserializations.
|
|
/// Efficient for deserializing the same JSON to multiple different types.
|
|
/// </summary>
|
|
public static IDeserializeChain<T> CreateDeserializeChain<T>(string json)
|
|
=> CreateDeserializeChain<T>(json, AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Create a deserialize chain with options.
|
|
/// </summary>
|
|
public static IDeserializeChain<T> CreateDeserializeChain<T>(string json, in AcJsonSerializerOptions options)
|
|
{
|
|
if (string.IsNullOrEmpty(json) || json == "null")
|
|
return DeserializeChain<T>.Empty;
|
|
|
|
var targetType = typeof(T);
|
|
ValidateJson(json, targetType);
|
|
|
|
var doc = JsonDocument.Parse(json);
|
|
var context = DeserializationContextPool.Get(options);
|
|
|
|
try
|
|
{
|
|
var result = ReadValue(doc.RootElement, targetType, context, 0);
|
|
context.ResolveReferences();
|
|
return new DeserializeChain<T>(doc, context, options, (T?)result);
|
|
}
|
|
catch
|
|
{
|
|
DeserializationContextPool.Return(context);
|
|
doc.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a populate chain that parses JSON once and allows populating multiple objects.
|
|
/// Efficient for populating multiple objects from the same JSON source.
|
|
/// </summary>
|
|
public static IPopulateChain CreatePopulateChain(string json, object target)
|
|
=> CreatePopulateChain(json, target, AcJsonSerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Create a populate chain with options.
|
|
/// </summary>
|
|
public static IPopulateChain CreatePopulateChain(string json, object target, in AcJsonSerializerOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(target);
|
|
|
|
if (string.IsNullOrEmpty(json) || json == "null")
|
|
return PopulateChain.Empty;
|
|
|
|
var targetType = target.GetType();
|
|
ValidateJson(json, targetType);
|
|
|
|
var doc = JsonDocument.Parse(json);
|
|
var context = DeserializationContextPool.Get(options);
|
|
context.IsMergeMode = true;
|
|
|
|
try
|
|
{
|
|
PopulateFromDocument(doc.RootElement, target, targetType, context);
|
|
context.ResolveReferences();
|
|
return new PopulateChain(doc, context, options);
|
|
}
|
|
catch
|
|
{
|
|
DeserializationContextPool.Return(context);
|
|
doc.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal helper: populate object from parsed JsonDocument.
|
|
/// Reuses existing populate logic without reparsing.
|
|
/// </summary>
|
|
private static void PopulateFromDocument(in JsonElement rootElement, object target, in Type targetType, DeserializationContext context)
|
|
{
|
|
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", null, targetType);
|
|
}
|
|
else if (rootElement.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var metadata = GetTypeMetadata(targetType);
|
|
PopulateObjectInternalMerge(rootElement, target, metadata, context, 0);
|
|
}
|
|
else
|
|
throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", null, targetType);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Chain Implementations (Nested Classes)
|
|
|
|
/// <summary>
|
|
/// Implementation of deserialize chain.
|
|
/// </summary>
|
|
private sealed class DeserializeChain<T> : IDeserializeChain<T>
|
|
{
|
|
public static readonly IDeserializeChain<T> Empty = new EmptyDeserializeChain();
|
|
|
|
private JsonDocument? _document;
|
|
private DeserializationContext? _context;
|
|
private readonly AcJsonSerializerOptions _options;
|
|
|
|
public T? Value { get; }
|
|
|
|
public DeserializeChain(JsonDocument document, DeserializationContext context, AcJsonSerializerOptions options, T? value)
|
|
{
|
|
_document = document;
|
|
_context = context;
|
|
_options = options;
|
|
Value = value;
|
|
}
|
|
|
|
public TOther? ThenDeserialize<TOther>()
|
|
{
|
|
if (_document == null || _context == null)
|
|
throw new ObjectDisposedException(nameof(DeserializeChain<T>));
|
|
|
|
var targetType = typeof(TOther);
|
|
|
|
try
|
|
{
|
|
var result = ReadValue(_document.RootElement, targetType, _context, 0);
|
|
_context.ResolveReferences();
|
|
return (TOther?)result;
|
|
}
|
|
catch (AcJsonDeserializationException) { throw; }
|
|
catch (Exception ex)
|
|
{
|
|
throw new AcJsonDeserializationException(
|
|
$"Failed to deserialize to type '{targetType.Name}' in chain: {ex.Message}",
|
|
null, targetType, ex);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_context != null)
|
|
{
|
|
DeserializationContextPool.Return(_context);
|
|
_context = null;
|
|
}
|
|
if (_document != null)
|
|
{
|
|
_document.Dispose();
|
|
_document = null;
|
|
}
|
|
}
|
|
|
|
private sealed class EmptyDeserializeChain : IDeserializeChain<T>
|
|
{
|
|
public T? Value => default;
|
|
public TOther? ThenDeserialize<TOther>() => default;
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implementation of populate chain.
|
|
/// </summary>
|
|
private sealed class PopulateChain : IPopulateChain
|
|
{
|
|
public static readonly IPopulateChain Empty = new EmptyPopulateChain();
|
|
|
|
private JsonDocument? _document;
|
|
private DeserializationContext? _context;
|
|
private readonly AcJsonSerializerOptions _options;
|
|
|
|
public PopulateChain(JsonDocument document, DeserializationContext context, AcJsonSerializerOptions options)
|
|
{
|
|
_document = document;
|
|
_context = context;
|
|
_options = options;
|
|
}
|
|
|
|
public IPopulateChain ThenPopulate(object target)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(target);
|
|
|
|
if (_document == null || _context == null)
|
|
throw new ObjectDisposedException(nameof(PopulateChain));
|
|
|
|
var targetType = target.GetType();
|
|
|
|
try
|
|
{
|
|
PopulateFromDocument(_document.RootElement, target, targetType, _context);
|
|
_context.ResolveReferences();
|
|
return this;
|
|
}
|
|
catch (AcJsonDeserializationException) { throw; }
|
|
catch (Exception ex)
|
|
{
|
|
throw new AcJsonDeserializationException(
|
|
$"Failed to populate object of type '{targetType.Name}' in chain: {ex.Message}",
|
|
null, targetType, ex);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_context != null)
|
|
{
|
|
DeserializationContextPool.Return(_context);
|
|
_context = null;
|
|
}
|
|
if (_document != null)
|
|
{
|
|
_document.Dispose();
|
|
_document = null;
|
|
}
|
|
}
|
|
|
|
private sealed class EmptyPopulateChain : IPopulateChain
|
|
{
|
|
public IPopulateChain ThenPopulate(object target) => this;
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region Chain Public Interfaces
|
|
|
|
/// <summary>
|
|
/// Represents a deserialize chain that allows multiple deserializations from the same parsed JSON.
|
|
/// Implements IDisposable - call Dispose() when done or use 'using' statement.
|
|
/// </summary>
|
|
public interface IDeserializeChain<T> : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// The first deserialized value.
|
|
/// </summary>
|
|
T? Value { get; }
|
|
|
|
/// <summary>
|
|
/// Deserialize to another type from the same JSON.
|
|
/// </summary>
|
|
TOther? ThenDeserialize<TOther>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a populate chain that allows populating multiple objects from the same parsed JSON.
|
|
/// Implements IDisposable - call Dispose() when done or use 'using' statement.
|
|
/// </summary>
|
|
public interface IPopulateChain : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Populate another object from the same JSON.
|
|
/// </summary>
|
|
IPopulateChain ThenPopulate(object target);
|
|
}
|
|
|
|
#endregion
|