From 43f7bb273e83e1cfbe40a3be44fd0962afe7df29 Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Fri, 11 Oct 2024 12:26:51 +0200 Subject: [PATCH] Add `SystemTextJsonSerializer` base class and relevant extensions methods --- .../LowLevelRequestResponseSerializer.cs | 91 +----- .../Serialization/SystemTextJsonSerializer.cs | 168 ++++++++++ .../TransportSerializerExtensions.cs | 305 ++++++++++++++++++ 3 files changed, 483 insertions(+), 81 deletions(-) create mode 100644 src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs diff --git a/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs b/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs index 36602fc..5533549 100644 --- a/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs +++ b/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs @@ -2,33 +2,28 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; + using Elastic.Transport.Extensions; -using static Elastic.Transport.SerializationFormatting; namespace Elastic.Transport; /// -/// Default implementation for . This uses from System.Text.Json. +/// Default low level request/response-serializer implementation for which serializes using +/// the Microsoft System.Text.Json library /// -internal sealed class LowLevelRequestResponseSerializer : Serializer +internal sealed class LowLevelRequestResponseSerializer : + SystemTextJsonSerializer { /// /// Provides a static reusable reference to an instance of to promote reuse. /// internal static readonly LowLevelRequestResponseSerializer Instance = new(); - private readonly Lazy _indented; - private readonly Lazy _none; - private IReadOnlyCollection AdditionalConverters { get; } private IList BakedInConverters { get; } = new List @@ -46,94 +41,28 @@ public LowLevelRequestResponseSerializer() : this(null) { } /// > /// /// Add more default converters onto being used - public LowLevelRequestResponseSerializer(IEnumerable? converters) - { + public LowLevelRequestResponseSerializer(IEnumerable? converters) => AdditionalConverters = converters != null ? new ReadOnlyCollection(converters.ToList()) : EmptyReadOnly.Collection; - _indented = new Lazy(() => CreateSerializerOptions(Indented)); - _none = new Lazy(() => CreateSerializerOptions(None)); - } /// /// Creates used for serialization. /// Override on a derived serializer to change serialization. /// - public JsonSerializerOptions CreateSerializerOptions(SerializationFormatting formatting) + protected override JsonSerializerOptions? CreateJsonSerializerOptions() { var options = new JsonSerializerOptions { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = formatting == Indented, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; + foreach (var converter in BakedInConverters) options.Converters.Add(converter); + foreach (var converter in AdditionalConverters) options.Converters.Add(converter); return options; - - } - - private static bool TryReturnDefault(Stream? stream, out T deserialize) - { - deserialize = default; - return stream == null || stream == Stream.Null || (stream.CanSeek && stream.Length == 0); - } - - private JsonSerializerOptions GetFormatting(SerializationFormatting formatting) => formatting == None ? _none.Value : _indented.Value; - - /// > - public override object Deserialize(Type type, Stream stream) - { - if (TryReturnDefault(stream, out object deserialize)) return deserialize; - - return JsonSerializer.Deserialize(stream, type, _none.Value)!; - } - - /// > - public override T Deserialize(Stream stream) - { - if (TryReturnDefault(stream, out T deserialize)) return deserialize; - - return JsonSerializer.Deserialize(stream, _none.Value); - } - - /// > - public override void Serialize(T data, Stream stream, SerializationFormatting formatting = None) - { - using var writer = new Utf8JsonWriter(stream); - if (data == null) - JsonSerializer.Serialize(writer, null, typeof(object), GetFormatting(formatting)); - //TODO validate if we can avoid boxing by checking if data is typeof(object) - else - JsonSerializer.Serialize(writer, data, data.GetType(), GetFormatting(formatting)); - } - - /// > - public override async Task SerializeAsync(T data, Stream stream, SerializationFormatting formatting = None, - CancellationToken cancellationToken = default - ) - { - if (data == null) - await JsonSerializer.SerializeAsync(stream, null, typeof(object), GetFormatting(formatting), cancellationToken).ConfigureAwait(false); - else - await JsonSerializer.SerializeAsync(stream, data, data.GetType(), GetFormatting(formatting), cancellationToken).ConfigureAwait(false); - } - - /// > - public override ValueTask DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default) - { - if (TryReturnDefault(stream, out object deserialize)) return new ValueTask(deserialize); - - return JsonSerializer.DeserializeAsync(stream, type, _none.Value, cancellationToken); - } - - /// > - public override ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) - { - if (TryReturnDefault(stream, out T deserialize)) return new ValueTask(deserialize); - - return JsonSerializer.DeserializeAsync(stream, _none.Value, cancellationToken); } } diff --git a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs new file mode 100644 index 0000000..6172ff8 --- /dev/null +++ b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs @@ -0,0 +1,168 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Transport; + +/// +/// An abstract implementation of a transport which serializes using the Microsoft +/// System.Text.Json library. +/// +public abstract class SystemTextJsonSerializer : + Serializer +{ + private readonly SemaphoreSlim _semaphore = new(1); + + private bool _initialized; + private JsonSerializerOptions? _options; + private JsonSerializerOptions? _indentedOptions; + + #region Serializer + + /// + public override T Deserialize(Stream stream) + { + Initialize(); + + if (TryReturnDefault(stream, out T deserialize)) + return deserialize; + + return JsonSerializer.Deserialize(stream, _options); + } + + /// + public override object? Deserialize(Type type, Stream stream) + { + Initialize(); + + if (TryReturnDefault(stream, out object deserialize)) + return deserialize; + + return JsonSerializer.Deserialize(stream, type, _options); + } + + /// + public override ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) + { + Initialize(); + + if (TryReturnDefault(stream, out T deserialize)) + return new ValueTask(deserialize); + + return JsonSerializer.DeserializeAsync(stream, _options, cancellationToken); + } + + /// + public override ValueTask DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default) + { + Initialize(); + + if (TryReturnDefault(stream, out object deserialize)) + return new ValueTask(deserialize); + + return JsonSerializer.DeserializeAsync(stream, type, _options, cancellationToken); + } + + /// + public override void Serialize(T data, Stream writableStream, + SerializationFormatting formatting = SerializationFormatting.None) + { + Initialize(); + + JsonSerializer.Serialize(writableStream, data, GetJsonSerializerOptions(formatting)); + } + + /// + public override Task SerializeAsync(T data, Stream stream, + SerializationFormatting formatting = SerializationFormatting.None, + CancellationToken cancellationToken = default) + { + Initialize(); + + return JsonSerializer.SerializeAsync(stream, data, GetJsonSerializerOptions(formatting), cancellationToken); + } + + #endregion Serializer + + /// + /// A factory method that can create an instance of that will + /// be used when serializing. + /// + /// + protected abstract JsonSerializerOptions? CreateJsonSerializerOptions(); + + /// + /// A callback function that is invoked after the have been created and the + /// serializer got fully initialized. + /// + protected virtual void Initialized() + { + } + + /// + /// Returns the for this serializer, based on the given . + /// + /// The serialization formatting. + /// The requested or null, if the serializer is not initialized yet. + protected internal JsonSerializerOptions? GetJsonSerializerOptions(SerializationFormatting formatting) => (formatting is SerializationFormatting.None) + ? _options + : _indentedOptions; + + /// + /// Initializes a serializer instance such that its are populated. + /// + protected internal void Initialize() + { + // Exit early, if already initialized + if (_initialized) + return; + + _semaphore.Wait(); + + try + { + // Exit early, if the current thread lost the race + if (_initialized) + return; + + var options = CreateJsonSerializerOptions(); + + if (options is null) + { + _options = new JsonSerializerOptions(); + _indentedOptions = new JsonSerializerOptions + { + WriteIndented = true + }; + } + else + { + _options = options; + _indentedOptions = new JsonSerializerOptions(options) + { + WriteIndented = true + }; + } + + _initialized = true; + + Initialized(); + } + finally + { + _semaphore.Release(); + } + } + + private static bool TryReturnDefault(Stream? stream, out T deserialize) + { + deserialize = default; + return (stream is null) || stream == Stream.Null || (stream.CanSeek && stream.Length == 0); + } +} diff --git a/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs b/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs index 68c0c8e..6ee146f 100644 --- a/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs +++ b/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs @@ -2,6 +2,11 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.IO; +using System.Text.Json; +using System; +using System.Text.Json.Nodes; + namespace Elastic.Transport.Extensions; /// @@ -72,4 +77,304 @@ public static string SerializeToString( serializer.Serialize(data, ms, formatting); return ms.Utf8String(); } + + #region STJ Extensions + + /// + /// Extension method that writes the serialized representation of an instance of to a + /// . + /// + /// The type of the data to be serialized. + /// + /// The data to serialize. + /// The destination . + /// + /// A factory yielding instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// + public static void Serialize( + this Serializer serializer, + T? data, + Utf8JsonWriter writer, + MemoryStreamFactory? memoryStreamFactory = null, + SerializationFormatting formatting = SerializationFormatting.None) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // serialize straight into the writer. + JsonSerializer.Serialize(writer, data, stjSerializer.GetJsonSerializerOptions(formatting)); + return; + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + serializer.Serialize(data, ms); + ms.Position = 0; + +#if NET6_0_OR_GREATER + writer.WriteRawValue(ms.GetBuffer().AsSpan()[..(int)ms.Length], true); +#else + using var document = JsonDocument.Parse(ms); + document.RootElement.WriteTo(writer); +#endif + } + + /// + /// Extension method that writes the serialized representation of the given to a + /// . + /// + /// + /// The data to serialize. + /// The type of the data to serialize. + /// The destination . + /// + /// A factory yielding instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// + public static void Serialize( + this Serializer serializer, + object? data, + Type type, + Utf8JsonWriter writer, + MemoryStreamFactory? memoryStreamFactory = null, + SerializationFormatting formatting = SerializationFormatting.None) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // serialize straight into the writer. + JsonSerializer.Serialize(writer, data, type, stjSerializer.GetJsonSerializerOptions(formatting)); + return; + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + serializer.Serialize(data, ms); + ms.Position = 0; + +#if NET6_0_OR_GREATER + writer.WriteRawValue(ms.GetBuffer().AsSpan()[..(int)ms.Length], true); +#else + using var document = JsonDocument.Parse(ms); + document.RootElement.WriteTo(writer); +#endif + } + + /// + /// Extension method that deserializes from a given . + /// + /// The type of the data to be deserialized. + /// + /// The source + /// + /// A factory yielding instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// The deserialized data. + public static T? Deserialize( + this Serializer serializer, + ref Utf8JsonReader reader, + MemoryStreamFactory? memoryStreamFactory = null) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // deserialize straight from the reader. + return JsonSerializer.Deserialize(ref reader, stjSerializer.GetJsonSerializerOptions(SerializationFormatting.None)); + } + + using var jsonDoc = JsonSerializer.Deserialize(ref reader); + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + var writer = new Utf8JsonWriter(ms); + jsonDoc.WriteTo(writer); + writer.Flush(); + ms.Position = 0; + + return serializer.Deserialize(ms); + } + + /// + /// Extension method that deserializes from a given . + /// + /// + /// The source + /// The type of the data to be deserialized. + /// + /// A factory yielding instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// The deserialized data. + public static object? Deserialize( + this Serializer serializer, + ref Utf8JsonReader reader, + Type type, + MemoryStreamFactory? memoryStreamFactory = null) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // deserialize straight from the reader. + return JsonSerializer.Deserialize(ref reader, type, stjSerializer.GetJsonSerializerOptions(SerializationFormatting.None)); + } + + using var jsonDoc = JsonSerializer.Deserialize(ref reader); + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + var writer = new Utf8JsonWriter(ms); + jsonDoc.WriteTo(writer); + writer.Flush(); + ms.Position = 0; + + return serializer.Deserialize(type, ms); + } + + /// + /// Extension method that deserializes from a given . + /// + /// The type of the data to be deserialized. + /// + /// The source + /// + /// A factory yielding instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// The deserialized data. + public static T? Deserialize( + this Serializer serializer, + JsonNode node, + MemoryStreamFactory? memoryStreamFactory = null) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // deserialize straight from the node. + return node.Deserialize(stjSerializer.GetJsonSerializerOptions(SerializationFormatting.None)); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + using var writer = new Utf8JsonWriter(ms); + node.WriteTo(writer); + writer.Flush(); + ms.Position = 0; + + return serializer.Deserialize(ms); + } + + /// + /// Extension method that deserializes from a given . + /// + /// + /// The source + /// The type of the data to be deserialized. + /// + /// A factory yielding instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// The deserialized data. + public static object? Deserialize( + this Serializer serializer, + JsonNode node, + Type type, + MemoryStreamFactory? memoryStreamFactory = null) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // deserialize straight from the node. + return node.Deserialize(type, stjSerializer.GetJsonSerializerOptions(SerializationFormatting.None)); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + using var writer = new Utf8JsonWriter(ms); + node.WriteTo(writer); + writer.Flush(); + ms.Position = 0; + + return serializer.Deserialize(type, ms); + } + + /// + /// Extension method that deserializes from a given . + /// + /// The type of the data to be deserialized. + /// + /// The source + /// + /// A factory yielding instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// The deserialized data. + public static T? Deserialize( + this Serializer serializer, + JsonElement node, + MemoryStreamFactory? memoryStreamFactory = null) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // deserialize straight from the node. + return node.Deserialize(stjSerializer.GetJsonSerializerOptions(SerializationFormatting.None)); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + using var writer = new Utf8JsonWriter(ms); + node.WriteTo(writer); + writer.Flush(); + ms.Position = 0; + + return serializer.Deserialize(ms); + } + + /// + /// Extension method that deserializes from a given . + /// + /// + /// The source + /// The type of the data to be deserialized. + /// + /// A factory yielding instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// The deserialized data. + public static object? Deserialize( + this Serializer serializer, + JsonElement node, + Type type, + MemoryStreamFactory? memoryStreamFactory = null) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // deserialize straight from the node. + return node.Deserialize(type, stjSerializer.GetJsonSerializerOptions(SerializationFormatting.None)); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + using var writer = new Utf8JsonWriter(ms); + node.WriteTo(writer); + writer.Flush(); + ms.Position = 0; + + return serializer.Deserialize(type, ms); + } + + #endregion STJ Extensions }