From 2350084164f2e83af7a3253b838f196560ce317a Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 6 Mar 2023 13:34:20 +0000 Subject: [PATCH] Tweak formatting of code for docs --- docs/client-concepts/client-concepts.asciidoc | 8 +- .../custom-serialization.asciidoc | 20 +- .../modeling-documents-with-types.asciidoc | 5 +- exclusion.dic | 3 +- tests/Tests/Documentation/.editorconfig | 2 + .../Serialization/CustomSerializationTests.cs | 620 +++++++++--------- .../ModellingDocumentsWithTypesTests.cs | 36 +- 7 files changed, 360 insertions(+), 334 deletions(-) create mode 100644 tests/Tests/Documentation/.editorconfig diff --git a/docs/client-concepts/client-concepts.asciidoc b/docs/client-concepts/client-concepts.asciidoc index 18fe3000282..ba04a66bdfa 100644 --- a/docs/client-concepts/client-concepts.asciidoc +++ b/docs/client-concepts/client-concepts.asciidoc @@ -1,11 +1,11 @@ [[client-concepts]] -== Client concepts += Client concepts -The high-level .NET client for {es} maps closely to the original {es} API. All +The .NET client for {es} maps closely to the original {es} API. All requests and responses are exposed through types, making it ideal for getting up and running quickly. [[serialization]] -=== Serialization +== Serialization By default, the .NET client for {es} uses the Microsoft System.Text.Json library for serialization. The client understands how to serialize and deserialize the request and response types correctly. It also handles (de)serialization of user POCO types representing documents read or written to {es}. @@ -13,7 +13,7 @@ deserialize the request and response types correctly. It also handles (de)serial The client has two distinct serialization responsibilities - serialization of the types owned by the `Elastic.Clients.Elasticsearch` library and serialization of source documents, modeled in application code. The first responsibility is entirely internal; the second is configurable. [[source-serialization]] -==== Source serialization +=== Source serialization Source serialization refers to the process of (de)serializing POCO types in consumer applications as source documents indexed and retrieved from {es}. A source serializer implementation handles serialization, with the default implementation using the `System.Text.Json` library. As a result, you may use `System.Text.Json` attributes and converters to control the serialization behavior. diff --git a/docs/client-concepts/serialization/custom-serialization.asciidoc b/docs/client-concepts/serialization/custom-serialization.asciidoc index ad4a98132f4..2341d2e49ba 100644 --- a/docs/client-concepts/serialization/custom-serialization.asciidoc +++ b/docs/client-concepts/serialization/custom-serialization.asciidoc @@ -1,12 +1,12 @@ [[custom-serialization]] -=== Custom Serialization +==== Custom serialization The built-in source serializer handles most POCO document models correctly. Sometimes, you may need further control over how your types are serialized. NOTE: The built-in source serializer uses the Microsoft https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview[`System.Text.Json` library] internally. You can apply `System.Text.Json` attributes and converters to control serialization of your document types. [[system-text-json-attributes]] -==== Using System.Text.Json attributes +===== Using `System.Text.Json` attributes `System.Text.Json` includes attributes that can be applied to types and properties to control how they are serialized. These can be applied to your POCO document types to perform actions such as controlling the name of a property, or ignoring a property entirely. Visit the https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview[Microsoft documentation for further examples]. @@ -14,9 +14,8 @@ We can model a document to represent data about a person using a regular class ( [source,csharp] ---- -include-tagged::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[usings-serialization] - -include-tagged::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[person-class-with-attributes] +include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=usings-serialization] +include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=person-class-with-attributes] ---- <1> the `JsonPropertyName` attribute is used to provide a specific name (`forename`) for the `FirstName` property when serialized. <2> the `JsonIgnore` attribute is used to prevent the `Age` property from appearing in the serialized JSON. @@ -25,9 +24,8 @@ We can then index the an instance of the document into {es}. [source,csharp] ---- -include-tagged::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[usings] - -include-tagged::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[index-person-with-attributes] +include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=usings] +include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=index-person-with-attributes] ---- The index request is serialized, with the source serializer handling the `Person` type, serializing the POCO property named `FirstName` to the JSON object member named `forename`. The `Age` property is ignored and does not appear in the JSON. @@ -40,17 +38,17 @@ The index request is serialized, with the source serializer handling the `Person ---- [[registering-custom-converters]] -==== Registering custom System.Text.Json converters +===== Registering custom `System.Text.Json` converters TODO [[configuring-custom-jsonserializeroptions]] -==== Configuring custom JsonSerializerOptions +===== Configuring custom `JsonSerializerOptions` TODO [[injecting-custom-serializer]] -==== Injecting a custom serializer +===== Injecting a custom serializer TODO - Deriving from SystemTextJsonSerializer diff --git a/docs/client-concepts/serialization/modeling-documents-with-types.asciidoc b/docs/client-concepts/serialization/modeling-documents-with-types.asciidoc index bdcc1f1501c..59486c7a41d 100644 --- a/docs/client-concepts/serialization/modeling-documents-with-types.asciidoc +++ b/docs/client-concepts/serialization/modeling-documents-with-types.asciidoc @@ -1,5 +1,5 @@ [[modeling-documents-with-types]] -=== Modeling documents with types +==== Modeling documents with types {es} provides search and aggregation capabilities on the documents that it is sent and indexes. These documents are sent as JSON objects within the request body of a HTTP request. It is natural to model documents within the {es} .NET client using @@ -8,7 +8,7 @@ https://en.wikipedia.org/wiki/Plain_Old_CLR_Object[POCOs (__Plain Old CLR Object This section provides an overview of how types and type hierarchies can be used to model documents. [[default-behaviour]] -==== Default behaviour +===== Default behaviour The default behaviour is to serialize type property names as camelcase JSON object members. @@ -24,7 +24,6 @@ We can then index the an instance of the document into {es}. [source,csharp] ---- include-tagged::{doc-tests-src}/ClientConcepts/Serialization/ModellingDocumentsWithTypesTests.cs[usings] - include-tagged::{doc-tests-src}/ClientConcepts/Serialization/ModellingDocumentsWithTypesTests.cs[index-my-document] ---- diff --git a/exclusion.dic b/exclusion.dic index 147a491931c..fbef39248b3 100644 --- a/exclusion.dic +++ b/exclusion.dic @@ -2,4 +2,5 @@ deserialize json async inferrer -elasticsearch \ No newline at end of file +elasticsearch +asciidocs \ No newline at end of file diff --git a/tests/Tests/Documentation/.editorconfig b/tests/Tests/Documentation/.editorconfig new file mode 100644 index 00000000000..4ce8de103f6 --- /dev/null +++ b/tests/Tests/Documentation/.editorconfig @@ -0,0 +1,2 @@ +[*] +indent_style = space \ No newline at end of file diff --git a/tests/Tests/Documentation/ClientConcepts/Serialization/CustomSerializationTests.cs b/tests/Tests/Documentation/ClientConcepts/Serialization/CustomSerializationTests.cs index 5616fe73a4b..85e2aecdbce 100644 --- a/tests/Tests/Documentation/ClientConcepts/Serialization/CustomSerializationTests.cs +++ b/tests/Tests/Documentation/ClientConcepts/Serialization/CustomSerializationTests.cs @@ -2,331 +2,351 @@ // 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. +// IMPORTANT: These tests have a secondary use as code snippets used in documentation. +// We disable formatting in sections of this file to ensure the correct indentation when tagged regions are +// included in the asciidocs. While hard to read, this formatting should be left as-is for docs generation. +// We also include using directives that are not required due to global using directives, but remain here +// so that can appear in the documentation. + #pragma warning disable IDE0005 -//tag::usings +//tag::usings[] using System; using System.Text.Json; -//tag::usings-serialization +//tag::usings-serialization[] using System.Text.Json.Serialization; -//end::usings-serialization +//end::usings-serialization[] using System.Threading.Tasks; using Elastic.Transport; using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch.Serialization; -//end::usings -#pragma warning restore IDE0005 +//end::usings[] using System.Text; using VerifyXunit; using System.IO; +#pragma warning restore IDE0005 namespace Tests.Documentation.Serialization; [UsesVerify] public class CustomSerializationTests : DocumentationTestBase { - [U] - public async Task CustomizingJsonSerializerOptions() - { - // This example resets the PropertyNamingPolicy, such that the existing C# Pascal case is sent in the JSON. - - //tag::custom-options - Action configureOptions = o => o.PropertyNamingPolicy = null; // <1> - //end::custom-options - - //tag::create-client - var nodePool = new SingleNodePool(new Uri("http://localhost:9200")); - var settings = new ElasticsearchClientSettings( - nodePool, - sourceSerializer: (defaultSerializer, settings) => - new DefaultSourceSerializer(settings, configureOptions)); // <2> - var client = new ElasticsearchClient(settings); - //end::create-client - - // Needed for the test assertion as we should use the in memory connection and disable direct streaming. - // We don't want to include those in the docs as it may mislead or confuse developers. - // Any changes to the documentation code needs to be applied here also. - settings = new ElasticsearchClientSettings( - nodePool, - new InMemoryConnection(), - sourceSerializer: (defaultSerializer, settings) => - new DefaultSourceSerializer(settings, configureOptions)) - .DisableDirectStreaming(); - client = new ElasticsearchClient(settings); - - //tag::index-person - var person = new Person { FirstName = "Steve" }; - var indexResponse = await client.IndexAsync(person, "my-index-name"); - //end::index-person - - var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); - await Verifier.Verify(requestJson); - - var ms = new MemoryStream(indexResponse.ApiCallDetails.RequestBodyInBytes); - var deserializedPerson = client.SourceSerializer.Deserialize(ms); - deserializedPerson.FirstName.Should().Be("Steve"); - } - - //tag::person-class - private class Person - { - public string FirstName { get; set; } - } - //end::person-class + [U] + public async Task CustomizingJsonSerializerOptions() + { + // This example resets the PropertyNamingPolicy, such that the existing C# Pascal case is sent in the JSON. + + //tag::custom-options-local-function[] + static void ConfigureOptions(JsonSerializerOptions o) => o.PropertyNamingPolicy = null; // <1> + //end::custom-options-local-function[] + + //tag::create-client[] + var nodePool = new SingleNodePool(new Uri("http://localhost:9200")); + var settings = new ElasticsearchClientSettings( + nodePool, + sourceSerializer: (defaultSerializer, settings) => + new DefaultSourceSerializer(settings, ConfigureOptions)); // <3> + var client = new ElasticsearchClient(settings); + //end::create-client[] + + // Needed for the test assertion as we should use the in memory connection and disable direct streaming. + // We don't want to include those in the docs as it may mislead or confuse developers. + // Any changes to the documentation code needs to be applied here also. + settings = new ElasticsearchClientSettings( + nodePool, + new InMemoryConnection(), + sourceSerializer: (defaultSerializer, settings) => + new DefaultSourceSerializer(settings, ConfigureOptions)) + .DisableDirectStreaming(); + client = new ElasticsearchClient(settings); + + //tag::index-person[] + var person = new Person { FirstName = "Steve" }; + var indexResponse = await client.IndexAsync(person, "my-index-name"); + //end::index-person[] + + var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); + await Verifier.Verify(requestJson); + + var ms = new MemoryStream(indexResponse.ApiCallDetails.RequestBodyInBytes); + var deserializedPerson = client.SourceSerializer.Deserialize(ms); + deserializedPerson.FirstName.Should().Be("Steve"); + + // Alternative example using an Action + + //tag::custom-options-action[] + Action configureOptions = o => o.PropertyNamingPolicy = null; // <3> + //end::custom-options-action[] + } + +#pragma warning disable format +//tag::person-class[] +private class Person +{ + public string FirstName { get; set; } +} +//end::person-class[] +#pragma warning restore format } [UsesVerify] public class CustomSerializationTests_WithAttributes : DocumentationTestBase { - [U] - public async Task UsingSystemTextJsonAttributes() - { - // This example uses a STJ attribute on the property to provide a specific name to use in the JSON. - - //tag::index-person-with-attributes - var person = new Person { FirstName = "Steve", Age = 35 }; - var indexResponse = await Client.IndexAsync(person, "my-index-name"); - //end::index-person-with-attributes - - var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); - await Verifier.Verify(requestJson); - } - - [U] - public async Task UsingSystemTextJsonConverterAttributes() - { - //tag::index-customer-with-converter - var customer = new Customer { CustomerName = "Customer Ltd", Type = CustomerType.Enhanced }; - var indexResponse = await Client.IndexAsync(customer, "my-index-name"); - //end::index-customer-with-converter - - var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); - await Verifier.Verify(requestJson); - - var ms = new MemoryStream(indexResponse.ApiCallDetails.RequestBodyInBytes); - var deserializedCustomer = Client.SourceSerializer.Deserialize(ms); - deserializedCustomer.CustomerName.Should().Be("Customer Ltd"); - deserializedCustomer.Type.Should().Be(CustomerType.Enhanced); - } - - //tag::person-class-with-attributes - private class Person - { - [JsonPropertyName("forename")] // <1> - public string FirstName { get; set; } - - [JsonIgnore] // <2> - public int Age { get; set; } - } - //end::person-class-with-attributes - - [JsonConverter(typeof(CustomerConverter))] - private class Customer - { - public string CustomerName { get; set; } - public CustomerType Type { get; set; } - } - - private enum CustomerType - { - Standard, - Enhanced - } - - private class CustomerConverter : JsonConverter - { - public override Customer Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - Customer customer = new Customer(); - - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) - { - if (reader.TokenType == JsonTokenType.PropertyName) - { - if (reader.ValueTextEquals("customerName")) - { - reader.Read(); - customer.CustomerName = reader.GetString(); - continue; - } - - if (reader.ValueTextEquals("isStandard")) - { - reader.Read(); - var isStandard = reader.GetBoolean(); - - if (isStandard) - { - customer.Type = CustomerType.Standard; - } - else - { - customer.Type = CustomerType.Enhanced; - } - - continue; - } - } - } - - return customer; - } - - public override void Write(Utf8JsonWriter writer, Customer value, JsonSerializerOptions options) - { - if (value is null) - { - writer.WriteNullValue(); - return; - } - - writer.WriteStartObject(); - - if (!string.IsNullOrEmpty(value.CustomerName)) - { - writer.WritePropertyName("customerName"); - writer.WriteStringValue(value.CustomerName); - } - - writer.WritePropertyName("isStandard"); - - if (value.Type == CustomerType.Standard) - { - writer.WriteBooleanValue(true); - } - else - { - writer.WriteBooleanValue(false); - } - - writer.WriteEndObject(); - } - } - - private class CustomerTypeConverter : JsonConverter - { - public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.GetString() switch - { - "basic" => CustomerType.Standard, - "premium" => CustomerType.Enhanced, - _ => throw new JsonException( - $"Unknown value read when deserializing {nameof(CustomerType)}."), - }; - } - - public override void Write(Utf8JsonWriter writer, CustomerType value, JsonSerializerOptions options) - { - switch (value) - { - case CustomerType.Standard: - writer.WriteStringValue("basic"); - return; - case CustomerType.Enhanced: - writer.WriteStringValue("premium"); - return; - } - - writer.WriteNullValue(); - } - } + [U] + public async Task UsingSystemTextJsonAttributes() + { + // This example uses a STJ attribute on the property to provide a specific name to use in the JSON. + +#pragma warning disable format +//tag::index-person-with-attributes[] +var person = new Person { FirstName = "Steve", Age = 35 }; +var indexResponse = await Client.IndexAsync(person, "my-index-name"); +//end::index-person-with-attributes[] +#pragma warning restore format + + var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); + await Verifier.Verify(requestJson); + } + + [U] + public async Task UsingSystemTextJsonConverterAttributes() + { +#pragma warning disable format +//tag::index-customer-with-converter[] +var customer = new Customer { CustomerName = "Customer Ltd", Type = CustomerType.Enhanced }; +var indexResponse = await Client.IndexAsync(customer, "my-index-name"); +//end::index-customer-with-converter[] +#pragma warning restore format + + var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); + await Verifier.Verify(requestJson); + + var ms = new MemoryStream(indexResponse.ApiCallDetails.RequestBodyInBytes); + var deserializedCustomer = Client.SourceSerializer.Deserialize(ms); + deserializedCustomer.CustomerName.Should().Be("Customer Ltd"); + deserializedCustomer.Type.Should().Be(CustomerType.Enhanced); + } + +#pragma warning disable format +//tag::person-class-with-attributes[] +private class Person +{ + [JsonPropertyName("forename")] // <1> + public string FirstName { get; set; } + + [JsonIgnore] // <2> + public int Age { get; set; } +} +//end::person-class-with-attributes[] +#pragma warning restore format + + [JsonConverter(typeof(CustomerConverter))] + private class Customer + { + public string CustomerName { get; set; } + public CustomerType Type { get; set; } + } + + private enum CustomerType + { + Standard, + Enhanced + } + + private class CustomerConverter : JsonConverter + { + public override Customer Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var customer = new Customer(); + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.ValueTextEquals("customerName")) + { + reader.Read(); + customer.CustomerName = reader.GetString(); + continue; + } + + if (reader.ValueTextEquals("isStandard")) + { + reader.Read(); + var isStandard = reader.GetBoolean(); + + if (isStandard) + { + customer.Type = CustomerType.Standard; + } + else + { + customer.Type = CustomerType.Enhanced; + } + + continue; + } + } + } + + return customer; + } + + public override void Write(Utf8JsonWriter writer, Customer value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + + if (!string.IsNullOrEmpty(value.CustomerName)) + { + writer.WritePropertyName("customerName"); + writer.WriteStringValue(value.CustomerName); + } + + writer.WritePropertyName("isStandard"); + + if (value.Type == CustomerType.Standard) + { + writer.WriteBooleanValue(true); + } + else + { + writer.WriteBooleanValue(false); + } + + writer.WriteEndObject(); + } + } + + private class CustomerTypeConverter : JsonConverter + { + public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString() switch + { + "basic" => CustomerType.Standard, + "premium" => CustomerType.Enhanced, + _ => throw new JsonException( + $"Unknown value read when deserializing {nameof(CustomerType)}."), + }; + } + + public override void Write(Utf8JsonWriter writer, CustomerType value, JsonSerializerOptions options) + { + switch (value) + { + case CustomerType.Standard: + writer.WriteStringValue("basic"); + return; + case CustomerType.Enhanced: + writer.WriteStringValue("premium"); + return; + } + + writer.WriteNullValue(); + } + } } [UsesVerify] public class CustomSerializationTestsEnumAttribute : DocumentationTestBase { - [U] - public async Task DerivingFromSystemTextJsonSerializer_ToRegisterACustomEnumConverter_BeforeBuiltInConverters() - { - var nodePool = new SingleNodePool(new Uri("http://localhost:9200")); - var settings = new ElasticsearchClientSettings( - nodePool, - sourceSerializer: (defaultSerializer, settings) => - new MyCustomSerializer(settings)); // <1> - var client = new ElasticsearchClient(settings); - - // Needed for the test assertion as we should use the in memory connection and disable direct streaming. - // We don't want to include those in the docs as it may mislead or confuse developers. - // Any changes to the documentation code needs to be applied here also. - settings = new ElasticsearchClientSettings( - nodePool, - new InMemoryConnection(), - sourceSerializer: (defaultSerializer, settings) => - new MyCustomSerializer(settings)) - .DisableDirectStreaming(); - client = new ElasticsearchClient(settings); - - var customer = new Customer - { - CustomerName = "Customer Ltd", - Type = CustomerType.Enhanced - }; - - var indexResponse = await client.IndexAsync(customer, "my-index-name"); - - var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); - await Verifier.Verify(requestJson); - - var ms = new MemoryStream(indexResponse.ApiCallDetails.RequestBodyInBytes); - var deserializedCustomer = client.SourceSerializer.Deserialize(ms); - deserializedCustomer.CustomerName.Should().Be("Customer Ltd"); - deserializedCustomer.Type.Should().Be(CustomerType.Enhanced); - } - - private class Customer - { - public string CustomerName { get; set; } - public CustomerType Type { get; set; } - } - - private enum CustomerType - { - Standard, - Enhanced - } - - private class MyCustomSerializer : SystemTextJsonSerializer - { - private readonly JsonSerializerOptions _options; - - public MyCustomSerializer(IElasticsearchClientSettings settings) : base(settings) - { - var options = DefaultSourceSerializer.CreateDefaultJsonSerializerOptions(false); - - options.Converters.Add(new CustomerTypeConverter()); - - _options = DefaultSourceSerializer.AddDefaultConverters(options); - } - - protected override JsonSerializerOptions CreateJsonSerializerOptions() => _options; - } - - private class CustomerTypeConverter : JsonConverter - { - public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.GetString() switch - { - "basic" => CustomerType.Standard, - "premium" => CustomerType.Enhanced, - _ => throw new JsonException( - $"Unknown value read when deserializing {nameof(CustomerType)}."), - }; - } - - public override void Write(Utf8JsonWriter writer, CustomerType value, JsonSerializerOptions options) - { - switch (value) - { - case CustomerType.Standard: - writer.WriteStringValue("basic"); - return; - case CustomerType.Enhanced: - writer.WriteStringValue("premium"); - return; - } - - writer.WriteNullValue(); - } - } + [U] + public async Task DerivingFromSystemTextJsonSerializer_ToRegisterACustomEnumConverter_BeforeBuiltInConverters() + { + var nodePool = new SingleNodePool(new Uri("http://localhost:9200")); + var settings = new ElasticsearchClientSettings( + nodePool, + sourceSerializer: (defaultSerializer, settings) => + new MyCustomSerializer(settings)); // <1> + var client = new ElasticsearchClient(settings); + + // Needed for the test assertion as we should use the in memory connection and disable direct streaming. + // We don't want to include those in the docs as it may mislead or confuse developers. + // Any changes to the documentation code needs to be applied here also. + settings = new ElasticsearchClientSettings( + nodePool, + new InMemoryConnection(), + sourceSerializer: (defaultSerializer, settings) => + new MyCustomSerializer(settings)) + .DisableDirectStreaming(); + client = new ElasticsearchClient(settings); + + var customer = new Customer + { + CustomerName = "Customer Ltd", + Type = CustomerType.Enhanced + }; + + var indexResponse = await client.IndexAsync(customer, "my-index-name"); + + var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); + await Verifier.Verify(requestJson); + + var ms = new MemoryStream(indexResponse.ApiCallDetails.RequestBodyInBytes); + var deserializedCustomer = client.SourceSerializer.Deserialize(ms); + deserializedCustomer.CustomerName.Should().Be("Customer Ltd"); + deserializedCustomer.Type.Should().Be(CustomerType.Enhanced); + } + + private class Customer + { + public string CustomerName { get; set; } + public CustomerType Type { get; set; } + } + + private enum CustomerType + { + Standard, + Enhanced + } + + private class MyCustomSerializer : SystemTextJsonSerializer + { + private readonly JsonSerializerOptions _options; + + public MyCustomSerializer(IElasticsearchClientSettings settings) : base(settings) + { + var options = DefaultSourceSerializer.CreateDefaultJsonSerializerOptions(false); + + options.Converters.Add(new CustomerTypeConverter()); + + _options = DefaultSourceSerializer.AddDefaultConverters(options); + } + + protected override JsonSerializerOptions CreateJsonSerializerOptions() => _options; + } + + private class CustomerTypeConverter : JsonConverter + { + public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString() switch + { + "basic" => CustomerType.Standard, + "premium" => CustomerType.Enhanced, + _ => throw new JsonException( + $"Unknown value read when deserializing {nameof(CustomerType)}."), + }; + } + + public override void Write(Utf8JsonWriter writer, CustomerType value, JsonSerializerOptions options) + { + switch (value) + { + case CustomerType.Standard: + writer.WriteStringValue("basic"); + return; + case CustomerType.Enhanced: + writer.WriteStringValue("premium"); + return; + } + + writer.WriteNullValue(); + } + } } diff --git a/tests/Tests/Documentation/ClientConcepts/Serialization/ModellingDocumentsWithTypesTests.cs b/tests/Tests/Documentation/ClientConcepts/Serialization/ModellingDocumentsWithTypesTests.cs index 632da6a1647..5933b7eb4da 100644 --- a/tests/Tests/Documentation/ClientConcepts/Serialization/ModellingDocumentsWithTypesTests.cs +++ b/tests/Tests/Documentation/ClientConcepts/Serialization/ModellingDocumentsWithTypesTests.cs @@ -2,6 +2,12 @@ // 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. +// IMPORTANT: These tests have a secondary use as code snippets used in documentation. +// We disable formatting in sections of this file to ensure the correct indentation when tagged regions are +// included in the asciidocs. While hard to read, this formatting should be left as-is for docs generation. +// We also include using directives that are not required due to global using directives, but remain here +// so that can appear in the documentation. + #pragma warning disable IDE0005 //tag::usings using System.Threading.Tasks; @@ -16,27 +22,27 @@ namespace Tests.Documentation.Serialization; //tag::my-document-poco public class MyDocument { - public string StringProperty { get; set; } + public string StringProperty { get; set; } } //end::my-document-poco [UsesVerify] public class ModellingDocumentsWithTypesTests : DocumentationTestBase { - [U] - public async Task IndexMyDocument() - { - //tag::index-my-document - var document = new MyDocument - { - StringProperty = "value" - }; + [U] + public async Task IndexMyDocument() + { + //tag::index-my-document + var document = new MyDocument + { + StringProperty = "value" + }; - var indexResponse = await Client - .IndexAsync(document, "my-index-name"); - //end::index-my-document + var indexResponse = await Client + .IndexAsync(document, "my-index-name"); + //end::index-my-document - var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); - await Verifier.Verify(requestJson); - } + var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes); + await Verifier.Verify(requestJson); + } }