Skip to content

Commit f72d9ac

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
1 parent 5a78a43 commit f72d9ac

File tree

4 files changed

+249
-20
lines changed

4 files changed

+249
-20
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: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,9 @@ IEnumerable<JsonConverter> contractJsonConverters
4545

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

48-
4948
private JsonSerializer CreateSerializer(SerializationFormatting formatting)
5049
{
5150
var s = CreateJsonSerializerSettings() ?? new JsonSerializerSettings();
52-
;
5351
var converters = CreateJsonConverters() ?? Enumerable.Empty<JsonConverter>();
5452
var contract = CreateContractResolver();
5553
s.Formatting = formatting == SerializationFormatting.Indented ? Formatting.Indented : Formatting.None;
@@ -60,7 +58,7 @@ private JsonSerializer CreateSerializer(SerializationFormatting formatting)
6058
return JsonSerializer.Create(s);
6159
}
6260

63-
private IContractResolver CreateContractResolver()
61+
protected virtual ConnectionSettingsAwareContractResolver CreateContractResolver()
6462
{
6563
var contract = new ConnectionSettingsAwareContractResolver(ConnectionSettings);
6664
ModifyContractResolver(contract);

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

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

167-
protected override IEnumerable<JsonConverter> CreateJsonConverters() =>
168-
Enumerable.Empty<JsonConverter>();
169-
170168
protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
171169
new JsonSerializerSettings
172170
{
@@ -176,6 +174,7 @@ protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
176174
protected override void ModifyContractResolver(ConnectionSettingsAwareContractResolver resolver) =>
177175
resolver.NamingStrategy = new SnakeCaseNamingStrategy();
178176
}
177+
179178
/**
180179
* Using `MyCustomJsonNetSerializer`, we can serialize using
181180
*
@@ -197,6 +196,13 @@ public class MyDocument
197196
public string FilePath { get; set; }
198197

199198
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; }
200206
}
201207

202208
/**
@@ -208,7 +214,7 @@ [U] public void UsingJsonNetSerializer()
208214
var connectionSettings = new ConnectionSettings(
209215
pool,
210216
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.
211-
sourceSerializer: (builtin, settings) => new MyCustomJsonNetSerializer(builtin, settings))
217+
sourceSerializer: (builtin, settings) => new MyFirstCustomJsonNetSerializer(builtin, settings))
212218
.DefaultIndex("my-index");
213219

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

0 commit comments

Comments
 (0)