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

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,可以实现你自己的序列化器。示例如下:

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 的构造函数中进行指定的,如下:

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),如下:

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 的方式,则此包也很有用。

示例:

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

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

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方法,如下:

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。

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

模型

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

注入序列化器:

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 中

var document = new MyDocument
{
    Id = 1,
    Name = "My first document",
    OwnerId = 2
};

var ndexResponse = client.IndexDocument(document);

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

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

序列化 Type 信息

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

定义序列化器

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

注入自定义序列化器

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 中


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);

序列化结果:

{
  "$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 类型的解析器的行为;这样做会导致意外的行为。