Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SystemTextJsonSerializer base class and relevant extensions methods #119

Merged
merged 7 commits into from
Oct 16, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Default implementation for <see cref="Serializer"/>. This uses <see cref="JsonSerializer"/> from <code>System.Text.Json</code>.
/// Default low level request/response-serializer implementation for <see cref="Serializer"/> which serializes using
/// the Microsoft <c>System.Text.Json</c> library
/// </summary>
internal sealed class LowLevelRequestResponseSerializer : Serializer
internal sealed class LowLevelRequestResponseSerializer :
SystemTextJsonSerializer
flobernd marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Provides a static reusable reference to an instance of <see cref="LowLevelRequestResponseSerializer"/> to promote reuse.
/// </summary>
internal static readonly LowLevelRequestResponseSerializer Instance = new();

private readonly Lazy<JsonSerializerOptions> _indented;
flobernd marked this conversation as resolved.
Show resolved Hide resolved
private readonly Lazy<JsonSerializerOptions> _none;

private IReadOnlyCollection<JsonConverter> AdditionalConverters { get; }

private IList<JsonConverter> BakedInConverters { get; } = new List<JsonConverter>
Expand All @@ -46,94 +41,28 @@ public LowLevelRequestResponseSerializer() : this(null) { }
/// <inheritdoc cref="LowLevelRequestResponseSerializer"/>>
/// </summary>
/// <param name="converters">Add more default converters onto <see cref="JsonSerializerOptions"/> being used</param>
public LowLevelRequestResponseSerializer(IEnumerable<JsonConverter>? converters)
{
public LowLevelRequestResponseSerializer(IEnumerable<JsonConverter>? converters) =>
AdditionalConverters = converters != null
? new ReadOnlyCollection<JsonConverter>(converters.ToList())
: EmptyReadOnly<JsonConverter>.Collection;
_indented = new Lazy<JsonSerializerOptions>(() => CreateSerializerOptions(Indented));
_none = new Lazy<JsonSerializerOptions>(() => CreateSerializerOptions(None));
}

/// <summary>
/// Creates <see cref="JsonSerializerOptions"/> used for serialization.
/// Override on a derived serializer to change serialization.
/// </summary>
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<T>(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;

/// <inheritdoc cref="Serializer.Deserialize"/>>
public override object Deserialize(Type type, Stream stream)
{
if (TryReturnDefault(stream, out object deserialize)) return deserialize;

return JsonSerializer.Deserialize(stream, type, _none.Value)!;
}

/// <inheritdoc cref="Serializer.Deserialize{T}"/>>
public override T Deserialize<T>(Stream stream)
{
if (TryReturnDefault(stream, out T deserialize)) return deserialize;

return JsonSerializer.Deserialize<T>(stream, _none.Value);
}

/// <inheritdoc cref="Serializer.Serialize{T}"/>>
public override void Serialize<T>(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));
}

/// <inheritdoc cref="Serializer.SerializeAsync{T}"/>>
public override async Task SerializeAsync<T>(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);
}

/// <inheritdoc cref="Serializer.DeserializeAsync"/>>
public override ValueTask<object> DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default)
{
if (TryReturnDefault(stream, out object deserialize)) return new ValueTask<object>(deserialize);

return JsonSerializer.DeserializeAsync(stream, type, _none.Value, cancellationToken);
}

/// <inheritdoc cref="Serializer.DeserializeAsync{T}"/>>
public override ValueTask<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
{
if (TryReturnDefault(stream, out T deserialize)) return new ValueTask<T>(deserialize);

return JsonSerializer.DeserializeAsync<T>(stream, _none.Value, cancellationToken);
}
}
2 changes: 2 additions & 0 deletions src/Elastic.Transport/Components/Serialization/Serializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public abstract class Serializer
/// <inheritdoc cref="Deserialize"/>
public abstract ValueTask<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default);

// TODO: Overloads for (object?, Type) inputs

/// <summary>
/// Serialize an instance of <typeparamref name="T"/> to <paramref name="stream"/> using <paramref name="formatting"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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;

/// <summary>
/// An abstract implementation of a transport <see cref="Serializer"/> which serializes using the Microsoft
/// <c>System.Text.Json</c> library.
/// </summary>
public abstract class SystemTextJsonSerializer :
Serializer
{
private readonly SemaphoreSlim _semaphore = new(1);

private bool _initialized;
private JsonSerializerOptions? _options;
private JsonSerializerOptions? _indentedOptions;

#region Serializer

/// <inheritdoc />
public override T Deserialize<T>(Stream stream)
{
Initialize();

if (TryReturnDefault(stream, out T deserialize))
return deserialize;

return JsonSerializer.Deserialize<T>(stream, _options);
}

/// <inheritdoc />
public override object? Deserialize(Type type, Stream stream)
{
Initialize();

if (TryReturnDefault(stream, out object deserialize))
return deserialize;

return JsonSerializer.Deserialize(stream, type, _options);
}

/// <inheritdoc />
public override ValueTask<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
{
Initialize();

if (TryReturnDefault(stream, out T deserialize))
return new ValueTask<T>(deserialize);

return JsonSerializer.DeserializeAsync<T>(stream, _options, cancellationToken);
}

/// <inheritdoc />
public override ValueTask<object?> DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default)
{
Initialize();

if (TryReturnDefault(stream, out object deserialize))
return new ValueTask<object?>(deserialize);

return JsonSerializer.DeserializeAsync(stream, type, _options, cancellationToken);
}

/// <inheritdoc />
public override void Serialize<T>(T data, Stream writableStream,
SerializationFormatting formatting = SerializationFormatting.None)
{
Initialize();

JsonSerializer.Serialize(writableStream, data, GetJsonSerializerOptions(formatting));
}

/// <inheritdoc />
public override Task SerializeAsync<T>(T data, Stream stream,
SerializationFormatting formatting = SerializationFormatting.None,
CancellationToken cancellationToken = default)
{
Initialize();

return JsonSerializer.SerializeAsync(stream, data, GetJsonSerializerOptions(formatting), cancellationToken);
}

#endregion Serializer

/// <summary>
/// A factory method that can create an instance of <see cref="JsonSerializerOptions"/> that will
/// be used when serializing.
/// </summary>
/// <returns></returns>
protected abstract JsonSerializerOptions? CreateJsonSerializerOptions();

/// <summary>
/// A callback function that is invoked after the <see cref="JsonSerializerOptions"/> have been created and the
/// serializer got fully initialized.
/// </summary>
protected virtual void Initialized()
flobernd marked this conversation as resolved.
Show resolved Hide resolved
{
}

/// <summary>
/// Returns the <see cref="JsonSerializerOptions"/> for this serializer, based on the given <paramref name="formatting"/>.
/// </summary>
/// <param name="formatting">The serialization formatting.</param>
/// <returns>The requested <see cref="JsonSerializerOptions"/> or <c>null</c>, if the serializer is not initialized yet.</returns>
protected internal JsonSerializerOptions? GetJsonSerializerOptions(SerializationFormatting formatting = SerializationFormatting.None) =>
(formatting is SerializationFormatting.None)
? _options
: _indentedOptions;

/// <summary>
/// Initializes a serializer instance such that its <see cref="JsonSerializerOptions"/> are populated.
/// </summary>
protected internal void Initialize()
flobernd marked this conversation as resolved.
Show resolved Hide resolved
{
// 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<T>(Stream? stream, out T deserialize)
{
deserialize = default;
return (stream is null) || stream == Stream.Null || (stream.CanSeek && stream.Length == 0);
}
}
Loading
Loading