Newtonsoft JSON PreserveReferences使用自定义Equals用法进行处理

时间:2018-10-11 10:46:50

标签: c# json.net

我目前在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和每个InnerWrapper的EntityA。但不包含属性甚至子属性(在这种情况下,EntityB的{​​{1}}的属性)。

使用自定义的ReferenceResolver,我也不走运,因为上面的设置确实是通用的,我也不知道如何编写通用的。

您是否知道如何进行这项工作?

//编辑:

下面是我期望的例子:

InnerWrapper

这就是我得到的:

{
    "$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"
            }
        }
    ]

}

当然,在这种情况下,影响很小。但是我的真实情况更大。

2 个答案:

答案 0 :(得分:2)

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

安德鲁(Andrew)的答案给出了一个自定义IReferenceResolver的示例,该自定义default reference resolver使用对象相等性对特定类型的对象解析引用,并假定所有序列化的对象均为该类型。您只想对某些类型(EntityAEntityB)使用对象相等性,而对其他所有类型都使用Json.NET的decorator pattern

您可以通过equivalence relation完成此操作,其中您将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()是正确的GetHashCode()时,上述逻辑才有效-即自反,对称和可传递。

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

  • 当覆盖Equals()时,还必须以兼容的方式覆盖DefaultReferenceResolver,以使相等的对象具有相同的哈希码。

    您的代码未完成此操作,因此我向EntityAEntityB添加了必要的逻辑。

  • Json.NET的Fiddle是内部的,因此我不得不使用一种有点怪异的方法来创建它,即构造一个临时JsonSerializer并获取其ReferenceResolver

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

  • SelectiveValueEqualityReferenceResolver设计为在序列化期间为对象相等的对象生成相同的$id值。它并非旨在在反序列化期间将具有不同$id值的相等对象合并为参考相等对象。我认为可以根据需要添加。

答案 1 :(得分:1)

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

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