Skip to content

Commit 7c87cd8

Browse files
floberndMpdreamz
andauthored
Add SystemTextJsonSerializer base class and relevant extensions methods (#119)
Add `SystemTextJsonSerializer` base class and relevant extensions methods. Closes #117 --------- Co-authored-by: Martijn Laarman <[email protected]>
1 parent d2ddb47 commit 7c87cd8

File tree

5 files changed

+745
-119
lines changed

5 files changed

+745
-119
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
10+
namespace Elastic.Transport;
11+
12+
/// <summary>
13+
/// Provides an instance of <see cref="JsonSerializerOptions"/> to <see cref="SystemTextJsonSerializer"/>
14+
/// </summary>
15+
public interface IJsonSerializerOptionsProvider
16+
{
17+
/// <inheritdoc cref="IJsonSerializerOptionsProvider"/>
18+
JsonSerializerOptions CreateJsonSerializerOptions();
19+
}
20+
21+
/// <summary>
22+
/// Default implementation of <see cref="IJsonSerializerOptionsProvider"/> specialized in providing more converters and
23+
/// altering the shared <see cref="JsonSerializerOptions"/> used by <see cref="SystemTextJsonSerializer"/> and its derived classes
24+
/// </summary>
25+
public class TransportSerializerOptionsProvider : IJsonSerializerOptionsProvider
26+
{
27+
private readonly IReadOnlyCollection<JsonConverter>? _bakedInConverters;
28+
private readonly IReadOnlyCollection<JsonConverter>? _userProvidedConverters;
29+
private readonly Action<JsonSerializerOptions>? _mutateOptions;
30+
31+
/// <inheritdoc cref="IJsonSerializerOptionsProvider"/>
32+
public JsonSerializerOptions? CreateJsonSerializerOptions()
33+
{
34+
var options = new JsonSerializerOptions();
35+
36+
foreach (var converter in _bakedInConverters ?? [])
37+
options.Converters.Add(converter);
38+
39+
foreach (var converter in _userProvidedConverters ?? [])
40+
options.Converters.Add(converter);
41+
42+
_mutateOptions?.Invoke(options);
43+
44+
return options;
45+
}
46+
47+
/// <inheritdoc cref="TransportSerializerOptionsProvider"/>
48+
public TransportSerializerOptionsProvider() { }
49+
50+
/// <inheritdoc cref="TransportSerializerOptionsProvider"/>
51+
public TransportSerializerOptionsProvider(
52+
IReadOnlyCollection<JsonConverter> bakedInConverters,
53+
IReadOnlyCollection<JsonConverter>? userProvidedConverters,
54+
Action<JsonSerializerOptions>? mutateOptions = null
55+
)
56+
{
57+
_bakedInConverters = bakedInConverters;
58+
_userProvidedConverters = userProvidedConverters;
59+
_mutateOptions = mutateOptions;
60+
}
61+
}

src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs

Lines changed: 10 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -2,138 +2,36 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5-
using System;
65
using System.Collections.Generic;
7-
using System.Collections.ObjectModel;
8-
using System.IO;
9-
using System.Linq;
106
using System.Text.Json;
117
using System.Text.Json.Serialization;
12-
using System.Threading;
13-
using System.Threading.Tasks;
14-
using Elastic.Transport.Extensions;
15-
using static Elastic.Transport.SerializationFormatting;
168

179
namespace Elastic.Transport;
1810

1911
/// <summary>
20-
/// Default implementation for <see cref="Serializer"/>. This uses <see cref="JsonSerializer"/> from <code>System.Text.Json</code>.
12+
/// Default low level request/response-serializer implementation for <see cref="Serializer"/> which serializes using
13+
/// the Microsoft <c>System.Text.Json</c> library
2114
/// </summary>
22-
internal sealed class LowLevelRequestResponseSerializer : Serializer
15+
internal sealed class LowLevelRequestResponseSerializer : SystemTextJsonSerializer
2316
{
2417
/// <summary>
2518
/// Provides a static reusable reference to an instance of <see cref="LowLevelRequestResponseSerializer"/> to promote reuse.
2619
/// </summary>
2720
internal static readonly LowLevelRequestResponseSerializer Instance = new();
2821

29-
private readonly Lazy<JsonSerializerOptions> _indented;
30-
private readonly Lazy<JsonSerializerOptions> _none;
31-
32-
private IReadOnlyCollection<JsonConverter> AdditionalConverters { get; }
33-
34-
private IList<JsonConverter> BakedInConverters { get; } = new List<JsonConverter>
35-
{
36-
new ExceptionConverter(),
37-
new ErrorCauseConverter(),
38-
new ErrorConverter(),
39-
new DynamicDictionaryConverter()
40-
};
41-
4222
/// <inheritdoc cref="LowLevelRequestResponseSerializer"/>>
4323
public LowLevelRequestResponseSerializer() : this(null) { }
4424

4525
/// <summary>
4626
/// <inheritdoc cref="LowLevelRequestResponseSerializer"/>>
4727
/// </summary>
4828
/// <param name="converters">Add more default converters onto <see cref="JsonSerializerOptions"/> being used</param>
49-
public LowLevelRequestResponseSerializer(IEnumerable<JsonConverter>? converters)
50-
{
51-
AdditionalConverters = converters != null
52-
? new ReadOnlyCollection<JsonConverter>(converters.ToList())
53-
: EmptyReadOnly<JsonConverter>.Collection;
54-
_indented = new Lazy<JsonSerializerOptions>(() => CreateSerializerOptions(Indented));
55-
_none = new Lazy<JsonSerializerOptions>(() => CreateSerializerOptions(None));
56-
}
57-
58-
/// <summary>
59-
/// Creates <see cref="JsonSerializerOptions"/> used for serialization.
60-
/// Override on a derived serializer to change serialization.
61-
/// </summary>
62-
public JsonSerializerOptions CreateSerializerOptions(SerializationFormatting formatting)
63-
{
64-
var options = new JsonSerializerOptions
65-
{
66-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
67-
WriteIndented = formatting == Indented,
68-
};
69-
foreach (var converter in BakedInConverters)
70-
options.Converters.Add(converter);
71-
foreach (var converter in AdditionalConverters)
72-
options.Converters.Add(converter);
73-
74-
return options;
75-
76-
}
77-
78-
private static bool TryReturnDefault<T>(Stream? stream, out T deserialize)
79-
{
80-
deserialize = default;
81-
return stream == null || stream == Stream.Null || (stream.CanSeek && stream.Length == 0);
82-
}
83-
84-
private JsonSerializerOptions GetFormatting(SerializationFormatting formatting) => formatting == None ? _none.Value : _indented.Value;
85-
86-
/// <inheritdoc cref="Serializer.Deserialize"/>>
87-
public override object Deserialize(Type type, Stream stream)
88-
{
89-
if (TryReturnDefault(stream, out object deserialize)) return deserialize;
90-
91-
return JsonSerializer.Deserialize(stream, type, _none.Value)!;
92-
}
93-
94-
/// <inheritdoc cref="Serializer.Deserialize{T}"/>>
95-
public override T Deserialize<T>(Stream stream)
96-
{
97-
if (TryReturnDefault(stream, out T deserialize)) return deserialize;
98-
99-
return JsonSerializer.Deserialize<T>(stream, _none.Value);
100-
}
101-
102-
/// <inheritdoc cref="Serializer.Serialize{T}"/>>
103-
public override void Serialize<T>(T data, Stream stream, SerializationFormatting formatting = None)
104-
{
105-
using var writer = new Utf8JsonWriter(stream);
106-
if (data == null)
107-
JsonSerializer.Serialize(writer, null, typeof(object), GetFormatting(formatting));
108-
//TODO validate if we can avoid boxing by checking if data is typeof(object)
109-
else
110-
JsonSerializer.Serialize(writer, data, data.GetType(), GetFormatting(formatting));
111-
}
112-
113-
/// <inheritdoc cref="Serializer.SerializeAsync{T}"/>>
114-
public override async Task SerializeAsync<T>(T data, Stream stream, SerializationFormatting formatting = None,
115-
CancellationToken cancellationToken = default
116-
)
117-
{
118-
if (data == null)
119-
await JsonSerializer.SerializeAsync(stream, null, typeof(object), GetFormatting(formatting), cancellationToken).ConfigureAwait(false);
120-
else
121-
await JsonSerializer.SerializeAsync(stream, data, data.GetType(), GetFormatting(formatting), cancellationToken).ConfigureAwait(false);
122-
}
123-
124-
/// <inheritdoc cref="Serializer.DeserializeAsync"/>>
125-
public override ValueTask<object> DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default)
126-
{
127-
if (TryReturnDefault(stream, out object deserialize)) return new ValueTask<object>(deserialize);
128-
129-
return JsonSerializer.DeserializeAsync(stream, type, _none.Value, cancellationToken);
130-
}
131-
132-
/// <inheritdoc cref="Serializer.DeserializeAsync{T}"/>>
133-
public override ValueTask<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
134-
{
135-
if (TryReturnDefault(stream, out T deserialize)) return new ValueTask<T>(deserialize);
29+
public LowLevelRequestResponseSerializer(IReadOnlyCollection<JsonConverter>? converters)
30+
: base(new TransportSerializerOptionsProvider([
31+
new ExceptionConverter(),
32+
new ErrorCauseConverter(),
33+
new ErrorConverter(),
34+
new DynamicDictionaryConverter()
35+
], converters, options => { options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; })) { }
13636

137-
return JsonSerializer.DeserializeAsync<T>(stream, _none.Value, cancellationToken);
138-
}
13937
}

src/Elastic.Transport/Components/Serialization/Serializer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public abstract class Serializer
3232
/// <inheritdoc cref="Deserialize"/>
3333
public abstract ValueTask<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default);
3434

35+
// TODO: Overloads for (object?, Type) inputs
36+
3537
/// <summary>
3638
/// Serialize an instance of <typeparamref name="T"/> to <paramref name="stream"/> using <paramref name="formatting"/>.
3739
/// </summary>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
13+
namespace Elastic.Transport;
14+
15+
/// <summary>
16+
/// An abstract implementation of a transport <see cref="Serializer"/> which serializes using the Microsoft
17+
/// <c>System.Text.Json</c> library.
18+
/// </summary>
19+
public abstract class SystemTextJsonSerializer : Serializer
20+
{
21+
private readonly JsonSerializerOptions? _options;
22+
private readonly JsonSerializerOptions? _indentedOptions;
23+
24+
/// <summary>
25+
/// An abstract implementation of a transport <see cref="Serializer"/> which serializes using the Microsoft
26+
/// <c>System.Text.Json</c> library.
27+
/// </summary>
28+
protected SystemTextJsonSerializer(IJsonSerializerOptionsProvider? provider = null)
29+
{
30+
provider ??= new TransportSerializerOptionsProvider();
31+
_options = provider.CreateJsonSerializerOptions();
32+
_indentedOptions = new JsonSerializerOptions(_options)
33+
{
34+
WriteIndented = true
35+
};
36+
}
37+
38+
#region Serializer
39+
40+
/// <inheritdoc />
41+
public override T Deserialize<T>(Stream stream)
42+
{
43+
if (TryReturnDefault(stream, out T deserialize))
44+
return deserialize;
45+
46+
return JsonSerializer.Deserialize<T>(stream, GetJsonSerializerOptions());
47+
}
48+
49+
/// <inheritdoc />
50+
public override object? Deserialize(Type type, Stream stream)
51+
{
52+
if (TryReturnDefault(stream, out object deserialize))
53+
return deserialize;
54+
55+
return JsonSerializer.Deserialize(stream, type, GetJsonSerializerOptions());
56+
}
57+
58+
/// <inheritdoc />
59+
public override ValueTask<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
60+
{
61+
if (TryReturnDefault(stream, out T deserialize))
62+
return new ValueTask<T>(deserialize);
63+
64+
return JsonSerializer.DeserializeAsync<T>(stream, GetJsonSerializerOptions(), cancellationToken);
65+
}
66+
67+
/// <inheritdoc />
68+
public override ValueTask<object?> DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default)
69+
{
70+
if (TryReturnDefault(stream, out object deserialize))
71+
return new ValueTask<object?>(deserialize);
72+
73+
return JsonSerializer.DeserializeAsync(stream, type, GetJsonSerializerOptions(), cancellationToken);
74+
}
75+
76+
/// <inheritdoc />
77+
public override void Serialize<T>(T data, Stream writableStream,
78+
SerializationFormatting formatting = SerializationFormatting.None) =>
79+
JsonSerializer.Serialize(writableStream, data, GetJsonSerializerOptions(formatting));
80+
81+
/// <inheritdoc />
82+
public override Task SerializeAsync<T>(T data, Stream stream,
83+
SerializationFormatting formatting = SerializationFormatting.None,
84+
CancellationToken cancellationToken = default) =>
85+
JsonSerializer.SerializeAsync(stream, data, GetJsonSerializerOptions(formatting), cancellationToken);
86+
87+
#endregion Serializer
88+
89+
/// <summary>
90+
/// Returns the <see cref="JsonSerializerOptions"/> for this serializer, based on the given <paramref name="formatting"/>.
91+
/// </summary>
92+
/// <param name="formatting">The serialization formatting.</param>
93+
/// <returns>The requested <see cref="JsonSerializerOptions"/> or <c>null</c>, if the serializer is not initialized yet.</returns>
94+
protected internal JsonSerializerOptions? GetJsonSerializerOptions(SerializationFormatting formatting = SerializationFormatting.None) =>
95+
formatting is SerializationFormatting.None ? _options : _indentedOptions;
96+
97+
private static bool TryReturnDefault<T>(Stream? stream, out T deserialize)
98+
{
99+
deserialize = default;
100+
return (stream is null) || stream == Stream.Null || (stream.CanSeek && stream.Length == 0);
101+
}
102+
}

0 commit comments

Comments
 (0)