Что эквивалентно в JToken.DeepEqual в System.Text. Json - PullRequest
1 голос
/ 07 марта 2020

Я хочу перенести свой код из Newtonsoft Json. Net в стандартную систему MicroSoft System.Text. Json. Но я не смог найти альтернативу для JToken.DeepEqual

В основном код должен сравнивать два JSON в модуле Test. Ссылка JSON и Результат JSON. Я использовал механизм в Newtonsoft для создания двух JObject и после сравнения с JToken.DeepEqual. Вот пример кода:

[TestMethod]
public void ExampleUnitTes()
{

    string resultJson = TestedUnit.TestedMethod();
    string referenceJson =
    @"
    {
      ...bla bla bla...
      ...some JSON Content...
      ...bla bla bla...
    }";

    JObject expected = ( JObject )JsonConvert.DeserializeObject( referenceJson );
    JObject result = ( JObject )JsonConvert.DeserializeObject( resultJson );
    Assert.IsTrue( JToken.DeepEquals( result, expected ) );
}

Если я прав в Newtonsoft JObject, похожем на System.Text.Json.JsonDocument, и я могу его создать, просто я не знаю, как сравнить содержимое it.

    System.Text.Json.JsonDocument expectedDoc = System.Text.Json.JsonDocument.Parse( referenceJson );
    System.Text.Json.JsonDocument resultDoc = System.Text.Json.JsonDocument.Parse( json );

    Compare???( expectedDoc, resulDoc );

Конечно, сравнение строк не является решением, потому что формат JSON не имеет значения, а порядок свойств также не имеет значения.

1 Ответ

1 голос
/ 09 марта 2020

Нет эквивалента в System.Text.Json по состоянию на Net 3.1, поэтому нам придется свернуть свой собственный. Вот один из возможных IEqualityComparer<JsonElement>:

public class JsonElementComparer : IEqualityComparer<JsonElement>
{
    public JsonElementComparer() : this(-1) { }

    public JsonElementComparer(int maxHashDepth) => this.MaxHashDepth = maxHashDepth;

    int MaxHashDepth { get; } = -1;

    #region IEqualityComparer<JsonElement> Members

    public bool Equals(JsonElement x, JsonElement y)
    {
        if (x.ValueKind != y.ValueKind)
            return false;
        switch (x.ValueKind)
        {
            case JsonValueKind.Null:
            case JsonValueKind.True:
            case JsonValueKind.False:
            case JsonValueKind.Undefined:
                return true;

            // Compare the raw values of numbers, and the text of strings.
            // Note this means that 0.0 will differ from 0.00 -- which may be correct as deserializing either to `decimal` will result in subtly different results.
            // Newtonsoft's JValue.Compare(JTokenType valueType, object? objA, object? objB) has logic for detecting "equivalent" values, 
            // you may want to examine it to see if anything there is required here.
            // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Linq/JValue.cs#L246
            case JsonValueKind.Number:
                return x.GetRawText() == y.GetRawText();

            case JsonValueKind.String:
                return x.GetString() == y.GetString(); // Do not use GetRawText() here, it does not automatically resolve JSON escape sequences to their corresponding characters.

            case JsonValueKind.Array:
                return x.EnumerateArray().SequenceEqual(y.EnumerateArray(), this);

            case JsonValueKind.Object:
                {
                    // Surprisingly, JsonDocument fully supports duplicate property names.
                    // I.e. it's perfectly happy to parse {"Value":"a", "Value" : "b"} and will store both
                    // key/value pairs inside the document!
                    // A close reading of https://tools.ietf.org/html/rfc8259#section-4 seems to indicate that
                    // such objects are allowed but not recommended, and when they arise, interpretation of 
                    // identically-named properties is order-dependent.  
                    // So stably sorting by name then comparing values seems the way to go.
                    var xPropertiesUnsorted = x.EnumerateObject().ToList();
                    var yPropertiesUnsorted = y.EnumerateObject().ToList();
                    if (xPropertiesUnsorted.Count != yPropertiesUnsorted.Count)
                        return false;
                    var xProperties = xPropertiesUnsorted.OrderBy(p => p.Name, StringComparer.Ordinal);
                    var yProperties = yPropertiesUnsorted.OrderBy(p => p.Name, StringComparer.Ordinal);
                    foreach (var (px, py) in xProperties.Zip(yProperties))
                    {
                        if (px.Name != py.Name)
                            return false;
                        if (!Equals(px.Value, py.Value))
                            return false;
                    }
                    return true;
                }

            default:
                throw new JsonException(string.Format("Unknown JsonValueKind {0}", x.ValueKind));
        }
    }

    public int GetHashCode(JsonElement obj)
    {
        var hash = new HashCode(); // New in .Net core: https://docs.microsoft.com/en-us/dotnet/api/system.hashcode
        ComputeHashCode(obj, ref hash, 0);
        return hash.ToHashCode();
    }

    void ComputeHashCode(JsonElement obj, ref HashCode hash, int depth)
    {
        hash.Add(obj.ValueKind);

        switch (obj.ValueKind)
        {
            case JsonValueKind.Null:
            case JsonValueKind.True:
            case JsonValueKind.False:
            case JsonValueKind.Undefined:
                break;

            case JsonValueKind.Number:
                hash.Add(obj.GetRawText());
                break;

            case JsonValueKind.String:
                hash.Add(obj.GetString());
                break;

            case JsonValueKind.Array:
                if (depth != MaxHashDepth)
                    foreach (var item in obj.EnumerateArray())
                        ComputeHashCode(item, ref hash, depth+1);
                else
                    hash.Add(obj.GetArrayLength());
                break;

            case JsonValueKind.Object:
                foreach (var property in obj.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
                {
                    hash.Add(property.Name);
                    if (depth != MaxHashDepth)
                        ComputeHashCode(property.Value, ref hash, depth+1);
                }
                break;

            default:
                throw new JsonException(string.Format("Unknown JsonValueKind {0}", obj.ValueKind));
        }            
    }

    #endregion
}

Используйте его следующим образом:

var comparer = new JsonElementComparer();
using var doc1 = System.Text.Json.JsonDocument.Parse(referenceJson);
using var doc2 = System.Text.Json.JsonDocument.Parse(resultJson);
Assert.IsTrue(comparer.Equals(doc1.RootElement, doc2.RootElement));

Примечания:

  • С Json. NET разрешает значения JSON с плавающей запятой в double или decimal во время синтаксического анализа, JToken.DeepEquals() считает значения с плавающей запятой, которые отличаются только конечными нулями, идентичными. Т.е. проходит следующее утверждение:

    Assert.IsTrue(JToken.DeepEquals(JToken.Parse("1.0"), JToken.Parse("1.00")));
    

    Мой компаратор не считает эти два равными. Я считаю это желательным, потому что приложения иногда хотят сохранить конечные нули, например, при десериализации до decimal, и, таким образом, это различие может иногда иметь значение. (Пример см., Например, Json. Net, не преобразующий десятичные дроби одинаковым образом дважды .) Если вы хотите считать такие значения JSON идентичными, вы будете необходимо изменить регистры для JsonValueKind.Number в ComputeHashCode() и Equals(JsonElement x, JsonElement y), чтобы обрезать концевые нули, если они присутствуют после десятичной точки. JsonDocument полностью поддерживает повторяющиеся имена свойств! Т.е. он совершенно счастлив разобрать {"Value":"a", "Value" : "b"} и сохранит обе пары ключ / значение внутри документа.

    При близком прочтении https://tools.ietf.org/html/rfc8259#section -4 , кажется, указывается, что такие объекты разрешены но не рекомендуется, и когда они возникают, интерпретация свойств с одинаковыми именами может зависеть от порядка. Я справился с этим, стабильно сортируя списки свойств по имени свойства, затем просматривая списки и сравнивая имена и значения. Если вам не нужны дубликаты имен свойств, вы, вероятно, могли бы повысить производительность, используя один словарь поиска вместо двух отсортированных списков.

  • JsonDocument является одноразовым, и фактически нуждается в этом. подлежит утилизации в соответствии с документами :

    Этот класс использует ресурсы из объединенной памяти, чтобы минимизировать влияние сборщика мусора (G C) в сценарии с высокой нагрузкой ios. Неправильная утилизация этого объекта приведет к тому, что память не будет возвращена в пул, что увеличит влияние G C на различные части платформы.

    В вашем вопросе вы этого не делаете , но вы должны.

Демонстрационная скрипка здесь .

...