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
@@ -0,0 +1,61 @@
// 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;

/// <summary>
/// Provides an instance of <see cref="JsonSerializerOptions"/> to <see cref="SystemTextJsonSerializer"/>
/// </summary>
public interface IJsonSerializerOptionsProvider
{
/// <inheritdoc cref="IJsonSerializerOptionsProvider"/>
JsonSerializerOptions CreateJsonSerializerOptions();
}

/// <summary>
/// Default implementation of <see cref="IJsonSerializerOptionsProvider"/> specialized in providing more converters and
/// altering the shared <see cref="JsonSerializerOptions"/> used by <see cref="SystemTextJsonSerializer"/> and its derived classes
/// </summary>
public class TransportSerializerOptionsProvider : IJsonSerializerOptionsProvider
{
private readonly IReadOnlyCollection<JsonConverter>? _bakedInConverters;
private readonly IReadOnlyCollection<JsonConverter>? _userProvidedConverters;
private readonly Action<JsonSerializerOptions>? _mutateOptions;

/// <inheritdoc cref="IJsonSerializerOptionsProvider"/>
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;
}

/// <inheritdoc cref="TransportSerializerOptionsProvider"/>
public TransportSerializerOptionsProvider() { }

/// <inheritdoc cref="TransportSerializerOptionsProvider"/>
public TransportSerializerOptionsProvider(
IReadOnlyCollection<JsonConverter> bakedInConverters,
IReadOnlyCollection<JsonConverter>? userProvidedConverters,
Action<JsonSerializerOptions>? mutateOptions = null
)
{
_bakedInConverters = bakedInConverters;
_userProvidedConverters = userProvidedConverters;
_mutateOptions = mutateOptions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,138 +2,36 @@
// 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;
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>
{
new ExceptionConverter(),
new ErrorCauseConverter(),
new ErrorConverter(),
new DynamicDictionaryConverter()
};

/// <inheritdoc cref="LowLevelRequestResponseSerializer"/>>
public LowLevelRequestResponseSerializer() : this(null) { }

/// <summary>
/// <inheritdoc cref="LowLevelRequestResponseSerializer"/>>
/// </summary>
/// <param name="converters">Add more default converters onto <see cref="JsonSerializerOptions"/> being used</param>
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)
{
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = formatting == Indented,
};
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);
public LowLevelRequestResponseSerializer(IReadOnlyCollection<JsonConverter>? converters)
: base(new TransportSerializerOptionsProvider([
new ExceptionConverter(),
new ErrorCauseConverter(),
new ErrorConverter(),
new DynamicDictionaryConverter()
], converters, options => { options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; })) { }

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,102 @@
// 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.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
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 JsonSerializerOptions? _options;
private readonly JsonSerializerOptions? _indentedOptions;

/// <summary>
/// An abstract implementation of a transport <see cref="Serializer"/> which serializes using the Microsoft
/// <c>System.Text.Json</c> library.
/// </summary>
protected SystemTextJsonSerializer(IJsonSerializerOptionsProvider? provider = null)
{
provider ??= new TransportSerializerOptionsProvider();
_options = provider.CreateJsonSerializerOptions();
_indentedOptions = new JsonSerializerOptions(_options)
{
WriteIndented = true
};
}

#region Serializer

/// <inheritdoc />
public override T Deserialize<T>(Stream stream)
{
if (TryReturnDefault(stream, out T deserialize))
return deserialize;

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

/// <inheritdoc />
public override object? Deserialize(Type type, Stream stream)
{
if (TryReturnDefault(stream, out object deserialize))
return deserialize;

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

/// <inheritdoc />
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, GetJsonSerializerOptions(), cancellationToken);
}

/// <inheritdoc />
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, GetJsonSerializerOptions(), cancellationToken);
}

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

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

#endregion Serializer

/// <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;

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