Elasticsearch.Nest 教程系列 3-1 序列化:Custom Serialization | 自定义序列化

  • 本系列博文是“伪”官方文档翻译(更加本土化),并非完全将官方文档进行翻译,而是在查阅、测试原始文档并转换为自己真知灼见后的“准”翻译。有不同见解 / 说明不周的地方,还请海涵、不吝拍砖 :)

  • 官方文档见此:https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html

  • 本系列对应的版本环境:ElasticSearch@7.3.1,NEST@7.3.1,IDE 和开发平台默认为 VS2019,.NET CORE 2.1

NEST 的默认 JSON 序列化知道如何正确的序列化所有请求和响应类型,以及如何正确处理的你的 .NET 模型类。然而有的时候,你可能需要修改默认行为或者自定义你自己的序列化器,本章节将说明如何自定义以及扩展 NEST 类型。


目前 NEST 客户端已经完全移除了 SimpleJson 和 Newtonsoft.Json,转而使用内置的 Utf8Json—一种直接与 UTF-8 二进制文件直接协同工作的快速序列化器。

随着转而使用 UtfJson,NEST 团队在 7.x 阶段删除了部分原先在之前 NEST 客户端中存在的 JSON 特性,变动如下:

  • 由于性能原因,由 Utf8Json 生成的 JSON 不会缩进。如请求中的 JSON 不会缩进,哪怕指定了 SerializationFormatting.Indented。但 NEST 团队正在考虑公开缩进 JSON 的选项以便于开发和调试。

  • NEST 类型不能通过继承扩展。在 6.x 版本,可以通过派生该类型并注释这些新属性来为该类型包括其他属性。在当前使用 Utf8Json 进行序列化的时候,此方法将不起作用。

  • 序列化器使用 Reflection.Emit,而 Utf8Json 使用 Reflection.Emit 来生成高效的格式化器。但并非所有平台都支持 Reflection.Emit,例如 UWP,Xamarin.iOS 和 Xamarin.Android。

  • Elasticsearch.Net.DynamicResponse 将 JSON 数组反序列化为 List。SimpleJson 将 JSON 数组反序列化为 object[]。但出于分配和性能方面的原因,Utf8Json 将它们反序列化为 List

  • 将 JSON 对象字段名称反序列化为 C# 类的属性时,Utf8Json 更加严格。在 6.x 版本中,内部使用 Json.NET 进行序列化,JSON 对象字段名称会先跟 C# 的类属性名完全匹配的进行匹配,之后再使用不区分大小写的进行匹配。但在 7.x 版本中(使用 Utf8Json 后),名称必须完全匹配。

  • 注入新的序列化器

    通过注入新的序列化器,可以让你在_source,_fields 或希望写入和返回用户提供的值的任何地方对(反)序列化进行调用。

    在 Nest 中,称这种序列化器为 SourceSerializer。

    另外,在 Nest 中,还存在另外一个被称为 RequestResponseSerializer 的序列化器。该序列化器是内部用的,主要负责 Nest 库自身的一些请求和响应类型。

    如果没有配置 SourceSerializer,那么内部使用的也是 SourceSerializer。

    通过实现 IElasticsearchSerializer,可以实现你自己的序列化器。示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class VanillaSerializer : IElasticsearchSerializer
    {
    public T Deserialize<T>(Stream stream) => throw new NotImplementedException();

    public object Deserialize(Type type, Stream stream) => throw new NotImplementedException();

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

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

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

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

    使用自定义的序列化器是在 ConnectionSettings 的构造函数中进行指定的,如下:

    1
    2
    3
    4
    5
    var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
    var connectionSettings = new ConnectionSettings(
    pool,
    sourceSerializer: (builtin, settings) => new VanillaSerializer());
    var client = new ElasticClient(connectionSettings);

    注入序列化器使用委托的原因:

    • 有的时候,你的 POCO 类由于某些原因,需要内嵌 NEST 类型(如你可能需要用到过滤,这个时候你希望获取文档中的 _source),如下:

    1
    2
    3
    4
    5
    public class MyPercolationDocument
    {
    public QueryContainer Query { get; set; }
    public string Category { get; set; }
    }
    • 诸如 QueryContainer 或者其他 NEST 的类型有可能会作为文档的 _source 的一部分,而自定义的序列化器并不知道如何处理这些 NEST 类型,因此自定义序列化器需要将 NEST 类型的序列化过程委派给 NEST 内置的序列化器,通过委托,就可以应对这种场景。

    JsonNetSerializer

    NEST 团队提供了一个单独的 NEST.JsonNetSerializer 包,该包有助于使用 Json.NET 组成自定义 SourceSerializer,该包可以将已知 NEST 类型的序列化委派回内置的 RequestResponseSerializer。如果要控制使用 Json.NET 从 Elasticsearch 存储和检索文档及值的方式,而又不干扰 NEST 内部使用 Json.NET 的方式,则此包也很有用。

    示例:

    1
    2
    3
    4
    var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
    var connectionSettings =
    new ConnectionSettings(pool, sourceSerializer: JsonNetSerializer.Default);
    var client = new ElasticClient(connectionSettings);

    JsonNetSerializer.Default 是语法糖,实际为:

    1
    2
    3
    4
    5
    var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
    var connectionSettings = new ConnectionSettings(
    pool,
    sourceSerializer: (builtin, settings) => new JsonNetSerializer(builtin, settings));
    var client = new ElasticClient(connectionSettings);

    Serializers 派生类

    如果你想要更加显式自定义,则可以继承 ConnectionSettingsAwareSerializerBase 并重写CreateJsonSerializerSettings 和 ModifyContractResolver方法,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class MyFirstCustomJsonNetSerializer : ConnectionSettingsAwareSerializerBase
    {
    public MyFirstCustomJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
    : base(builtinSerializer, connectionSettings) { }

    protected override JsonSerializerSettings CreateJsonSerializerSettings() =>
    new JsonSerializerSettings
    {
    NullValueHandling = NullValueHandling.Include
    };

    protected override void ModifyContractResolver(ConnectionSettingsAwareContractResolver resolver) =>
    resolver.NamingStrategy = new SnakeCaseNamingStrategy();
    }
    • 这里显式指定了 Json.Net 的命名策略为 SnakeCaseNamingStrategy。

    • JsonSerializerSettings 包含 null 属性

    • 不影响 NEST 自己的类型如何序列化。

    • 此外,由于此序列化器知道内置的序列化器,因此我们可以自动注入JsonConverter来处理可能出现在源中的已知 NEST 类型,如上面的 QueryContainer。

    示例:通过文档类型来说明

    模型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MyDocument
    {
    public int Id { get; set; }

    public string Name { get; set; }

    public string FilePath { get; set; }

    public int OwnerId { get; set; }

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

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

    注入序列化器:

    1
    2
    3
    4
    5
    6
    7
    8
    var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
    var connectionSettings = new ConnectionSettings(
    pool,
    connection: new InMemoryConnection(),
    sourceSerializer: (builtin, settings) => new MyFirstCustomJsonNetSerializer(builtin, settings))
    .DefaultIndex("my-index");

    var client = new ElasticClient(connectionSettings);

    索引一份文档到 ES 中

    1
    2
    3
    4
    5
    6
    7
    8
    var document = new MyDocument
    {
    Id = 1,
    Name = "My first document",
    OwnerId = 2
    };

    var ndexResponse = client.IndexDocument(document);

    以上文档将会被序列化为:

    1
    2
    3
    4
    5
    6
    7
    {
    "id": 1,
    "name": "My first document",
    "file_path": null,
    "owner_id": 2,
    "sub_documents": null
    }
    • 符合 MyFirstCustomJsonNetSerializer 中设定的数据.

    序列化 Type 信息

    这是另一个自定义序列化器的例子,自定义的解析器将在文档的序列化 JSON 中包含类型名称,当返回的集合中包含协变类型的时候,将会很有用。

    定义序列化器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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);
    }
    • 重写 resolver

    注入自定义序列化器

    1
    2
    3
    4
    5
    6
    7
    8
    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);

    添加以下文档到 ES 中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    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 ndexResponse = client.IndexDocument(document);

    序列化结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "$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"
    }
    ]
    }
    • 类型信息将针对外部 MyDocument 实例进行序列化,但不会针对 SubDocuments 集合中的每个MySubDocument 实例进行序列化。

    在实现派生自 ConnectionSettingsAwareContractResolver 的自定义解析器时,请注意不要更改 NEST 类型的解析器的行为;这样做会导致意外的行为。