< Summary

Class:Azure.Search.Documents.JsonSerialization
Assembly:Azure.Search.Documents
File(s):C:\Git\azure-sdk-for-net\sdk\search\Azure.Search.Documents\src\Serialization\JsonSerialization.cs
Covered lines:97
Uncovered lines:26
Coverable lines:123
Total lines:389
Line coverage:78.8% (97 of 123)
Covered branches:69
Total branches:98
Branch coverage:70.4% (69 of 98)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
get_SerializerOptions()-100%100%
.cctor()-100%100%
AddSearchConverters(...)-100%50%
Float(...)-100%100%
Double(...)-100%100%
Date(...)-100%50%
Date(...)-100%100%
ToStream(...)-100%100%
GetSearchObject(...)-46.15%48.15%
ReadSearchDocument(...)-93.33%87.5%
WriteSearchDocument(...)-100%100%
DeserializeAsync()-76.47%50%

File(s)

C:\Git\azure-sdk-for-net\sdk\search\Azure.Search.Documents\src\Serialization\JsonSerialization.cs

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.IO;
 8using System.Text;
 9using System.Text.Json;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using Azure.Core.Pipeline;
 13#if EXPERIMENTAL_SERIALIZER
 14using Azure.Core.Serialization;
 15#endif
 16#if EXPERIMENTAL_SPATIAL
 17using Azure.Core.Spatial;
 18#endif
 19using Azure.Search.Documents.Models;
 20
 21namespace Azure.Search.Documents
 22{
 23    /// <summary>
 24    /// JSON serialization and conversion helpers.
 25    /// </summary>
 26    internal static class JsonSerialization
 27    {
 28        /// <summary>
 29        /// Default JsonSerializerOptions to use.
 30        /// </summary>
 25231        public static JsonSerializerOptions SerializerOptions { get; } =
 132            new JsonSerializerOptions().AddSearchConverters();
 33
 34        /// <summary>
 35        /// Add all of the Search JsonConverters.
 36        /// </summary>
 37        /// <param name="options">Serialization options.</param>
 38        /// <returns>Serialization options.</returns>
 39        public static JsonSerializerOptions AddSearchConverters(this JsonSerializerOptions options)
 40        {
 141            options ??= new JsonSerializerOptions();
 142            options.Converters.Add(SearchDoubleConverter.Shared);
 143            options.Converters.Add(SearchDateTimeOffsetConverter.Shared);
 144            options.Converters.Add(SearchDateTimeConverter.Shared);
 145            options.Converters.Add(SearchDocumentConverter.Shared);
 46#if EXPERIMENTAL_SPATIAL
 47            options.Converters.Add(new GeometryJsonConverter());
 48#endif
 149            return options;
 50        }
 51
 52        /// <summary>
 53        /// Format floating point values.
 54        /// </summary>
 55        /// <param name="value">Float.</param>
 56        /// <param name="formatProvider">Format Provider.</param>
 57        /// <returns>OData string.</returns>
 58        public static string Float(float value, IFormatProvider formatProvider) =>
 859            value switch
 860            {
 961                float.NegativeInfinity => Constants.NegativeInfValue,
 962                float.PositiveInfinity => Constants.InfValue,
 1563                float x when float.IsNaN(x) => Constants.NanValue,
 1364                float x => x.ToString(formatProvider).ToLowerInvariant()
 865            };
 66
 67        /// <summary>
 68        /// Format floating point values.
 69        /// </summary>
 70        /// <param name="value">Double.</param>
 71        /// <param name="formatProvider">Format Provider.</param>
 72        /// <returns>OData string.</returns>
 73        public static string Double(double value, IFormatProvider formatProvider) =>
 874            value switch
 875            {
 976                double.NegativeInfinity => Constants.NegativeInfValue,
 977                double.PositiveInfinity => Constants.InfValue,
 1578                double x when double.IsNaN(x) => Constants.NanValue,
 1379                double x => x.ToString(formatProvider).ToLowerInvariant()
 880            };
 81
 82        /// <summary>
 83        /// Format dates.
 84        /// </summary>
 85        /// <param name="value">Date</param>
 86        /// <param name="formatProvider">Format Provider.</param>
 87        /// <returns>OData string.</returns>
 88        public static string Date(DateTime value, IFormatProvider formatProvider) =>
 189            Date(
 190                value.Kind == DateTimeKind.Unspecified ?
 191                    new DateTimeOffset(value, TimeSpan.Zero) :
 192                    new DateTimeOffset(value),
 193                formatProvider);
 94
 95        /// <summary>
 96        /// Format dates.
 97        /// </summary>
 98        /// <param name="value">Date</param>
 99        /// <param name="formatProvider">Format Provider.</param>
 100        /// <returns>OData string.</returns>
 101        public static string Date(DateTimeOffset value, IFormatProvider formatProvider) =>
 30102            value.ToString("o", formatProvider);
 103
 104        /// <summary>
 105        /// Get a stream representation of a JsonElement.  This is an
 106        /// inefficient hack to let us rip out nested sub-documents
 107        /// representing different model types and pass them to
 108        /// ObjectSerializer.
 109        /// </summary>
 110        /// <param name="element">The JsonElement.</param>
 111        /// <returns>The JsonElement's content wrapped in a Stream.</returns>
 112        public static Stream ToStream(this JsonElement element) =>
 10595113            new MemoryStream(
 10595114                Encoding.UTF8.GetBytes(
 10595115                    element.GetRawText()));
 116
 117        /// <summary>
 118        /// Convert a JSON value into a .NET object relative to Search's EDM
 119        /// types.
 120        /// </summary>
 121        /// <param name="element">The JSON element.</param>
 122        /// <returns>A corresponding .NET value.</returns>
 123        public static object GetSearchObject(this JsonElement element)
 124        {
 70125            switch (element.ValueKind)
 126            {
 127                case JsonValueKind.String:
 42128                    return element.GetString() switch
 42129                    {
 0130                        Constants.NanValue => double.NaN,
 0131                        Constants.InfValue => double.PositiveInfinity,
 0132                        Constants.NegativeInfValue => double.NegativeInfinity,
 42133                        string text =>
 84134                            DateTimeOffset.TryParse(text, out DateTimeOffset date) ?
 84135                                (object)date :
 84136                                (object)text
 42137                    };
 138                case JsonValueKind.Number:
 28139                    if (element.TryGetInt32(out int intValue)) { return intValue; }
 0140                    if (element.TryGetInt64(out long longValue)) { return longValue; }
 20141                    return element.GetDouble();
 142                case JsonValueKind.True:
 2143                    return true;
 144                case JsonValueKind.False:
 2145                    return false;
 146                case JsonValueKind.Undefined:
 147                case JsonValueKind.Null:
 0148                    return null;
 149                case JsonValueKind.Object:
 0150                    var dictionary = new Dictionary<string, object>();
 0151                    foreach (JsonProperty jsonProperty in element.EnumerateObject())
 152                    {
 0153                        dictionary.Add(jsonProperty.Name, jsonProperty.Value.GetSearchObject());
 154                    }
 155#if EXPERIMENTAL_SPATIAL
 156                    // Check if we've got a Point instead of a complex type
 157                    if (dictionary.TryGetValue("type", out object type) &&
 158                        type is string typeName &&
 159                        string.Equals(typeName, "Point", StringComparison.Ordinal) &&
 160                        dictionary.TryGetValue("coordinates", out object coordArray) &&
 161                        coordArray is double[] coords &&
 162                        (coords.Length == 2 || coords.Length == 3))
 163                    {
 164                        double longitude = coords[0];
 165                        double latitude = coords[1];
 166                        double? altitude = coords.Length == 3 ? (double?)coords[2] : null;
 167                        // TODO: Should we also pull in other PointGeometry properties?
 168                        return new PointGeometry(new GeometryPosition(longitude, latitude, altitude));
 169                    }
 170#endif
 0171                    return dictionary;
 172                case JsonValueKind.Array:
 0173                    var list = new List<object>();
 0174                    foreach (JsonElement item in element.EnumerateArray())
 175                    {
 0176                        list.Add(item.GetSearchObject());
 177                    }
 0178                    return list.ToArray();
 179                default:
 0180                    throw new NotSupportedException("Not supported value kind " + element.ValueKind);
 181            }
 182        }
 183
 184        /// <summary>
 185        /// Parse JSON into a SearchDocument.
 186        /// </summary>
 187        /// <param name="reader">The JSON reader.</param>
 188        /// <param name="typeToConvert">The type to convert to.</param>
 189        /// <param name="options">Serialization options.</param>
 190        /// <param name="recursionDepth">
 191        /// Depth of the current read recursion to bail out of circular
 192        /// references.
 193        /// </param>
 194        /// <returns>A deserialized SearchDocument.</returns>
 195        public static SearchDocument ReadSearchDocument(
 196            ref Utf8JsonReader reader,
 197            Type typeToConvert,
 198            JsonSerializerOptions options,
 199            int? recursionDepth = null)
 200        {
 201            Debug.Assert(typeToConvert != null);
 202            Debug.Assert(typeToConvert.IsAssignableFrom(typeof(SearchDocument)));
 203
 9020204            recursionDepth ??= options?.MaxDepth ?? Constants.MaxJsonRecursionDepth;
 9020205            if (!recursionDepth.HasValue || recursionDepth.Value <= 0)
 206            {
 207                // JsonSerializerOptions uses 0 to mean pick their default of 64
 8748208                recursionDepth = Constants.MaxJsonRecursionDepth;
 209            }
 0210            if (recursionDepth.Value < 0) { throw new JsonException("Exceeded maximum recursion depth."); }
 211
 9020212            SearchDocument doc = new SearchDocument();
 9020213            Expects(reader, JsonTokenType.StartObject);
 27277214            while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
 215            {
 18321216                Expects(reader, JsonTokenType.PropertyName);
 18321217                string propertyName = reader.GetString();
 18321218                reader.Read(); // Advance the past the property name
 219
 220                // Ignore OData properties - we don't expose those on custom
 221                // user schemas
 18320222                if (!propertyName.StartsWith(Constants.ODataKeyPrefix, StringComparison.OrdinalIgnoreCase))
 223                {
 9953224                    object value = ReadSearchDocObject(ref reader, recursionDepth.Value - 1);
 9890225                    doc[propertyName] = value;
 226                }
 227                else
 228                {
 8367229                    reader.Skip();
 230                }
 231            }
 8956232            return doc;
 233
 234            object ReadSearchDocObject(ref Utf8JsonReader reader, int depth)
 235            {
 0236                if (depth < 0) { throw new JsonException("Exceeded maximum recursion depth."); }
 10341237                switch (reader.TokenType)
 238                {
 239                    case JsonTokenType.String:
 8949240                        return reader.GetString();
 241                    case JsonTokenType.Number:
 872242                        if (reader.TryGetInt32(out int intValue)) { return intValue; }
 158243                        if (reader.TryGetInt64(out long longValue)) { return longValue; }
 118244                        return reader.GetDouble();
 245                    case JsonTokenType.True:
 65246                        return true;
 247                    case JsonTokenType.False:
 62248                        return false;
 249                    case JsonTokenType.None:
 250                    case JsonTokenType.Null:
 252251                        return null;
 252                    case JsonTokenType.StartObject:
 253#if EXPERIMENTAL_SPATIAL
 254                        // Clone the reader so we can check if the object is a
 255                        // GeoJsonPoint without advancing tokens if not
 256                        Utf8JsonReader clone = reader;
 257                        try
 258                        {
 259                            GeometryJsonConverter converter = new GeometryJsonConverter();
 260                            PointGeometry point = converter.Read(ref clone, typeof(PointGeometry), options) as PointGeom
 261                            if (point != null)
 262                            {
 263                                reader = clone;
 264                                return point;
 265                            }
 266                        }
 267                        catch
 268                        {
 269                        }
 270#endif
 271
 272                        // Return a complex object
 274273                        return ReadSearchDocument(ref reader, typeof(SearchDocument), options, depth - 1);
 274                    case JsonTokenType.StartArray:
 234275                        var list = new List<object>();
 622276                        while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
 277                        {
 388278                            object value = ReadSearchDocObject(ref reader, depth - 1);
 388279                            list.Add(value);
 280                        }
 234281                        return list.ToArray();
 282                    default:
 0283                        throw new JsonException();
 284                }
 285            }
 286
 287            static void Expects(in Utf8JsonReader reader, JsonTokenType expected)
 288            {
 27341289                if (reader.TokenType != expected)
 290                {
 0291                    throw new JsonException(
 0292                        $"Expected {nameof(JsonTokenType)} " +
 0293                        expected.ToString() +
 0294                        ", not " +
 0295                        reader.TokenType.ToString());
 296                }
 27341297            }
 298        }
 299
 300        /// <summary>
 301        /// Serialize a SearchDocument as JSON.
 302        /// </summary>
 303        /// <param name="writer">JSON writer.</param>
 304        /// <param name="document">The document.</param>
 305        /// <param name="options">Serialization options.</param>
 306        public static void WriteSearchDocument(
 307            Utf8JsonWriter writer,
 308            SearchDocument document,
 309            JsonSerializerOptions options)
 310        {
 311            Debug.Assert(writer != null);
 312            Debug.Assert(document != null);
 313            Debug.Assert(options != null);
 314
 16670315            writer.WriteStartObject();
 67420316            foreach (string key in document.Keys)
 317            {
 17040318                writer.WritePropertyName(key);
 17040319                object value = document[key];
 320
 321                // Write the value using JsonSerializer so all of our
 322                // converters take effect
 17040323                JsonSerializer.Serialize(
 17040324                    writer,
 17040325                    value,
 17040326                    value?.GetType() ?? typeof(object),
 17040327                    options);
 328            }
 16670329            writer.WriteEndObject();
 16670330        }
 331
 332        #pragma warning disable CS1572 // Not all parameters will be used depending on feature flags
 333        /// <summary>
 334        /// Deserialize a JSON stream.
 335        /// </summary>
 336        /// <typeparam name="T">
 337        /// The target type to deserialize the JSON stream into.
 338        /// </typeparam>
 339        /// <param name="json">A JSON stream.</param>
 340        /// <param name="serializer">
 341        /// Optional serializer that can be used to customize the serialization
 342        /// of strongly typed models.
 343        /// </param>
 344        /// <param name="async">Whether to execute sync or async.</param>
 345        /// <param name="cancellationToken">
 346        /// Optional <see cref="CancellationToken"/> to propagate notifications
 347        /// that the operation should be canceled.
 348        /// </param>
 349        /// <returns>A deserialized object.</returns>
 350        public static async Task<T> DeserializeAsync<T>(
 351            this Stream json,
 352#if EXPERIMENTAL_SERIALIZER
 353            ObjectSerializer serializer,
 354#endif
 355            bool async,
 356            CancellationToken cancellationToken)
 357        #pragma warning restore CS1572
 358        {
 51359            if (json is null)
 360            {
 0361                return default;
 362            }
 363#if EXPERIMENTAL_SERIALIZER
 51364            else if (serializer != null)
 365            {
 0366                return async ?
 0367                    (T)await serializer.DeserializeAsync(json, typeof(T), cancellationToken).ConfigureAwait(false) :
 0368                    (T)serializer.Deserialize(json, typeof(T), cancellationToken);
 369            }
 370#endif
 51371            else if (async)
 372            {
 25373                return await JsonSerializer.DeserializeAsync<T>(
 25374                    json,
 25375                    JsonSerialization.SerializerOptions,
 25376                    cancellationToken)
 25377                    .ConfigureAwait(false);
 378            }
 379            else
 380            {
 381                // Copy the stream into memory and then deserialize
 26382                using MemoryStream memory = json.CopyToMemoryStreamAsync(async, cancellationToken).EnsureCompleted();
 26383                return JsonSerializer.Deserialize<T>(
 26384                    memory.ToArray(),
 26385                    JsonSerialization.SerializerOptions);
 386            }
 51387        }
 388    }
 389}