【问题标题】:Newtonsoft JSON PreserveReferencesHandling with custom Equals usageNewtonsoft JSON PreserveReferencesHandling 与自定义 Equals 用法
【发布时间】:2018-10-12 13:40:56
【问题描述】:

我目前在使用 Newtonsoft Json 时遇到了一些问题。

我想要的很简单:将要序列化的对象与所有属性和子属性进行相等性比较。

我现在尝试创建自己的 EqualityComparer,但它只与父对象的属性进行比较。

另外,我尝试编写自己的 ReferenceResolver,但没有成功。

让我们用一个例子来谈谈:

public class EntityA
{
    int Foo {get; set;}

    public override bool Equals(object obj)
    {
        return (obj is EntityA other) && other.Foo == this.Foo;
    }
}

public class EntityB
{
    int Bar {get; set;}

    EntityA Parent {get; set;}

    public override bool Equals(object obj)
    {
        return (obj is EntityB other) && other.Bar == this.Bar;
    }
}

public class InnerWrapper
{
    public string FooBar {get; set;}

    public EntityB BEntity {get; set;}
}

public class OuterClass
{
    public EntityA AEntity { get; set;}

    List<InnerWrapper> InnerElements {get; set;}
}    

现在我想要的是从 EntityB 到 EntityA 的引用。在我的情况下,它们总是相同的。所以我期望的是,在每个 EntityB 的 JSON 中,对 EntityA 的引用都写为 ref。实体的相等覆盖相等以检查它们是否相同。它们是数据库对象,因此只要它们的 ID 相同,它们就相等。在这种情况下,我称它们为 FooBar

我尝试过的如下:

public class MyEqualComparer : IEqualityComparer
{
    public bool Equals(object x, object y)
    {
        return x.Equals(y);
    }

    public int GetHashCode(object obj)
    {
        return obj.GetHashCode();
    }
}

使用以下 JSON 设置

public static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All,
    NullValueHandling = NullValueHandling.Ignore,
    FloatParseHandling = FloatParseHandling.Decimal,
    Formatting = Formatting.Indented,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    EqualityComparer = new MyEqualComparer(),
    ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
    Error = (sender, args) => Log.Error(args.ErrorContext.Error, $"Error while (de)serializing: {args.ErrorContext}; object: {args.CurrentObject}")
};

但它不起作用。它比较完全错误的值。例如来自OuterClass 的EntityA 与InnerWrapper 中的每一个。但不是与属性甚至子属性(在本例中为InnerWrapperEntityB 的属性)。

使用自定义的 ReferenceResolver,我也没有运气,因为上面的设置非常通用,我不知道如何编写通用的。

您知道如何完成这项工作吗?

// 编辑:

下面是我所期望的示例:

{
    "$id" : "1",
    "AEntity": {
        "$id": "2",
        "Foo": 200
    },
    "InnerElements": [
        {
            "$id": "3",
            "Bar": 20,
            "Parent": {
                "$ref" : "2"
            }
        },
        {
            "$id": "4",
            "Bar": 21,
            "Parent": {
                "$ref" : "2"
            }
        },
        {
            "$id": "5",
            "Bar": 23,
            "Parent": {
                "$ref" : "2"
            }
        },
        {
            "$id": "6",
            "Bar": 24,
            "Parent": {
                "$ref" : "2"
            }
        },
        {
            "$id": "7",
            "Bar": 25,
            "Parent": {
                "$ref" : "2"
            }
        }
    ]

}

这就是我得到的:

    {
    "$id" : "1",
    "AEntity": {
        "$id": "2",
        "Foo": 200
    },
    "InnerElements": [
        {
            "$id": "3",
            "Bar": 20,
            "Parent": {
                "$id": "8",
                "Foo": 200
            }
        },
        {
            "$id": "4",
            "Bar": 21,
            "Parent": {
                "$id": "9",
                "Foo": 200
            }
        },
        {
            "$id": "5",
            "Bar": 23,
            "Parent": {
                "$id": "10",
                "Foo": 200
            }
        },
        {
            "$id": "6",
            "Bar": 24,
            "Parent": {
                "$id": "11",
                "Foo": 200
            }
        },
        {
            "$id": "7",
            "Bar": 25,
            "Parent": {
                "$id": "12",
                "Foo": 200
            }
        }
    ]

}

当然,在这种情况下,影响很小。但我的真实场景要大得多。

【问题讨论】:

  • 简单来说就是比较两个json对象的属性和子属性。对吗?
  • 不是两个 JSON 对象。在一个 JSON 对象中,另一个元素中有 EntityA。但是我在 OuterElement 的列表中有大约 thound 个条目。这些都包含对 OuterElement 的 EntityA 的引用。为了减小序列化 JSON 文件的大小,我希望引用外部元素。我将添加一个示例。
  • this answerJSON.NET Serialization - How does DefaultReferenceResolver compare equality? 是否解释了编写自己的自定义引用解析器需要做什么? JsonSerializerSettings.EqualityComparer 用于参考循环检测,请参阅Why doesn't reference loop detection use reference equality?
  • 您好,感谢您的回答。我已经看过了。但问题就在那里,我不知道 T 的类型。因为(正如您在上面的简单类型中看到的)我有不同的类型。它应该始终采用 a.Equals(b)。所以这应该在对象基础上工作。我也用一个对象尝试过这个例子,但是一旦我有了结构,它就不再起作用了。
  • @CodeRain - 在这种情况下,您能否请edit 分享您迄今为止尝试过的问题,最好是您的自定义参考解析器的正在进行中的工作?如果您可以提供minimal reproducible example 显示您遇到的问题,您更有可能获得有用的答案。

标签: c# json.net


【解决方案1】:

正如Andrew Whitakerthis answerJSON.NET Serialization - How does DefaultReferenceResolver compare equality? 中所述,Json.NET 在通过PreserveReferencesHandling 保留引用时专门使用引用相等。设置JsonSerializerSettings.EqualityComparer 用于参考循环检测,而不是参考保存和解析,如this answerWhy doesn't reference loop detection use reference equality? 中所述。

Andrew 的回答给出了一个自定义 IReferenceResolver 的示例,该示例使用对象相等性来解析特定类型对象的引用,并假设 所有 序列化对象都属于该类型。您想要做的是仅对某些类型(EntityAEntityB)使用对象相等性,而对所有其他类型使用 Json.NET 的 default reference resolver

您可以通过decorator pattern 完成此操作,其中您将Json.NET 的引用解析器的实例包装在您自己的IReferenceResolver 中。然后为需要自己的自定义相等比较的类型实现任何必要的逻辑,并将其他所有内容传递给底层的默认解析器。

这里有一个满足您的要求:

public class SelectiveValueEqualityReferenceResolver : EquivalencingReferenceResolver
{
    readonly Dictionary<Type, Dictionary<object, object>> representatives;

    public SelectiveValueEqualityReferenceResolver(IReferenceResolver defaultResolver, IEnumerable<Type> valueTypes)
        : base(defaultResolver)
    {
        if (valueTypes == null)
            throw new ArgumentNullException();
        representatives = valueTypes.ToDictionary(t => t, t => new Dictionary<object, object>());
    }

    protected override bool TryGetRepresentativeObject(object obj, out object representative)
    {
        var type = obj.GetType();
        Dictionary<object, object> typedItems;

        if (representatives.TryGetValue(type, out typedItems))
        {
            return typedItems.TryGetValue(obj, out representative);
        }
        return base.TryGetRepresentativeObject(obj, out representative);
    }

    protected override object GetOrAddRepresentativeObject(object obj)
    {
        var type = obj.GetType();
        Dictionary<object, object> typedItems;

        if (representatives.TryGetValue(type, out typedItems))
        {
            object representative;
            if (!typedItems.TryGetValue(obj, out representative))
                representative = (typedItems[obj] = obj);
            return representative;

        }
        return base.GetOrAddRepresentativeObject(obj);
    }
}

public abstract class EquivalencingReferenceResolver : IReferenceResolver
{
    readonly IReferenceResolver defaultResolver;

    public EquivalencingReferenceResolver(IReferenceResolver defaultResolver)
    {
        if (defaultResolver == null)
            throw new ArgumentNullException();
        this.defaultResolver = defaultResolver;
    }

    protected virtual bool TryGetRepresentativeObject(object obj, out object representative)
    {
        representative = obj;
        return true;
    }

    protected virtual object GetOrAddRepresentativeObject(object obj)
    {
        return obj;
    }

    #region IReferenceResolver Members

    public void AddReference(object context, string reference, object value)
    {
        var representative = GetOrAddRepresentativeObject(value);
        defaultResolver.AddReference(context, reference, representative);
    }

    public string GetReference(object context, object value)
    {
        var representative = GetOrAddRepresentativeObject(value);
        return defaultResolver.GetReference(context, representative);
    }

    public bool IsReferenced(object context, object value)
    {
        object representative;

        if (!TryGetRepresentativeObject(value, out representative))
            return false;
        return defaultResolver.IsReferenced(context, representative);
    }

    public object ResolveReference(object context, string reference)
    {
        return defaultResolver.ResolveReference(context, reference);
    }

    #endregion
}

然后你会使用如下:

var settings = new JsonSerializerSettings
{
    //Commented out TypeNameHandling since the JSON in the question does not include type information
    //TypeNameHandling = TypeNameHandling.All,
    NullValueHandling = NullValueHandling.Ignore,
    FloatParseHandling = FloatParseHandling.Decimal,
    Formatting = Formatting.Indented,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
    ReferenceResolverProvider = () => new SelectiveValueEqualityReferenceResolver(
        new JsonSerializer().ReferenceResolver, 
        new [] { typeof(EntityA), typeof(EntityB) }),
    Error = (sender, args) => Log.Error(args.ErrorContext.Error, $"Error while (de)serializing: {args.ErrorContext}; object: {args.CurrentObject}")
};

var outer = JsonConvert.DeserializeObject<OuterClass>(jsonString, settings);

var json2 = JsonConvert.SerializeObject(outer, settings);

请注意,我必须对您的类型进行各种修复才能使其正常工作:

public static class EqualityHelper
{
    public static bool? EqualsQuickReject<T1, T2>(T1 item1, T2 item2) 
        where T1 : class
        where T2 : class
    {
        if ((object)item1 == (object)item2)
            return true;
        else if ((object)item1 == null || (object)item2 == null)
            return false;

        if (item1.GetType() != item2.GetType())
            return false;

        return null;
    }
}

public class EntityA : IEquatable<EntityA> //Fixed added IEquatable<T>
{
    public int Foo { get; set; } // FIXED made public

    public override bool Equals(object obj)
    {
        return Equals(obj as EntityA);
    }

    // Fixed added required GetHashCode() that is compatible with Equals()
    public override int GetHashCode()
    {
        return Foo.GetHashCode();
    }

    #region IEquatable<EntityA> Members

    public bool Equals(EntityA other)
    {
        // FIXED - ensure Equals is reflexive, symmetric and transitive even when dealing with derived types
        var initial = EqualityHelper.EqualsQuickReject(this, other);
        if (initial != null)
            return initial.Value;
        return this.Foo == other.Foo;
    }

    #endregion
}

public class EntityB : IEquatable<EntityB> //Fixed added IEquatable<T>
{
    public int Bar { get; set; } // FIXED made public

    public EntityA Parent { get; set; } // FIXED made public

    public override bool Equals(object obj)
    {
        return Equals(obj as EntityB);
    }

    // Fixed added required GetHashCode() that is compatible with Equals()
    public override int GetHashCode()
    {
        return Bar.GetHashCode();
    }

    #region IEquatable<EntityB> Members

    public bool Equals(EntityB other)
    {
        // FIXED - ensure Equals is reflexive, symmetric and transitive even when dealing with derived types
        var initial = EqualityHelper.EqualsQuickReject(this, other);
        if (initial != null)
            return initial.Value;
        return this.Bar == other.Bar;
    }

    #endregion
}

public class InnerWrapper
{
    public string FooBar { get; set; }

    public EntityB BEntity { get; set; }
}

public class OuterClass
{
    public EntityA AEntity { get; set; }

    public List<EntityB> InnerElements { get; set; }//FIXED -- made public and corrected type to be consistent with sample JSON
}

注意事项:

  • SelectiveValueEqualityReferenceResolver 的工作原理如下。在构造它时,会给出一个默认的引用解析器和一个使用对象相等的类型列表。然后,当调用 IReferenceResolver 方法之一时,它会检查传入对象是否属于自定义类型之一。如果是这样,它会检查它是否已经使用对象相等性遇到了相同类型的等效对象。如果是这样,则将该初始对象传递给默认引用解析器。如果不是,它将传入对象缓存为对象等效对象的定义实例,然后将传入对象传递给默认引用解析器。

  • 上述逻辑仅在被覆盖的object.Equals() 是正确的equivalence relation 时才有效——即自反、对称和传递。

    在您的代码中,如果 EntityAEntityB 曾经被子类化,则不能保证是这种情况。因此,我修改了您的 Equals() 方法,要求传入的对象是相同的类型,而不仅仅是兼容的类型。

  • Equals()被覆盖时,还需要以兼容的方式覆盖GetHashCode(),使得相等的对象具有相等的哈希码。

    这在您的代码中没有完成,因此我将必要的逻辑添加到 EntityAEntityB

  • Json.NET 的DefaultReferenceResolver 是内部的,所以我不得不使用一种稍微老套的方法来创建一个,即构造一个临时的JsonSerializer 并获取它的ReferenceResolver

  • SelectiveValueEqualityReferenceResolver 不是线程安全的,因此应在每个线程中使用新的序列化程序实例。

  • SelectiveValueEqualityReferenceResolver 旨在在序列化期间为对象相等的对象生成相同的$id 值。它的设计目的不是为了在反序列化期间将具有不同 $id 值的相等对象合并到引用相等的对象中。我认为如果需要可以添加。

【讨论】:

    【解决方案2】:

    感谢 dbc 提供帮助。

    您的代码几乎就像我想要的那样工作。在那个例子中,它工作得非常好(抱歉代码问题)。

    如果对您的代码进行小幅调整,不要只依赖特定类型。

    public class SelectiveValueEqualityReferenceResolver : EquivalencingReferenceResolver
    {
          private readonly Dictionary<Type, Dictionary<object, object>> _representatives;
    
          public SelectiveValueEqualityReferenceResolver(IReferenceResolver defaultResolver)
              : base(defaultResolver)
          {
              this._representatives = new Dictionary<Type, Dictionary<object, object>>();
          }
    
          protected override bool TryGetRepresentativeObject(object obj, out object representative)
          {
              var type = obj.GetType();
              if (type.GetTypeInfo().IsClass && this._representatives.TryGetValue(type, out var typedItems))
                  return typedItems.TryGetValue(obj, out representative);
    
              return base.TryGetRepresentativeObject(obj, out representative);
          }
    
          protected override object GetOrAddRepresentativeObject(object obj)
          {
              var type = obj.GetType();
    
              if (!type.GetTypeInfo().IsClass)
                  return base.GetOrAddRepresentativeObject(obj);
    
              if (!this._representatives.TryGetValue(type, out var typedItems))
              {
                  typedItems = new Dictionary<object, object>();
                  this._representatives.Add(type, typedItems);
              }
    
              if (!typedItems.TryGetValue(obj, out var representative))
                  representative = typedItems[obj] = obj;
    
              return representative;
          }
    }
    

    这个使用所有类的默认比较器。对于所有其他(结构等),它使用默认的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-28
      • 1970-01-01
      • 1970-01-01
      • 2023-03-28
      相关资源
      最近更新 更多