Skip to content

Commit

Permalink
Allow derived implementations of ConnectionSettingsAwareContractResol…
Browse files Browse the repository at this point in the history
…ver (#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
  • Loading branch information
russcam committed Mar 20, 2019
1 parent 096fd87 commit 3d244b9
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,11 @@ and override the `CreateJsonSerializerSettings` and `ModifyContractResolver` met

[source,csharp]
----
public class MyCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
public class MyFirstCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
{
public MyCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
public MyFirstCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
: base(builtinSerializer, connectionSettings) { }
protected override IEnumerable<JsonConverter> CreateJsonConverters() =>
Enumerable.Empty<JsonConverter>();
protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
new JsonSerializerSettings
{
Expand Down Expand Up @@ -197,6 +194,13 @@ public class MyDocument
public string FilePath { get; set; }
public int OwnerId { get; set; }
public IEnumerable<MySubDocument> SubDocuments { get; set; }
}
public class MySubDocument
{
public string Name { get; set; }
}
----

Expand All @@ -208,7 +212,7 @@ var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var connectionSettings = new ConnectionSettings(
pool,
connection: new InMemoryConnection(), <1>
sourceSerializer: (builtin, settings) => new MyCustomJsonNetSerializer(builtin, settings))
sourceSerializer: (builtin, settings) => new MyFirstCustomJsonNetSerializer(builtin, settings))
.DefaultIndex("my-index");
var client = new ElasticClient(connectionSettings);
Expand Down Expand Up @@ -237,9 +241,123 @@ it serializes to
"id": 1,
"name": "My first document",
"file_path": null,
"owner_id": 2
"owner_id": 2,
"sub_documents": null
}
----

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

==== Serializing Type Information

Here's another example that implements a custom contract resolver. The custom contract resolver
will include the type name within the serialized JSON for the document, which can be useful when
returning covariant document types within a collection.

[source,csharp]
----
public class MySecondCustomContractResolver : ConnectionSettingsAwareContractResolver
{
public MySecondCustomContractResolver(IConnectionSettingsValues connectionSettings) : base(connectionSettings)
{
}
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
if (contract is JsonContainerContract containerContract)
{
if (containerContract.ItemTypeNameHandling == null)
containerContract.ItemTypeNameHandling = TypeNameHandling.None;
}
return contract;
}
}
public class MySecondCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
{
public MySecondCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
: base(builtinSerializer, connectionSettings) { }
protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
NullValueHandling = NullValueHandling.Ignore,
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
};
protected override ConnectionSettingsAwareContractResolver CreateContractResolver() =>
new MySecondCustomContractResolver(ConnectionSettings); <1>
}
----
<1> override the contract resolver

Now, hooking up this serializer

[source,csharp]
----
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var connectionSettings = new ConnectionSettings(
pool,
connection: new InMemoryConnection(),
sourceSerializer: (builtin, settings) => new MySecondCustomJsonNetSerializer(builtin, settings))
.DefaultIndex("my-index");
var client = new ElasticClient(connectionSettings);
----

and indexing an instance of our document type

[source,csharp]
----
var document = new MyDocument
{
Id = 1,
Name = "My first document",
OwnerId = 2,
SubDocuments = new []
{
new MySubDocument { Name = "my first sub document" },
new MySubDocument { Name = "my second sub document" },
}
};
var indexResponse = client.IndexDocument(document);
----

serializes to

[source,javascript]
----
{
"$type": "Tests.ClientConcepts.HighLevel.Serialization.GettingStarted+MyDocument, Tests",
"id": 1,
"name": "My first document",
"ownerId": 2,
"subDocuments": [
{
"name": "my first sub document"
},
{
"name": "my second sub document"
}
]
}
----

the type information is serialized for the outer `MyDocument` instance, but not for each
`MySubDocument` instance in the `SubDocuments` collection.

When implementing a custom contract resolver derived from `ConnectionSettingsAwareContractResolver`,
be careful not to change the behaviour of the resolver for NEST types; doing so will result in
unexpected behaviour.

[WARNING]
--
Per the https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_TypeNameHandling.htm[Json.NET documentation on TypeNameHandling],
it should be used with caution when your application deserializes JSON from an external source.

--

5 changes: 2 additions & 3 deletions src/CodeGeneration/DocGenerator/SyntaxNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ public static bool TryGetJsonForSyntaxNode(this SyntaxNode node, out string json
{
json = null;

// find the first anonymous object expression
// find the first anonymous object or new object expression
var creationExpressionSyntax = node.DescendantNodes()
.OfType<AnonymousObjectCreationExpressionSyntax>()
.FirstOrDefault();
.FirstOrDefault(n => n is AnonymousObjectCreationExpressionSyntax || n is ObjectCreationExpressionSyntax);

return creationExpressionSyntax != null &&
creationExpressionSyntax.ToFullString().TryGetJsonForAnonymousType(out json);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,9 @@ IEnumerable<JsonConverter> contractJsonConverters

private List<JsonConverter> Converters { get; }


private JsonSerializer CreateSerializer(SerializationFormatting formatting)
{
var s = CreateJsonSerializerSettings() ?? new JsonSerializerSettings();
;
var converters = CreateJsonConverters() ?? Enumerable.Empty<JsonConverter>();
var contract = CreateContractResolver();
s.Formatting = formatting == SerializationFormatting.Indented ? Formatting.Indented : Formatting.None;
Expand All @@ -60,7 +58,7 @@ private JsonSerializer CreateSerializer(SerializationFormatting formatting)
return JsonSerializer.Create(s);
}

private IContractResolver CreateContractResolver()
protected virtual ConnectionSettingsAwareContractResolver CreateContractResolver()
{
var contract = new ConnectionSettingsAwareContractResolver(ConnectionSettings);
ModifyContractResolver(contract);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Nest;
using Nest.JsonNetSerializer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Tests.Framework;
using static Tests.Core.Serialization.SerializationTestHelper;
Expand Down Expand Up @@ -159,14 +160,11 @@ public void DefaultJsonNetSerializerFactoryMethods()
* If you'd like to be more explicit, you can also derive from `ConnectionSettingsAwareSerializerBase`
* and override the `CreateJsonSerializerSettings` and `ModifyContractResolver` methods
*/
public class MyCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
public class MyFirstCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
{
public MyCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
public MyFirstCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
: base(builtinSerializer, connectionSettings) { }

protected override IEnumerable<JsonConverter> CreateJsonConverters() =>
Enumerable.Empty<JsonConverter>();

protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
new JsonSerializerSettings
{
Expand All @@ -176,6 +174,7 @@ protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
protected override void ModifyContractResolver(ConnectionSettingsAwareContractResolver resolver) =>
resolver.NamingStrategy = new SnakeCaseNamingStrategy();
}

/**
* Using `MyCustomJsonNetSerializer`, we can serialize using
*
Expand All @@ -197,6 +196,13 @@ public class MyDocument
public string FilePath { get; set; }

public int OwnerId { get; set; }

public IEnumerable<MySubDocument> SubDocuments { get; set; }
}

public class MySubDocument
{
public string Name { get; set; }
}

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

//hide
Expand All @@ -233,7 +239,8 @@ [U] public void UsingJsonNetSerializer()
id = 1,
name = "My first document",
file_path = (string) null,
owner_id = 2
owner_id = 2,
sub_documents = (object) null
};
/**
* which adheres to the conventions of our configured `MyCustomJsonNetSerializer` serializer.
Expand All @@ -242,5 +249,112 @@ [U] public void UsingJsonNetSerializer()
// hide
Expect(expected, preserveNullInExpected: true).FromRequest(indexResponse);
}

/** ==== Serializing Type Information
* Here's another example that implements a custom contract resolver. The custom contract resolver
* will include the type name within the serialized JSON for the document, which can be useful when
* returning covariant document types within a collection.
*/
public class MySecondCustomContractResolver : ConnectionSettingsAwareContractResolver
{
public MySecondCustomContractResolver(IConnectionSettingsValues connectionSettings) : base(connectionSettings)
{
}

protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
if (contract is JsonContainerContract containerContract)
{
if (containerContract.ItemTypeNameHandling == null)
containerContract.ItemTypeNameHandling = TypeNameHandling.None;
}

return contract;
}
}

public class MySecondCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
{
public MySecondCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
: base(builtinSerializer, connectionSettings) { }

protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
NullValueHandling = NullValueHandling.Ignore,
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
};

protected override ConnectionSettingsAwareContractResolver CreateContractResolver() =>
new MySecondCustomContractResolver(ConnectionSettings); // <1> override the contract resolver
}

/**
* Now, hooking up this serializer
*/
[U] public void MySecondJsonNetSerializer()
{
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var connectionSettings = new ConnectionSettings(
pool,
connection: new InMemoryConnection(),
sourceSerializer: (builtin, settings) => new MySecondCustomJsonNetSerializer(builtin, settings))
.DefaultIndex("my-index");

//hide
connectionSettings.DisableDirectStreaming();

var client = new ElasticClient(connectionSettings);

/** and indexing an instance of our document type */
var document = new MyDocument
{
Id = 1,
Name = "My first document",
OwnerId = 2,
SubDocuments = new []
{
new MySubDocument { Name = "my first sub document" },
new MySubDocument { Name = "my second sub document" },
}
};

var indexResponse = client.IndexDocument(document);

/** serializes to */
//json
var expected = new JObject
{
{ "$type", "Tests.ClientConcepts.HighLevel.Serialization.GettingStarted+MyDocument, Tests" },
{ "id", 1 },
{ "name", "My first document" },
{ "ownerId", 2 },
{ "subDocuments", new JArray
{
new JObject { { "name", "my first sub document" } },
new JObject { { "name", "my second sub document" } },
}
}
};
/**
* the type information is serialized for the outer `MyDocument` instance, but not for each
* `MySubDocument` instance in the `SubDocuments` collection.
*
* When implementing a custom contract resolver derived from `ConnectionSettingsAwareContractResolver`,
* be careful not to change the behaviour of the resolver for NEST types; doing so will result in
* unexpected behaviour.
*
* [WARNING]
* --
* Per the https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_TypeNameHandling.htm[Json.NET documentation on TypeNameHandling],
* it should be used with caution when your application deserializes JSON from an external source.
* --
*/

// hide
Expect(expected).FromRequest(indexResponse);
}
}
}

0 comments on commit 3d244b9

Please sign in to comment.