From a3f1378602baca4974a9780cbd58af9eadc90c5d Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Fri, 11 Oct 2024 12:26:51 +0200 Subject: [PATCH 1/7] 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 } From aaea31afe0dcf9298cf63b3784210f282560384b Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Fri, 11 Oct 2024 13:19:30 +0200 Subject: [PATCH 2/7] Add `ReadOnlySpan` and `ReadOnlySpan` overloads to `TransportSerializerExtensions` --- .../Components/Serialization/Serializer.cs | 2 + .../Serialization/SystemTextJsonSerializer.cs | 7 +- .../TransportSerializerExtensions.cs | 131 +++++++++++++++++- 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/src/Elastic.Transport/Components/Serialization/Serializer.cs b/src/Elastic.Transport/Components/Serialization/Serializer.cs index 04e3744..f0a58c4 100644 --- a/src/Elastic.Transport/Components/Serialization/Serializer.cs +++ b/src/Elastic.Transport/Components/Serialization/Serializer.cs @@ -32,6 +32,8 @@ public abstract class Serializer /// public abstract ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = default); + // TODO: Overloads for (object?, Type) inputs + /// /// Serialize an instance of to using . /// diff --git a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs index 6172ff8..7c0e0f3 100644 --- a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs +++ b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs @@ -110,9 +110,10 @@ protected virtual void Initialized() /// /// 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; + protected internal JsonSerializerOptions? GetJsonSerializerOptions(SerializationFormatting formatting = SerializationFormatting.None) => + (formatting is SerializationFormatting.None) + ? _options + : _indentedOptions; /// /// Initializes a serializer instance such that its are populated. diff --git a/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs b/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs index 6ee146f..22c065c 100644 --- a/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs +++ b/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text.Json; using System; +using System.Text; using System.Text.Json.Nodes; namespace Elastic.Transport.Extensions; @@ -80,6 +81,124 @@ public static string SerializeToString( #region STJ Extensions + /// + /// Extension method that deserializes from a UTF8 . + /// + /// The type of the data to be deserialized. + /// + /// The source that contains the UTF8 encoded JSON string. + /// + /// 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, + ReadOnlySpan span, + MemoryStreamFactory? memoryStreamFactory = null) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // deserialize straight from the span. + return JsonSerializer.Deserialize(span, stjSerializer.GetJsonSerializerOptions()); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(span.ToArray()); + + return serializer.Deserialize(ms); + } + + /// + /// Extension method that deserializes from a UTF8 . + /// + /// + /// The source that contains the UTF8 encoded JSON. + /// 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, + ReadOnlySpan span, + 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 span. + return JsonSerializer.Deserialize(span, type, stjSerializer.GetJsonSerializerOptions()); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(span.ToArray()); + + return serializer.Deserialize(type, ms); + } + + /// + /// Extension method that deserializes from a UTF8 . + /// + /// The type of the data to be deserialized. + /// + /// The source that contains the UTF8 encoded JSON string. + /// + /// 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, + ReadOnlySpan span, + MemoryStreamFactory? memoryStreamFactory = null) + { + if (serializer is SystemTextJsonSerializer stjSerializer) + { + // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and + // deserialize straight from the span. + return JsonSerializer.Deserialize(span, stjSerializer.GetJsonSerializerOptions()); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(Encoding.UTF8.GetBytes(span.ToArray())); + + return serializer.Deserialize(ms); + } + + /// + /// Extension method that deserializes from a UTF8 . + /// + /// + /// The source that contains the UTF8 encoded JSON. + /// 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, + ReadOnlySpan span, + 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 span. + return JsonSerializer.Deserialize(span, type, stjSerializer.GetJsonSerializerOptions()); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(Encoding.UTF8.GetBytes(span.ToArray())); + + return serializer.Deserialize(type, ms); + } + /// /// Extension method that writes the serialized representation of an instance of to a /// . @@ -185,7 +304,7 @@ public static void Serialize( { // 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)); + return JsonSerializer.Deserialize(ref reader, stjSerializer.GetJsonSerializerOptions()); } using var jsonDoc = JsonSerializer.Deserialize(ref reader); @@ -222,7 +341,7 @@ public static void Serialize( { // 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)); + return JsonSerializer.Deserialize(ref reader, type, stjSerializer.GetJsonSerializerOptions()); } using var jsonDoc = JsonSerializer.Deserialize(ref reader); @@ -258,7 +377,7 @@ public static void Serialize( { // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and // deserialize straight from the node. - return node.Deserialize(stjSerializer.GetJsonSerializerOptions(SerializationFormatting.None)); + return node.Deserialize(stjSerializer.GetJsonSerializerOptions()); } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; @@ -293,7 +412,7 @@ public static void Serialize( { // 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)); + return node.Deserialize(type, stjSerializer.GetJsonSerializerOptions()); } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; @@ -327,7 +446,7 @@ public static void Serialize( { // When the serializer derives from `SystemTextJsonSerializer` we can avoid unnecessary allocations and // deserialize straight from the node. - return node.Deserialize(stjSerializer.GetJsonSerializerOptions(SerializationFormatting.None)); + return node.Deserialize(stjSerializer.GetJsonSerializerOptions()); } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; @@ -362,7 +481,7 @@ public static void Serialize( { // 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)); + return node.Deserialize(type, stjSerializer.GetJsonSerializerOptions()); } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; From 1ce032cafc7f5bab12482909d19dca50c8b8b913 Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Fri, 11 Oct 2024 13:39:08 +0200 Subject: [PATCH 3/7] Improve existing `SerializeToBytes()` and `SerializeToString()` extensions --- .../TransportSerializerExtensions.cs | 301 +++++++++++++----- 1 file changed, 220 insertions(+), 81 deletions(-) diff --git a/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs b/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs index 22c065c..cfc6a87 100644 --- a/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs +++ b/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs @@ -18,25 +18,69 @@ public static class TransportSerializerExtensions /// /// Extension method that serializes an instance of to a byte array. /// + /// The type of the data to be serialized. + /// + /// + /// public static byte[] SerializeToBytes( this Serializer serializer, - T data, + T? data, SerializationFormatting formatting = SerializationFormatting.None) => SerializeToBytes(serializer, data, TransportConfiguration.DefaultMemoryStreamFactory, formatting); /// /// Extension method that serializes an instance of to a byte array. /// + /// The type of the data to be serialized. + /// /// /// /// A factory yielding MemoryStream instances, defaults to /// that yields memory streams backed by pooled byte arrays. /// - /// /// public static byte[] SerializeToBytes( this Serializer serializer, - T data, + T? data, + MemoryStreamFactory? memoryStreamFactory = null, + SerializationFormatting formatting = SerializationFormatting.None + ) + { + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + serializer.Serialize(data, ms, formatting); + return ms.ToArray(); + } + + /// + /// Extension method that serializes the given to a byte array. + /// + /// + /// + /// The type of the data to serialize. + /// + public static byte[] SerializeToBytes( + this Serializer serializer, + object? data, + Type type, + SerializationFormatting formatting = SerializationFormatting.None) => + SerializeToBytes(serializer, data, type, TransportConfiguration.DefaultMemoryStreamFactory, formatting); + + /// + /// Extension method that serializes the given to a byte array. + /// + /// + /// + /// The type of the data to serialize. + /// + /// A factory yielding MemoryStream instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// + public static byte[] SerializeToBytes( + this Serializer serializer, + object? data, + Type type, MemoryStreamFactory? memoryStreamFactory = null, SerializationFormatting formatting = SerializationFormatting.None ) @@ -50,94 +94,215 @@ public static byte[] SerializeToBytes( /// /// Extension method that serializes an instance of to a string. /// + /// The type of the data to be serialized. + /// + /// The data to serialize. + /// public static string SerializeToString( this Serializer serializer, - T data, + T? data, SerializationFormatting formatting = SerializationFormatting.None) => SerializeToString(serializer, data, TransportConfiguration.DefaultMemoryStreamFactory, formatting); /// /// Extension method that serializes an instance of to a string. /// - /// + /// The type of the data to be serialized. + /// + /// The data to serialize. /// /// A factory yielding MemoryStream instances, defaults to /// that yields memory streams backed by pooled byte arrays. /// - /// /// public static string SerializeToString( this Serializer serializer, - T data, + T? data, 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 string. + return JsonSerializer.Serialize(data, stjSerializer.GetJsonSerializerOptions(formatting)); + } + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; using var ms = memoryStreamFactory.Create(); + serializer.Serialize(data, ms, formatting); + + return ms.Utf8String(); + } + + /// + /// Extension method that serializes the given to a string. + /// + /// + /// The data to serialize. + /// The type of the data to serialize. + /// + public static string SerializeToString( + this Serializer serializer, + object? data, + Type type, + SerializationFormatting formatting = SerializationFormatting.None) => + SerializeToString(serializer, data, type, TransportConfiguration.DefaultMemoryStreamFactory, formatting); + + /// + /// Extension method that serializes the given to a string. + /// + /// + /// The data to serialize. + /// The type of the data to serialize. + /// + /// A factory yielding MemoryStream instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// + public static string SerializeToString( + this Serializer serializer, + object? data, + Type type, + 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 string. + return JsonSerializer.Serialize(data, type, stjSerializer.GetJsonSerializerOptions(formatting)); + } + + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using var ms = memoryStreamFactory.Create(); + + serializer.Serialize(data, ms, formatting); + return ms.Utf8String(); } #region STJ Extensions /// - /// Extension method that deserializes from a UTF8 . + /// Extension method that writes the serialized representation of an instance of to a + /// . /// - /// The type of the data to be deserialized. + /// The type of the data to be serialized. /// - /// The source that contains the UTF8 encoded JSON string. + /// The data to serialize. + /// The destination . + /// + public static void Serialize( + this Serializer serializer, + T? data, + Utf8JsonWriter writer, + SerializationFormatting formatting = SerializationFormatting.None + ) => Serialize(serializer, data, writer, TransportConfiguration.DefaultMemoryStreamFactory, formatting); + + /// + /// 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. /// - /// The deserialized data. - public static T? Deserialize( + /// + public static void Serialize( this Serializer serializer, - ReadOnlySpan span, - MemoryStreamFactory? memoryStreamFactory = null) + 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 - // deserialize straight from the span. - return JsonSerializer.Deserialize(span, stjSerializer.GetJsonSerializerOptions()); + // serialize straight into the writer. + JsonSerializer.Serialize(writer, data, stjSerializer.GetJsonSerializerOptions(formatting)); + return; } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; - using var ms = memoryStreamFactory.Create(span.ToArray()); + using var ms = memoryStreamFactory.Create(); - return serializer.Deserialize(ms); + 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 UTF8 . + /// Extension method that writes the serialized representation of the given to a + /// . /// /// - /// The source that contains the UTF8 encoded JSON. - /// The type of the data to be deserialized. + /// The data to serialize. + /// The type of the data to serialize. + /// The destination . + /// + public static void Serialize( + this Serializer serializer, + object? data, + Type type, + Utf8JsonWriter writer, + SerializationFormatting formatting = SerializationFormatting.None + ) => Serialize(serializer, data, type, writer, TransportConfiguration.DefaultMemoryStreamFactory, formatting); + + /// + /// 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. /// - /// The deserialized data. - public static object? Deserialize( + /// + public static void Serialize( this Serializer serializer, - ReadOnlySpan span, + object? data, Type type, - MemoryStreamFactory? memoryStreamFactory = null) + 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 - // deserialize straight from the span. - return JsonSerializer.Deserialize(span, type, stjSerializer.GetJsonSerializerOptions()); + // serialize straight into the writer. + JsonSerializer.Serialize(writer, data, type, stjSerializer.GetJsonSerializerOptions(formatting)); + return; } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; - using var ms = memoryStreamFactory.Create(span.ToArray()); + using var ms = memoryStreamFactory.Create(); - return serializer.Deserialize(type, ms); + 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 } /// @@ -153,7 +318,7 @@ public static string SerializeToString( /// The deserialized data. public static T? Deserialize( this Serializer serializer, - ReadOnlySpan span, + ReadOnlySpan span, MemoryStreamFactory? memoryStreamFactory = null) { if (serializer is SystemTextJsonSerializer stjSerializer) @@ -164,7 +329,7 @@ public static string SerializeToString( } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; - using var ms = memoryStreamFactory.Create(Encoding.UTF8.GetBytes(span.ToArray())); + using var ms = memoryStreamFactory.Create(span.ToArray()); return serializer.Deserialize(ms); } @@ -182,7 +347,7 @@ public static string SerializeToString( /// The deserialized data. public static object? Deserialize( this Serializer serializer, - ReadOnlySpan span, + ReadOnlySpan span, Type type, MemoryStreamFactory? memoryStreamFactory = null) { @@ -194,94 +359,68 @@ public static string SerializeToString( } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; - using var ms = memoryStreamFactory.Create(Encoding.UTF8.GetBytes(span.ToArray())); + using var ms = memoryStreamFactory.Create(span.ToArray()); return serializer.Deserialize(type, ms); } /// - /// Extension method that writes the serialized representation of an instance of to a - /// . + /// Extension method that deserializes from a UTF8 . /// - /// The type of the data to be serialized. + /// The type of the data to be deserialized. /// - /// The data to serialize. - /// The destination . + /// The source that contains the UTF8 encoded JSON string. /// /// A factory yielding instances, defaults to /// that yields memory streams backed by pooled byte arrays. /// - /// - public static void Serialize( + /// The deserialized data. + public static T? Deserialize( this Serializer serializer, - T? data, - Utf8JsonWriter writer, - MemoryStreamFactory? memoryStreamFactory = null, - SerializationFormatting formatting = SerializationFormatting.None) + ReadOnlySpan span, + MemoryStreamFactory? memoryStreamFactory = null) { 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; + // deserialize straight from the span. + return JsonSerializer.Deserialize(span, stjSerializer.GetJsonSerializerOptions()); } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; - using var ms = memoryStreamFactory.Create(); - - serializer.Serialize(data, ms); - ms.Position = 0; + using var ms = memoryStreamFactory.Create(Encoding.UTF8.GetBytes(span.ToArray())); -#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 + return serializer.Deserialize(ms); } /// - /// Extension method that writes the serialized representation of the given to a - /// . + /// Extension method that deserializes from a UTF8 . /// /// - /// The data to serialize. - /// The type of the data to serialize. - /// The destination . + /// The source that contains the UTF8 encoded JSON. + /// The type of the data to be deserialized. /// /// A factory yielding instances, defaults to /// that yields memory streams backed by pooled byte arrays. /// - /// - public static void Serialize( + /// The deserialized data. + public static object? Deserialize( this Serializer serializer, - object? data, + ReadOnlySpan span, Type type, - Utf8JsonWriter writer, - MemoryStreamFactory? memoryStreamFactory = null, - SerializationFormatting formatting = SerializationFormatting.None) + MemoryStreamFactory? memoryStreamFactory = null) { 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; + // deserialize straight from the span. + return JsonSerializer.Deserialize(span, type, stjSerializer.GetJsonSerializerOptions()); } memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; - using var ms = memoryStreamFactory.Create(); - - serializer.Serialize(data, ms); - ms.Position = 0; + using var ms = memoryStreamFactory.Create(Encoding.UTF8.GetBytes(span.ToArray())); -#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 + return serializer.Deserialize(type, ms); } /// From 2871d17d7f95a6f527f2cf6684f0a808f0ed5642 Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Wed, 16 Oct 2024 09:37:22 +0200 Subject: [PATCH 4/7] Simplify code --- .../Serialization/SystemTextJsonSerializer.cs | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs index 7c0e0f3..6d8fe3b 100644 --- a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs +++ b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs @@ -28,65 +28,49 @@ public abstract class SystemTextJsonSerializer : /// public override T Deserialize(Stream stream) { - Initialize(); - if (TryReturnDefault(stream, out T deserialize)) return deserialize; - return JsonSerializer.Deserialize(stream, _options); + return JsonSerializer.Deserialize(stream, GetJsonSerializerOptions()); } /// public override object? Deserialize(Type type, Stream stream) { - Initialize(); - if (TryReturnDefault(stream, out object deserialize)) return deserialize; - return JsonSerializer.Deserialize(stream, type, _options); + return JsonSerializer.Deserialize(stream, type, GetJsonSerializerOptions()); } /// 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); + return JsonSerializer.DeserializeAsync(stream, GetJsonSerializerOptions(), 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); + return JsonSerializer.DeserializeAsync(stream, type, GetJsonSerializerOptions(), cancellationToken); } /// public override void Serialize(T data, Stream writableStream, - SerializationFormatting formatting = SerializationFormatting.None) - { - Initialize(); - + SerializationFormatting formatting = SerializationFormatting.None) => 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); - } + CancellationToken cancellationToken = default) => + JsonSerializer.SerializeAsync(stream, data, GetJsonSerializerOptions(formatting), cancellationToken); #endregion Serializer @@ -110,15 +94,19 @@ protected virtual void Initialized() /// /// The serialization formatting. /// The requested or null, if the serializer is not initialized yet. - protected internal JsonSerializerOptions? GetJsonSerializerOptions(SerializationFormatting formatting = SerializationFormatting.None) => - (formatting is SerializationFormatting.None) + protected internal JsonSerializerOptions? GetJsonSerializerOptions(SerializationFormatting formatting = SerializationFormatting.None) + { + Initialize(); + + return (formatting is SerializationFormatting.None) ? _options : _indentedOptions; + } /// /// Initializes a serializer instance such that its are populated. /// - protected internal void Initialize() + private void Initialize() { // Exit early, if already initialized if (_initialized) From 9fcd85710e3cc6cb279d419782598803afe84c15 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 16 Oct 2024 10:48:01 +0200 Subject: [PATCH 5/7] Introduce providing json options as its own construct --- .../IJsonSerializerOptionsProvider.cs | 47 ++++++++++ .../LowLevelRequestResponseSerializer.cs | 47 ++-------- .../Serialization/SystemTextJsonSerializer.cs | 93 ++++--------------- 3 files changed, 74 insertions(+), 113 deletions(-) create mode 100644 src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs diff --git a/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs b/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs new file mode 100644 index 0000000..eb040a7 --- /dev/null +++ b/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs @@ -0,0 +1,47 @@ +// 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.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Elastic.Transport; + +/// +/// Provides an instance of to +/// +public interface IJsonSerializerOptionsProvider +{ + /// + JsonSerializerOptions CreateJsonSerializerOptions(); +} +/// +/// Default implementation of specialized in providing more converters and +/// altering the shared used by and its derrived classes +/// +public class TransportSerializerOptionsProvider : IJsonSerializerOptionsProvider +{ + private readonly JsonSerializerOptions _options = new(); + + /// + public JsonSerializerOptions? CreateJsonSerializerOptions() => _options; + + /// + public TransportSerializerOptionsProvider() { } + + /// + public TransportSerializerOptionsProvider(IReadOnlyCollection bakedIn, IReadOnlyCollection? userProvided, Action? optionsAction = null) + { + foreach (var converter in bakedIn) + _options.Converters.Add(converter); + + foreach (var converter in userProvided ?? []) + _options.Converters.Add(converter); + + optionsAction?.Invoke(_options); + + } +} + diff --git a/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs b/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs index 5533549..183d4cf 100644 --- a/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs +++ b/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs @@ -3,37 +3,22 @@ // See the LICENSE file in the project root for more information using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; -using Elastic.Transport.Extensions; - namespace Elastic.Transport; /// /// Default low level request/response-serializer implementation for which serializes using /// the Microsoft System.Text.Json library /// -internal sealed class LowLevelRequestResponseSerializer : - SystemTextJsonSerializer +internal sealed class LowLevelRequestResponseSerializer : SystemTextJsonSerializer { /// /// Provides a static reusable reference to an instance of to promote reuse. /// internal static readonly LowLevelRequestResponseSerializer Instance = new(); - private IReadOnlyCollection AdditionalConverters { get; } - - private IList BakedInConverters { get; } = new List - { - new ExceptionConverter(), - new ErrorCauseConverter(), - new ErrorConverter(), - new DynamicDictionaryConverter() - }; - /// > public LowLevelRequestResponseSerializer() : this(null) { } @@ -41,28 +26,12 @@ public LowLevelRequestResponseSerializer() : this(null) { } /// > /// /// Add more default converters onto being used - public LowLevelRequestResponseSerializer(IEnumerable? converters) => - AdditionalConverters = converters != null - ? new ReadOnlyCollection(converters.ToList()) - : EmptyReadOnly.Collection; - - /// - /// Creates used for serialization. - /// Override on a derived serializer to change serialization. - /// - protected override JsonSerializerOptions? CreateJsonSerializerOptions() - { - var options = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - foreach (var converter in BakedInConverters) - options.Converters.Add(converter); - - foreach (var converter in AdditionalConverters) - options.Converters.Add(converter); + public LowLevelRequestResponseSerializer(IReadOnlyCollection? converters) + : base(new TransportSerializerOptionsProvider([ + new ExceptionConverter(), + new ErrorCauseConverter(), + new ErrorConverter(), + new DynamicDictionaryConverter() + ], converters, options => { options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; })) { } - return options; - } } diff --git a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs index 6d8fe3b..6e9a5c0 100644 --- a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs +++ b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs @@ -3,8 +3,10 @@ // See the LICENSE file in the project root for more information using System; +using System.Collections.Generic; using System.IO; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -14,14 +16,23 @@ namespace Elastic.Transport; /// An abstract implementation of a transport which serializes using the Microsoft /// System.Text.Json library. /// -public abstract class SystemTextJsonSerializer : - Serializer +public abstract class SystemTextJsonSerializer : Serializer { - private readonly SemaphoreSlim _semaphore = new(1); + private readonly JsonSerializerOptions? _options; + private readonly JsonSerializerOptions? _indentedOptions; - private bool _initialized; - private JsonSerializerOptions? _options; - private JsonSerializerOptions? _indentedOptions; + /// + /// An abstract implementation of a transport which serializes using the Microsoft + /// System.Text.Json library. + /// + protected SystemTextJsonSerializer(IJsonSerializerOptionsProvider? provider = null) + { + + provider ??= new TransportSerializerOptionsProvider(); + _options = provider.CreateJsonSerializerOptions(); + _indentedOptions = provider.CreateJsonSerializerOptions(); + _indentedOptions.WriteIndented = true; + } #region Serializer @@ -74,80 +85,14 @@ public override Task SerializeAsync(T data, Stream stream, #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 = SerializationFormatting.None) - { - Initialize(); - - return (formatting is SerializationFormatting.None) - ? _options - : _indentedOptions; - } + protected internal JsonSerializerOptions? GetJsonSerializerOptions(SerializationFormatting formatting = SerializationFormatting.None) => + formatting is SerializationFormatting.None ? _options : _indentedOptions; - /// - /// Initializes a serializer instance such that its are populated. - /// - private 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) { From e53e509a3ed9396475d9c87c7966fb0bc9278449 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 16 Oct 2024 10:53:56 +0200 Subject: [PATCH 6/7] CreateJsonSerializerOptions needs to be pure because its called multiple times --- .../IJsonSerializerOptionsProvider.cs | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs b/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs index eb040a7..1660828 100644 --- a/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs +++ b/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs @@ -23,25 +23,38 @@ public interface IJsonSerializerOptionsProvider /// public class TransportSerializerOptionsProvider : IJsonSerializerOptionsProvider { - private readonly JsonSerializerOptions _options = new(); + private readonly IReadOnlyCollection? _bakedInConverters; + private readonly IReadOnlyCollection? _userProvidedConverters; + private readonly Action? _mutateOptions; /// - public JsonSerializerOptions? CreateJsonSerializerOptions() => _options; + public JsonSerializerOptions? CreateJsonSerializerOptions() + { + var options = new JsonSerializerOptions(); + foreach (var converter in _bakedInConverters ?? []) + options.Converters.Add(converter); + + foreach (var converter in _userProvidedConverters ?? []) + options.Converters.Add(converter); + + _mutateOptions?.Invoke(options); + + return options; + } /// public TransportSerializerOptionsProvider() { } /// - public TransportSerializerOptionsProvider(IReadOnlyCollection bakedIn, IReadOnlyCollection? userProvided, Action? optionsAction = null) + public TransportSerializerOptionsProvider( + IReadOnlyCollection bakedInConverters, + IReadOnlyCollection? userProvidedConverters, + Action? mutateOptions = null + ) { - foreach (var converter in bakedIn) - _options.Converters.Add(converter); - - foreach (var converter in userProvided ?? []) - _options.Converters.Add(converter); - - optionsAction?.Invoke(_options); - + _bakedInConverters = bakedInConverters; + _userProvidedConverters = userProvidedConverters; + _mutateOptions = mutateOptions; } } From f56f1a5a2d60c84688a72528e2c7282a6a8f4776 Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Wed, 16 Oct 2024 11:02:57 +0200 Subject: [PATCH 7/7] Only call `CreateJsonSerializerOptions` once --- .../Serialization/IJsonSerializerOptionsProvider.cs | 5 +++-- .../Components/Serialization/SystemTextJsonSerializer.cs | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs b/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs index 1660828..10ec20e 100644 --- a/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs +++ b/src/Elastic.Transport/Components/Serialization/IJsonSerializerOptionsProvider.cs @@ -17,9 +17,10 @@ public interface IJsonSerializerOptionsProvider /// JsonSerializerOptions CreateJsonSerializerOptions(); } + /// /// Default implementation of specialized in providing more converters and -/// altering the shared used by and its derrived classes +/// altering the shared used by and its derived classes /// public class TransportSerializerOptionsProvider : IJsonSerializerOptionsProvider { @@ -31,6 +32,7 @@ public class TransportSerializerOptionsProvider : IJsonSerializerOptionsProvider public JsonSerializerOptions? CreateJsonSerializerOptions() { var options = new JsonSerializerOptions(); + foreach (var converter in _bakedInConverters ?? []) options.Converters.Add(converter); @@ -57,4 +59,3 @@ public TransportSerializerOptionsProvider( _mutateOptions = mutateOptions; } } - diff --git a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs index 6e9a5c0..0a713dd 100644 --- a/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs +++ b/src/Elastic.Transport/Components/Serialization/SystemTextJsonSerializer.cs @@ -27,11 +27,12 @@ public abstract class SystemTextJsonSerializer : Serializer /// protected SystemTextJsonSerializer(IJsonSerializerOptionsProvider? provider = null) { - provider ??= new TransportSerializerOptionsProvider(); _options = provider.CreateJsonSerializerOptions(); - _indentedOptions = provider.CreateJsonSerializerOptions(); - _indentedOptions.WriteIndented = true; + _indentedOptions = new JsonSerializerOptions(_options) + { + WriteIndented = true + }; } #region Serializer @@ -93,7 +94,6 @@ public override Task SerializeAsync(T data, Stream stream, protected internal JsonSerializerOptions? GetJsonSerializerOptions(SerializationFormatting formatting = SerializationFormatting.None) => formatting is SerializationFormatting.None ? _options : _indentedOptions; - private static bool TryReturnDefault(Stream? stream, out T deserialize) { deserialize = default;