Newtonsoft JSON PreserveReferences使用自定义Equals处理 [英] Newtonsoft JSON PreserveReferencesHandling with custom Equals usage

查看:80
本文介绍了Newtonsoft JSON PreserveReferences使用自定义Equals处理的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前在Newtonsoft Json方面遇到一些问题.

I'm currently having some issues with Newtonsoft Json.

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

What I want is simple: Compare the Object which will be serialized with all Properties and Subproperties for Equality.

我现在尝试创建自己的EqualityComparer,但仅将其与父对象的属性进行比较.

I tried now to Create my own EqualityComparer but it only compared with the Properties of the Parent Object.

此外,我尝试编写自己的ReferenceResolver,但运气不佳.

Also, I tried to write my own ReferenceResolver but had no luck with it.

我们来看一个例子:

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.

Now what I want is to have the References from EntityB to EntityA. They are in my case always the same. So what I expect is, that in the JSON in every EntityB the reference to EntityA is written as ref. The Equal of the Entities overwrites the Equals to check if they are the same. They are Database Objects, so that they are equals as soon as their ID is the same. In this case I've called them Foo and Bar.

我尝试过的操作如下:

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的属性).

But it doesn't work. It compares totally wrong values. For example the EntityA from the OuterClass with each of the InnerWrapper. But not with the Properties or even Subproperties (in this case the Properties of the EntityB of the InnerWrapper).

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

With a custom ReferenceResolver, I've also no luck, because the settings above are really generic and I don't have any idea how to write a generic one.

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

Do you have any idea how to get this work?

//

//

下面是我期望的例子:

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

}

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

Of course, in this case, the impact is low. But my real scenario is much bigger.

推荐答案

此答案中所述 JSON.NET序列化-DefaultReferenceResolver如何比较相等性? 通过 JsonSerializerSettings.EqualityComparer 用于参考循环检测,而非参考保存和解析,如此答案

As stated in this answer to JSON.NET Serialization - How does DefaultReferenceResolver compare equality? by Andrew Whitaker, Json.NET exclusively uses reference equality when preserving references via PreserveReferencesHandling. The setting JsonSerializerSettings.EqualityComparer is intended for reference loop detection rather than reference preservation and resolution, as explain in this answer to Why doesn't reference loop detection use reference equality?.

安德鲁(Andrew)的答案给出了一个自定义 IReferenceResolver 它使用对象相等性对特定类型的对象解析引用,并假定 all 序列化的对象属于该类型.您想要做的是仅对某些类型(EntityAEntityB)使用对象相等性,然后依靠Json.NET的

Andrew's answer gives an example of a custom IReferenceResolver that resolves references using object equality for objects of a certain type and assumes all serialized objects are of that type. What you would like to do is to use object equality only for certain types (EntityA and EntityB) and fall back on Json.NET's default reference resolver for all other types.

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

You can accomplish this via the decorator pattern, in which you wrap an instance of Json.NET's reference resolver in your own IReferenceResolver. Then implement whatever logic is necessary for the types that need their own custom equality comparison, and pass everything else on to the underlying default resolver.

这是一个满足您要求的产品:

Here is one that meets your requirements:

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
}

然后您将使用以下方式:

Which you would then use as follows:

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);

请注意,为了使此功能有效,我必须对您的类型进行各种修复:

Note that I had to make a variety of fixes to your types to make this work:

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方法之一时,它将检查传入的对象是否属于自定义类型之一.如果是这样,它将使用对象相等性检查是否已遇到相同类型的等效对象.如果是这样,则将该初始对象传递给默认的引用解析器.否则,它将传入的对象作为与对象等效的对象的定义实例进行缓存,然后将传入的对象传递给默认的引用解析器.

  • SelectiveValueEqualityReferenceResolver works as follows. When constructed it is given a default reference resolver and a list of types for which to use object equality. Then, when one of the IReferenceResolver methods is called, it checks to see whether the incoming object is of one of the custom types. If so, it checks to see whether it has already encountered an equivalent object of the same type using object equality. If so, passes that initial object on to the default reference resolver. If not, it caches the incoming object as a defining instance of object-equivalent objects, then passes the incoming object on to the default reference resolver.

仅当被覆盖的object.Equals()是正确的等效关系-即反身,对称和及物.

The above logic only works if the overridden object.Equals() is a proper equivalence relation -- i.e. reflexive, symmetric and transitive.

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

In your code this was not guaranteed to be the case if EntityA or EntityB were ever subclassed. Thus I modified your Equals() methods to require that the incoming object be of the same type, rather than just a compatible type.

覆盖Equals()时,还必须覆盖

When Equals() is overriden, it is also necessary to override GetHashCode() in a compatible manner, such that equal objects have equal hash codes.

这在您的代码中没有完成,所以我在EntityAEntityB中添加了必要的逻辑.

This was not done in your code so I added the necessary logic to EntityA and EntityB.

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

Json.NET's DefaultReferenceResolver is internal so I had to use a slightly hacky method to create one, namely construct a temporary JsonSerializer and grab its ReferenceResolver.

SelectiveValueEqualityReferenceResolver并非线程安全的,因此应在每个线程中使用一个新的序列化程序实例.

SelectiveValueEqualityReferenceResolver is not thread safe so a fresh serializer instance should be used in each thread.

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

SelectiveValueEqualityReferenceResolver is designed to generate identical $id values for object-equal objects during serialization. It's not designed to merge equal objects with different $id values into reference-equal objects during deserialization. I think that could be added if required.

这篇关于Newtonsoft JSON PreserveReferences使用自定义Equals处理的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆