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 (#119)

Add `SystemTextJsonSerializer` base class and relevant extensions
methods.

Closes #117

---------

Co-authored-by: Martijn Laarman <[email protected]>
  • Loading branch information
flobernd and Mpdreamz authored Oct 16, 2024
1 parent d2ddb47 commit 7c87cd8
Show file tree
Hide file tree
Showing 5 changed files with 745 additions and 119 deletions.
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;
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

0 comments on commit 7c87cd8

Please sign in to comment.