Skip to content

Commit

Permalink
Add SystemTextJsonSerializer base class and relevant extensions met…
Browse files Browse the repository at this point in the history
…hods
  • Loading branch information
flobernd committed Oct 11, 2024
1 parent b65498c commit 43f7bb2
Show file tree
Hide file tree
Showing 3 changed files with 483 additions and 81 deletions.
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
{
/// <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;
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <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()
{
}

/// <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) => (formatting is SerializationFormatting.None)
? _options
: _indentedOptions;

/// <summary>
/// Initializes a serializer instance such that its <see cref="JsonSerializerOptions"/> are populated.
/// </summary>
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<T>(Stream? stream, out T deserialize)
{
deserialize = default;
return (stream is null) || stream == Stream.Null || (stream.CanSeek && stream.Length == 0);
}
}
Loading

0 comments on commit 43f7bb2

Please sign in to comment.