question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

ElasticClient.Search is not respecting SourceSerializer on results

See original GitHub issue

NEST/Elasticsearch.Net version: v6.0.2 NEST/JsonNetSerializer: v6.0.2

Elasticsearch version: 6.2.2 (Docker container)

Description of the problem including expected versus actual behavior: Using a custom SourceSerializer, specified via the ConnectionSettings, to control DateTimeOffset serialization. The test object contains two DateTimeOffset fields. One field contains a value with offset -0400 (local) while the other an offset of +0000 (utc). After retrieving the records via Search, the ISearchResponse<>.Documents show that both DateTimeOffset values have been adjusted to local time.

Adding EnableDebugMode to the ConnnectionSettings and inspecting ApiCall.ResponseBodyInBytes shows that the json correctly specifies the offset values for both fields.

Further, by using ElasticClient.SourceSerializer.SerializeToString and ElasticClient.SourceSerializer.Deserialize directly, both fields are converted correctly.

Based on this, it appears that Search is not using the SourceDeserializer or not using the injected JsonSerializerSettings

Steps to reproduce:

  1. Create a custom serializer that preserves the offset value in DateTimeOffset
public sealed class ElasticSearchSerializer : ConnectionSettingsAwareSerializerBase
    {
        public ElasticSearchSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings)
            : base(builtinSerializer, connectionSettings)
        {
        }

        protected override JsonSerializerSettings CreateJsonSerializerSettings()
        {
            return new JsonSerializerSettings()
            {
                DateFormatHandling = DateFormatHandling.IsoDateFormat,
                DateParseHandling = DateParseHandling.DateTimeOffset,
                DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind,
                Formatting = Formatting.Indented
            }
        }

        protected override void ModifyContractResolver(ConnectionSettingsAwareContractResolver resolver)
        {
            resolver.NamingStrategy = new CamelCaseNamingStrategy();
        }
    }
  1. Setup ConnectionSettings & Client
            ConnectionSettings connectionSettings = new ConnectionSettings(pool, new HttpConnection(), (builtin, values) => new ElasticSearchSerializer(builtin, values));
            connectionSettings.DefaultMappingFor(typeof(TestEntity), desc => desc.IndexName("entity")).EnableDebugMode();
            ElasticClient client = new ElasticClient(connectionSettings);
  1. Setup a TestEntity
        private class TestEntity
        {
            public Guid Uuid { get; set; } = new Guid("12345678-aaaa-bbbb-cccc-ddddeeeeffff");

            public string Str { get; set; } = "Hello World";

            public int Int { get; set; } = 2000;

            public double Dbl { get; set; } = 3.1415;

            public bool TrueFalse { get; set; } = true;

            public DateTime DtLocal { get; set; } = new DateTime(2018, 2, 1, 15, 0, 0, DateTimeKind.Local);

            public DateTime DtUtc { get; set; } = new DateTime(2018, 4, 1, 0, 0, 0, DateTimeKind.Utc);

            public DateTime DtUnspec { get; set; } = new DateTime(2018, 6, 1, 15, 0, 0, DateTimeKind.Unspecified);

            public DateTimeOffset DtoLocal { get; set; } = new DateTime(2018, 8, 1, 15, 0, 0, DateTimeKind.Local);

            public DateTimeOffset DtoUtc { get; set; } = new DateTime(2018, 10, 1, 0, 0, 0, DateTimeKind.Utc);
        }
  1. Index and Search for the entity
            client.Index(entity, descriptor => descriptor.Id(entity.Uuid.ToString("N")).Index("entity"));
            ISearchResponse<TestEntity> result = client.Search<TestEntity>(s => s.Query(q => q.MatchAll()));
  1. Inspect the ‘result’ object. Look at the result.ApiCall.ResponseBodyInBytes & result.Documents. Notice the offset value in the json and what is shown in the document. The dtoUtc will logically have the correct value but will have been converted to the local offset.

  2. Test the serializer directly. Notice that the offset remains correct.

            string json = client.SourceSerializer.SerializeToString(entity);
            TestEntity deserialized;
            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
            {
                deserialized = serializer.Deserialize<T>(stream);
            }

For anyone else encountering this issue, it can be worked around by using EnableDebugMode and manually processing response json.

private IEnumerable<T> ManualDeserialize(ISearchResponse<T> resp)
        {
            if (!resp.IsValid)
                return null;

            if (resp.ApiCall?.ResponseBodyInBytes == null)
                throw new Exception("ElasticSearch connection settings must include 'EnableDebugMode' or the json serialization workaround fails");

            // Get the raw response JSON
            string respJson = Encoding.UTF8.GetString(resp.ApiCall?.ResponseBodyInBytes);

            // Parse it generically
            JObject rootObj = (JObject)JsonConvert.DeserializeObject(respJson, {see above for JsonSerializerSettings});

            // NOTE: This is fragile; json format may change (Assumes Newtonsoft.JSON 11.0.1)
            // Check for any matches
            int hitCount = (int)rootObj.SelectToken("$.hits.total");
            TDto[] dtos = new TDto[hitCount];

            // Process each 'hit' individually due to the actual json being nested down a few levels
            for (int i = 0; i < hitCount; ++i)
            {
                // Grab json for a result and deserialized
                string hitJson = rootObj.SelectToken($"$.hits.hits[{i}]._source").ToString();
                dtos[i] = JsonConvert.DeserializeObject<T>(hitJson,  {see above for JsonSerializerSettings});
            }

            return dtos;
        }

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
russcamcommented, Jun 7, 2018
0reactions
WereDevcommented, Jun 7, 2018

Thank you!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Custom Serialization | Elasticsearch.Net and NEST
A custom serializer would not know how to serialize QueryContainer or other NEST types that could appear as part of the _source of...
Read more >
Source serialization | Elasticsearch .NET Client [8.9]
A source serializer implementation handles serialization, with the default implementation using the System.Text.Json library. As a result, you may use ...
Read more >
nest - ElasticSearch.Net does not recognize the timestamp ...
The first time, I thought it was my object that was wrongly indexed and simply excluded the result but I got the same...
Read more >
Newest 'elasticsearch-net' Questions
I'm trying to search using the ElasticClient.Search method but no matter what terms I set, or field I search by, I always get...
Read more >
The Ultimate Guide to Using Elasticsearch in Node.js
Learn how to set up a secure Node.js API with search capabilities using Express.js and Elasticsearch. Then secure the API with Okta so...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found