源序列化编辑

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

使用类型对文档建模编辑

Elasticsearch 提供对发送和索引的文档的搜索和聚合功能。这些文档作为 HTTP 请求的请求主体中的 JSON 对象发送。在 Elasticsearch .NET 客户端中使用 POCO(普通旧 CLR 对象 对文档建模是自然而然的。

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

默认行为编辑

默认行为是将类型属性名称序列化为驼峰式 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 将优先考虑添加到 JsonSerializerOptions 上的 Converters 集合中的转换器,而不是应用于 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");

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

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

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

创建自定义 Serializeredit

假设您更喜欢为源类型使用替代 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 委托提供了对默认内置序列化器的访问,因此您可以在需要时访问它。例如,如果您想使用渗透,您需要将 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 类型的序列化委托给它。