源序列化

编辑

源序列化指的是在消费者应用程序中(反)序列化 POCO 类型,作为从 Elasticsearch 索引和检索的源文档的过程。源序列化器实现处理序列化,默认实现使用 System.Text.Json 库。因此,您可以使用 System.Text.Json 属性和转换器来控制序列化行为。

使用类型建模文档

编辑

Elasticsearch 提供对其发送和索引的文档的搜索和聚合功能。这些文档作为 JSON 对象在 HTTP 请求的请求正文中发送。使用 POCO(Plain Old CLR Objects在 Elasticsearch .NET 客户端中建模文档是很自然的。

本节概述了如何使用类型和类型层次结构来建模文档。

默认行为

编辑

默认行为是将类型属性名称序列化为小驼峰式 JSON 对象成员。

我们可以使用常规类(POCO)来建模文档。

public class MyDocument
{
    public string StringProperty { get; set; }
}

然后,我们可以将文档实例索引到 Elasticsearch 中。

using System.Threading.Tasks;
using Elastic.Clients.Elasticsearch;

var document = new MyDocument
{
    StringProperty = "value"
};

var indexResponse = await Client
    .IndexAsync(document, "my-index-name");

索引请求被序列化,源序列化器处理 MyDocument 类型,将名为 StringProperty 的 POCO 属性序列化为名为 stringProperty 的 JSON 对象成员。

{
  "stringProperty": "value"
}

自定义源序列化

编辑

内置的源序列化器可以正确处理大多数 POCO 文档模型。有时,您可能需要进一步控制类型的序列化方式。

内置的源序列化器在内部使用 Microsoft System.Text.Json。您可以应用 System.Text.Json 属性和转换器来控制文档类型的序列化。

使用 System.Text.Json 属性
编辑

System.Text.Json 包括可以应用于类型和属性以控制其序列化的属性。这些属性可以应用于您的 POCO 文档类型,以执行诸如控制属性的名称或完全忽略属性等操作。请访问 Microsoft 文档以获取更多示例

我们可以使用常规类(POCO)来建模一个表示人员数据的文档,并根据需要应用 System.Text.Json 属性。

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("forename")] 
    public string FirstName { get; set; }

    [JsonIgnore] 
    public int Age { get; set; }
}

JsonPropertyName 属性确保 FirstName 属性在序列化时使用 JSON 名称 forename

JsonIgnore 属性阻止 Age 属性出现在序列化的 JSON 中。

然后,我们可以将文档实例索引到 Elasticsearch 中。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

var person = new Person { FirstName = "Steve", Age = 35 };
var indexResponse = await Client.IndexAsync(person, "my-index-name");

索引请求被序列化,源序列化器处理 Person 类型,将名为 FirstName 的 POCO 属性序列化为名为 forename 的 JSON 对象成员。Age 属性被忽略,并且不会出现在 JSON 中。

{
  "forename": "Steve"
}
配置自定义 JsonSerializerOptions
编辑

默认源序列化器在序列化源文档类型时应用一组标准 JsonSerializerOptions。在某些情况下,您可能需要覆盖我们的一些默认设置。这可以通过创建 DefaultSourceSerializer 实例并传递一个 Action<JsonSerializerOptions> 来实现,该实例在设置我们的默认设置后应用。此机制允许您应用其他设置或更改我们默认设置的值。

DefaultSourceSerializer 包括一个接受当前 IElasticsearchClientSettings 和一个 configureOptions Action 的构造函数。

public DefaultSourceSerializer(IElasticsearchClientSettings settings, Action<JsonSerializerOptions> configureOptions);

我们的应用程序定义了以下 Person 类,该类对我们将要索引到 Elasticsearch 的文档进行建模。

public class Person
{
    public string FirstName { get; set; }
}

我们希望使用 Pascal 大小写来序列化 JSON 属性中的源文档。由于在 DefaultSouceSerializer 中应用的选项将 PropertyNamingPolicy 设置为 JsonNamingPolicy.CamelCase,因此我们必须覆盖此设置。在配置 ElasticsearchClientSettings 后,我们将文档索引到 Elasticsearch。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

static void ConfigureOptions(JsonSerializerOptions o) => 
    o.PropertyNamingPolicy = null;

var nodePool = new SingleNodePool(new Uri("https://127.0.0.1:9200"));
var settings = new ElasticsearchClientSettings(
    nodePool,
    sourceSerializer: (defaultSerializer, settings) =>
        new DefaultSourceSerializer(settings, ConfigureOptions)); 
var client = new ElasticsearchClient(settings);

var person = new Person { FirstName = "Steve" };
var indexResponse = await client.IndexAsync(person, "my-index-name");

可以定义一个接受 JsonSerializerOptions 参数的本地函数。在这里,我们将 PropertyNamingPolicy 设置为 null。这将返回到 System.Text.Json 的默认行为,即使用 Pascal 大小写。

在创建 ElasticsearchClientSettings 时,我们使用 lambda 提供一个 SourceSerializerFactory。工厂函数创建 DefaultSourceSerializer 的新实例,并传入 settings 和我们的 ConfigureOptions 本地函数。我们现在已经使用源序列化器的自定义实例配置了设置。

Person 实例被序列化,源序列化器使用 Pascal 大小写序列化名为 FirstName 的 POCO 属性。

{
  "FirstName": "Steve"
}

作为使用本地函数的替代方法,我们可以将 Action<JsonSerializerOptions> 存储到一个变量中,该变量可以传递给 DefaultSouceSerializer 构造函数。

Action<JsonSerializerOptions> configureOptions = o => o.PropertyNamingPolicy = null;
注册自定义 System.Text.Json 转换器
编辑

在某些更高级的情况下,您的类型可能需要在序列化期间进行进一步的自定义,而这是使用 System.Text.Json 属性无法实现的。在这些情况下,Microsoft 的建议是利用自定义 JsonConverter。使用 DefaultSourceSerializer 序列化的源文档类型可以利用自定义转换器的强大功能。

对于此示例,我们的应用程序有一个文档类,该文档类应使用旧的 JSON 结构来继续使用现有的索引文档进行操作。有几个选项可用,但在此情况下,我们将应用自定义转换器。

定义了我们的类,并且 JsonConverter 属性应用于类类型,指定了自定义转换器的类型。

using System.Text.Json.Serialization;

[JsonConverter(typeof(CustomerConverter))] 
public class Customer
{
    public string CustomerName { get; set; }
    public CustomerType CustomerType { get; set; }
}

public enum CustomerType
{
    Standard,
    Enhanced
}

JsonConverter 属性向 System.Text.Json 发出信号,表明它在序列化此类的实例时应使用 CustomerConverter 类型的转换器。

在序列化此类时,我们必须发送一个名为 isStandard 的布尔属性,而不是包含一个表示 CustomerType 属性值的字符串值。此要求可以通过自定义 JsonConverter 实现来实现。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

public class CustomerConverter : JsonConverter<Customer>
{
    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.CustomerType = CustomerType.Standard;
                    }
                    else
                    {
                        customer.CustomerType = 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.CustomerType == CustomerType.Standard) 
        {
            writer.WriteBooleanValue(true);
        }
        else
        {
            writer.WriteBooleanValue(false);
        }

        writer.WriteEndObject();
    }
}

读取时,此转换器读取 isStandard 布尔值并将其转换为正确的 CustomerType 枚举值。

写入时,此转换器将 CustomerType 枚举值转换为 isStandard 布尔属性。

然后,我们可以将客户文档索引到 Elasticsearch 中。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

var customer = new Customer
{
    CustomerName = "Customer Ltd",
    CustomerType = CustomerType.Enhanced
};
var indexResponse = await Client.IndexAsync(customer, "my-index-name");

使用自定义转换器序列化 Customer 实例,创建以下 JSON 文档。

{
  "customerName": "Customer Ltd",
  "isStandard": false
}
创建自定义 SystemTextJsonSerializer
编辑

内置的 DefaultSourceSerializer 包括在源序列化期间应用的 JsonConverter 实例的注册。在大多数情况下,这些实例为序列化源文档提供了正确的行为,包括在其属性上使用 Elastic.Clients.Elasticsearch 类型的文档。

您可能需要更多控制转换器注册顺序的一个示例是序列化 enum 类型。 DefaultSourceSerializer 注册 System.Text.Json.Serialization.JsonStringEnumConverter,因此枚举值使用其字符串表示形式进行序列化。通常,这是用于索引到 Elasticsearch 的文档类型的首选选项。

在某些情况下,您可能需要控制为枚举值发送的字符串值。这在 System.Text.Json 中不直接支持,但可以通过为您希望自定义的 enum 类型创建自定义 JsonConverter 来实现。在这种情况下,仅在 enum 类型上使用 JsonConverterAttribute 来注册转换器是不够的。 System.Text.Json 将优先选择添加到 JsonSerializerOptionsConverters 集合中的转换器,而不是应用于 enum 类型的属性。因此,必须从 Converters 集合中删除 JsonStringEnumConverter,或者在 JsonStringEnumConverter 之前为您的 enum 类型注册一个专门的转换器。

后者可以通过多种技术实现。使用 Elasticsearch .NET 库时,我们可以通过从抽象 SystemTextJsonSerializer 类派生来实现此目的。

这里我们有一个 POCO,它使用 CustomerType 枚举作为属性的类型。

using System.Text.Json.Serialization;

public class Customer
{
    public string CustomerName { get; set; }
    public CustomerType CustomerType { get; set; }
}

public enum CustomerType
{
    Standard,
    Enhanced
}

为了自定义 CustomerType 序列化期间使用的字符串,我们定义了一个特定于我们的 enum 类型的自定义 JsonConverter

using System.Text.Json.Serialization;

public class CustomerTypeConverter : JsonConverter<CustomerType>
{
    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();
    }
}

读取时,此转换器将 JSON 中使用的字符串转换为匹配的枚举值。

写入时,此转换器将 CustomerType 枚举值转换为写入 JSON 的自定义字符串值。

我们创建了一个从 SystemTextJsonSerializer 派生的序列化器,以便完全控制转换器的注册顺序。

using System.Text.Json;
using Elastic.Clients.Elasticsearch.Serialization;

public 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; 
}

继承自 SystemTextJsonSerializer

在构造函数中,使用工厂方法 DefaultSourceSerializer.CreateDefaultJsonSerializerOptions 创建序列化的默认选项。在此阶段不注册任何默认转换器,因为我们传递 false 作为参数。

将我们的 CustomerTypeConverter 注册为第一个转换器。

要应用任何默认转换器,请调用 DefaultSourceSerializer.AddDefaultConverters 帮助方法,传递要修改的选项。

实现 CreateJsonSerializerOptions 方法,返回存储的 JsonSerializerOptions

由于我们在默认转换器(包括 JsonStringEnumConverter)之前注册了 CustomerTypeConverter,因此在序列化源文档中的 CustomerType 实例时,我们的转换器具有优先权。

基类 SystemTextJsonSerializer 处理绑定的实现细节,这对于确保内置转换器在需要时可以访问 IElasticsearchClientSettings 是必需的。

然后,我们可以将客户文档索引到 Elasticsearch 中。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

var customer = new Customer
{
    CustomerName = "Customer Ltd",
    CustomerType = CustomerType.Enhanced
};

var indexResponse = await client.IndexAsync(customer, "my-index-name");

使用自定义 enum 转换器序列化 Customer 实例,从而创建以下 JSON 文档。

{
  "customerName": "Customer Ltd",
  "customerType": "premium" 
}

序列化期间应用的字符串值由我们的自定义转换器提供。

创建自定义 Serializer
编辑

假设你更喜欢为你的源类型使用替代的 JSON 序列化库。在这种情况下,你可以注入一个独立的序列化器,仅用于序列化 _source_fields 或任何需要写入和返回用户提供值的地方。

从技术上讲,实现 Elastic.Transport.Serializer 足以创建一个自定义源序列化器。

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Elastic.Transport;

public class VanillaSerializer : Serializer
{
    public override object Deserialize(Type type, Stream stream) =>
        throw new NotImplementedException();

    public override T Deserialize<T>(Stream stream) =>
        throw new NotImplementedException();

    public override ValueTask<object> DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default) =>
        throw new NotImplementedException();

    public override ValueTask<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default) =>
        throw new NotImplementedException();

    public override void Serialize<T>(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None) =>
        throw new NotImplementedException();

    public override Task SerializeAsync<T>(T data, Stream stream,
        SerializationFormatting formatting = SerializationFormatting.None, CancellationToken cancellationToken = default) =>
            throw new NotImplementedException();
}

序列化器的注册在 ConnectionSettings 构造函数中执行。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

var nodePool = new SingleNodePool(new Uri("https://127.0.0.1:9200"));
var settings = new ElasticsearchClientSettings(
    nodePool,
    sourceSerializer: (defaultSerializer, settings) =>
        new VanillaSerializer()); 
var client = new ElasticsearchClient(settings);

如果实现 Serializer 就足够了,为什么我们必须提供一个封装在工厂 Func 中的实例?

在各种情况下,你可能有一个 POCO 类型,其中包含 Elastic.Clients.Elasticsearch 类型作为其属性之一。SourceSerializerFactory 委托提供对默认内置序列化器的访问权限,以便你在必要时可以访问它。例如,考虑你是否想使用 percolation;你需要将 Elasticsearch 查询存储为文档的 _source 的一部分,这意味着你需要一个看起来像这样的 POCO。

using Elastic.Clients.Elasticsearch.QueryDsl;

public class MyPercolationDocument
{
    public Query Query { get; set; }
    public string Category { get; set; }
}

自定义序列化器不知道如何序列化 Query 或其他可能作为文档 _source 一部分出现的 Elastic.Clients.Elasticsearch 类型。因此,你的自定义 Serializer 需要存储对我们内置序列化器的引用,并将 Elastic 类型的序列化委托回它。