Skip to content

Commit c2e3907

Browse files
committed
Allow derived implementations of ConnectionSettingsAwareContractResolver (#3599)
This commit allows derived implementations of ConnectionSettingsAwareContractResolver to be specified when creating a derived ConnectionSettingsAwareSerializerBase. This can be useful when wishing to include Type information in the serialized JSON. Add documentation to demonstrate how to use. Closes #3494 (cherry picked from commit f72d9ac)
1 parent c94b49f commit c2e3907

File tree

4 files changed

+249
-22
lines changed

4 files changed

+249
-22
lines changed

docs/client-concepts/high-level/serialization/custom-serialization.asciidoc

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,11 @@ and override the `CreateJsonSerializerSettings` and `ModifyContractResolver` met
155155

156156
[source,csharp]
157157
----
158-
public class MyCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
158+
public class MyFirstCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
159159
{
160-
public MyCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
160+
public MyFirstCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
161161
: base(builtinSerializer, connectionSettings) { }
162162
163-
protected override IEnumerable<JsonConverter> CreateJsonConverters() =>
164-
Enumerable.Empty<JsonConverter>();
165-
166163
protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
167164
new JsonSerializerSettings
168165
{
@@ -197,6 +194,13 @@ public class MyDocument
197194
public string FilePath { get; set; }
198195
199196
public int OwnerId { get; set; }
197+
198+
public IEnumerable<MySubDocument> SubDocuments { get; set; }
199+
}
200+
201+
public class MySubDocument
202+
{
203+
public string Name { get; set; }
200204
}
201205
----
202206

@@ -208,7 +212,7 @@ var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
208212
var connectionSettings = new ConnectionSettings(
209213
pool,
210214
connection: new InMemoryConnection(), <1>
211-
sourceSerializer: (builtin, settings) => new MyCustomJsonNetSerializer(builtin, settings))
215+
sourceSerializer: (builtin, settings) => new MyFirstCustomJsonNetSerializer(builtin, settings))
212216
.DefaultIndex("my-index");
213217
214218
var client = new ElasticClient(connectionSettings);
@@ -237,9 +241,123 @@ it serializes to
237241
"id": 1,
238242
"name": "My first document",
239243
"file_path": null,
240-
"owner_id": 2
244+
"owner_id": 2,
245+
"sub_documents": null
241246
}
242247
----
243248

244249
which adheres to the conventions of our configured `MyCustomJsonNetSerializer` serializer.
245250

251+
==== Serializing Type Information
252+
253+
Here's another example that implements a custom contract resolver. The custom contract resolver
254+
will include the type name within the serialized JSON for the document, which can be useful when
255+
returning covariant document types within a collection.
256+
257+
[source,csharp]
258+
----
259+
public class MySecondCustomContractResolver : ConnectionSettingsAwareContractResolver
260+
{
261+
public MySecondCustomContractResolver(IConnectionSettingsValues connectionSettings) : base(connectionSettings)
262+
{
263+
}
264+
265+
protected override JsonContract CreateContract(Type objectType)
266+
{
267+
var contract = base.CreateContract(objectType);
268+
if (contract is JsonContainerContract containerContract)
269+
{
270+
if (containerContract.ItemTypeNameHandling == null)
271+
containerContract.ItemTypeNameHandling = TypeNameHandling.None;
272+
}
273+
274+
return contract;
275+
}
276+
}
277+
278+
public class MySecondCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
279+
{
280+
public MySecondCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
281+
: base(builtinSerializer, connectionSettings) { }
282+
283+
protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
284+
new JsonSerializerSettings
285+
{
286+
TypeNameHandling = TypeNameHandling.All,
287+
NullValueHandling = NullValueHandling.Ignore,
288+
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
289+
};
290+
291+
protected override ConnectionSettingsAwareContractResolver CreateContractResolver() =>
292+
new MySecondCustomContractResolver(ConnectionSettings); <1>
293+
}
294+
----
295+
<1> override the contract resolver
296+
297+
Now, hooking up this serializer
298+
299+
[source,csharp]
300+
----
301+
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
302+
var connectionSettings = new ConnectionSettings(
303+
pool,
304+
connection: new InMemoryConnection(),
305+
sourceSerializer: (builtin, settings) => new MySecondCustomJsonNetSerializer(builtin, settings))
306+
.DefaultIndex("my-index");
307+
308+
var client = new ElasticClient(connectionSettings);
309+
----
310+
311+
and indexing an instance of our document type
312+
313+
[source,csharp]
314+
----
315+
var document = new MyDocument
316+
{
317+
Id = 1,
318+
Name = "My first document",
319+
OwnerId = 2,
320+
SubDocuments = new []
321+
{
322+
new MySubDocument { Name = "my first sub document" },
323+
new MySubDocument { Name = "my second sub document" },
324+
}
325+
};
326+
327+
var indexResponse = client.IndexDocument(document);
328+
----
329+
330+
serializes to
331+
332+
[source,javascript]
333+
----
334+
{
335+
"$type": "Tests.ClientConcepts.HighLevel.Serialization.GettingStarted+MyDocument, Tests",
336+
"id": 1,
337+
"name": "My first document",
338+
"ownerId": 2,
339+
"subDocuments": [
340+
{
341+
"name": "my first sub document"
342+
},
343+
{
344+
"name": "my second sub document"
345+
}
346+
]
347+
}
348+
----
349+
350+
the type information is serialized for the outer `MyDocument` instance, but not for each
351+
`MySubDocument` instance in the `SubDocuments` collection.
352+
353+
When implementing a custom contract resolver derived from `ConnectionSettingsAwareContractResolver`,
354+
be careful not to change the behaviour of the resolver for NEST types; doing so will result in
355+
unexpected behaviour.
356+
357+
[WARNING]
358+
--
359+
Per the https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_TypeNameHandling.htm[Json.NET documentation on TypeNameHandling],
360+
it should be used with caution when your application deserializes JSON from an external source.
361+
362+
--
363+

src/CodeGeneration/DocGenerator/SyntaxNodeExtensions.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,9 @@ public static bool TryGetJsonForSyntaxNode(this SyntaxNode node, out string json
7171
{
7272
json = null;
7373

74-
// find the first anonymous object expression
74+
// find the first anonymous object or new object expression
7575
var creationExpressionSyntax = node.DescendantNodes()
76-
.OfType<AnonymousObjectCreationExpressionSyntax>()
77-
.FirstOrDefault();
76+
.FirstOrDefault(n => n is AnonymousObjectCreationExpressionSyntax || n is ObjectCreationExpressionSyntax);
7877

7978
return creationExpressionSyntax != null &&
8079
creationExpressionSyntax.ToFullString().TryGetJsonForAnonymousType(out json);

src/Serializers/Nest.JsonNetSerializer/ConnectionSettingsAwareSerializerBase.Serializer.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,20 @@ IEnumerable<JsonConverter> contractJsonConverters
4646

4747
private List<JsonConverter> Converters { get; }
4848

49-
50-
private Newtonsoft.Json.JsonSerializer CreateSerializer(SerializationFormatting formatting)
49+
private JsonSerializer CreateSerializer(SerializationFormatting formatting)
5150
{
5251
var s = CreateJsonSerializerSettings() ?? new JsonSerializerSettings();
53-
;
5452
var converters = CreateJsonConverters() ?? Enumerable.Empty<JsonConverter>();
5553
var contract = CreateContractResolver();
5654
s.Formatting = formatting == SerializationFormatting.Indented ? Formatting.Indented : Formatting.None;
5755
s.ContractResolver = contract;
5856
foreach (var converter in converters.Concat(Converters))
5957
s.Converters.Add(converter);
6058

61-
return Newtonsoft.Json.JsonSerializer.Create(s);
59+
return JsonSerializer.Create(s);
6260
}
6361

64-
private IContractResolver CreateContractResolver()
62+
protected virtual ConnectionSettingsAwareContractResolver CreateContractResolver()
6563
{
6664
var contract = new ConnectionSettingsAwareContractResolver(ConnectionSettings);
6765
ModifyContractResolver(contract);

src/Tests/Tests/ClientConcepts/HighLevel/Serialization/CustomSerialization.doc.cs

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Nest;
1111
using Nest.JsonNetSerializer;
1212
using System.Runtime.Serialization;
13+
using Newtonsoft.Json.Linq;
1314
using Newtonsoft.Json.Serialization;
1415
using Newtonsoft.Json;
1516
using Tests.Framework;
@@ -160,14 +161,11 @@ public void DefaultJsonNetSerializerFactoryMethods()
160161
* If you'd like to be more explicit, you can also derive from `ConnectionSettingsAwareSerializerBase`
161162
* and override the `CreateJsonSerializerSettings` and `ModifyContractResolver` methods
162163
*/
163-
public class MyCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
164+
public class MyFirstCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
164165
{
165-
public MyCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
166+
public MyFirstCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
166167
: base(builtinSerializer, connectionSettings) { }
167168

168-
protected override IEnumerable<JsonConverter> CreateJsonConverters() =>
169-
Enumerable.Empty<JsonConverter>();
170-
171169
protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
172170
new JsonSerializerSettings
173171
{
@@ -198,6 +196,13 @@ public class MyDocument
198196
public string FilePath { get; set; }
199197

200198
public int OwnerId { get; set; }
199+
200+
public IEnumerable<MySubDocument> SubDocuments { get; set; }
201+
}
202+
203+
public class MySubDocument
204+
{
205+
public string Name { get; set; }
201206
}
202207

203208
/**
@@ -209,7 +214,7 @@ [U] public void UsingJsonNetSerializer()
209214
var connectionSettings = new ConnectionSettings(
210215
pool,
211216
connection: new InMemoryConnection(), // <1> an _in-memory_ connection is used here for example purposes. In your production application, you would use an `IConnection` implementation that actually sends a request.
212-
sourceSerializer: (builtin, settings) => new MyCustomJsonNetSerializer(builtin, settings))
217+
sourceSerializer: (builtin, settings) => new MyFirstCustomJsonNetSerializer(builtin, settings))
213218
.DefaultIndex("my-index");
214219

215220
//hide
@@ -234,7 +239,8 @@ [U] public void UsingJsonNetSerializer()
234239
id = 1,
235240
name = "My first document",
236241
file_path = (string) null,
237-
owner_id = 2
242+
owner_id = 2,
243+
sub_documents = (object) null
238244
};
239245
/**
240246
* which adheres to the conventions of our configured `MyCustomJsonNetSerializer` serializer.
@@ -243,5 +249,111 @@ [U] public void UsingJsonNetSerializer()
243249
// hide
244250
Expect(expected, preserveNullInExpected: true).FromRequest(indexResponse);
245251
}
252+
253+
/** ==== Serializing Type Information
254+
* Here's another example that implements a custom contract resolver. The custom contract resolver
255+
* will include the type name within the serialized JSON for the document, which can be useful when
256+
* returning covariant document types within a collection.
257+
*/
258+
public class MySecondCustomContractResolver : ConnectionSettingsAwareContractResolver
259+
{
260+
public MySecondCustomContractResolver(IConnectionSettingsValues connectionSettings)
261+
: base(connectionSettings) { }
262+
263+
protected override JsonContract CreateContract(Type objectType)
264+
{
265+
var contract = base.CreateContract(objectType);
266+
if (contract is JsonContainerContract containerContract)
267+
{
268+
if (containerContract.ItemTypeNameHandling == null)
269+
containerContract.ItemTypeNameHandling = TypeNameHandling.None;
270+
}
271+
272+
return contract;
273+
}
274+
}
275+
276+
public class MySecondCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
277+
{
278+
public MySecondCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
279+
: base(builtinSerializer, connectionSettings) { }
280+
281+
protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
282+
new JsonSerializerSettings
283+
{
284+
TypeNameHandling = TypeNameHandling.All,
285+
NullValueHandling = NullValueHandling.Ignore,
286+
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
287+
};
288+
289+
protected override ConnectionSettingsAwareContractResolver CreateContractResolver() =>
290+
new MySecondCustomContractResolver(ConnectionSettings); // <1> override the contract resolver
291+
}
292+
293+
/**
294+
* Now, hooking up this serializer
295+
*/
296+
[U] public void MySecondJsonNetSerializer()
297+
{
298+
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
299+
var connectionSettings = new ConnectionSettings(
300+
pool,
301+
connection: new InMemoryConnection(),
302+
sourceSerializer: (builtin, settings) => new MySecondCustomJsonNetSerializer(builtin, settings))
303+
.DefaultIndex("my-index");
304+
305+
//hide
306+
connectionSettings.DisableDirectStreaming();
307+
308+
var client = new ElasticClient(connectionSettings);
309+
310+
/** and indexing an instance of our document type */
311+
var document = new MyDocument
312+
{
313+
Id = 1,
314+
Name = "My first document",
315+
OwnerId = 2,
316+
SubDocuments = new []
317+
{
318+
new MySubDocument { Name = "my first sub document" },
319+
new MySubDocument { Name = "my second sub document" },
320+
}
321+
};
322+
323+
var indexResponse = client.IndexDocument(document);
324+
325+
/** serializes to */
326+
//json
327+
var expected = new JObject
328+
{
329+
{ "$type", "Tests.ClientConcepts.HighLevel.Serialization.GettingStarted+MyDocument, Tests" },
330+
{ "id", 1 },
331+
{ "name", "My first document" },
332+
{ "ownerId", 2 },
333+
{ "subDocuments", new JArray
334+
{
335+
new JObject { { "name", "my first sub document" } },
336+
new JObject { { "name", "my second sub document" } },
337+
}
338+
}
339+
};
340+
/**
341+
* the type information is serialized for the outer `MyDocument` instance, but not for each
342+
* `MySubDocument` instance in the `SubDocuments` collection.
343+
*
344+
* When implementing a custom contract resolver derived from `ConnectionSettingsAwareContractResolver`,
345+
* be careful not to change the behaviour of the resolver for NEST types; doing so will result in
346+
* unexpected behaviour.
347+
*
348+
* [WARNING]
349+
* --
350+
* Per the https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_TypeNameHandling.htm[Json.NET documentation on TypeNameHandling],
351+
* it should be used with caution when your application deserializes JSON from an external source.
352+
* --
353+
*/
354+
355+
// hide
356+
Expect(expected).FromRequest(indexResponse);
357+
}
246358
}
247359
}

0 commit comments

Comments
 (0)