c# - 引用自动创建的对象

标签 c# json.net

我正在尝试序列化和反序列化一个复杂的对象图:

类(class)A包含一个只读属性,其中包含类型为 B 的不可变对象(immutable对象)数组。 . B 类型的对象以及不可变数组是在类型 A 的构造函数中创建的。 .

其他类型包含对类型 B 的对象的引用通过访问 A 类型的对象的数组获得.

在反序列化期间,我需要对 B 的任何引用。最终指向由 A 创建的适当对象通过索引构造函数,而不是创建全新的 B来自 JSON 的对象。我正在尝试使用 PreserveReferencesHandling使用 JSON.NET。这可以理解是行不通的,因为它试图使用 B 的反序列化版本。而不是 A - 构建版本。

我可以在这里使用另一种策略而不修改我的类型吗?

编辑:为了澄清并非常清楚,解决方案不得修改类型本身。您可以触摸契约(Contract)解析器、活页夹、引用解析器等,但不能触摸类型。另外,B类型不能反序列化。它们必须由 A 制作的构造函数。

最佳答案

更新

你的问题没有给出你想要完成的例子,所以我在猜测你的一些设计要求。确认一下,你的情况是:

  • 您需要使用 Json.NET 序列化一些复杂的对象图
  • 在整个图中,有许多类 A 的实例。 .
  • A包含类 B 的不可变实例数组只能在 A 的构造函数中构造.
  • A的每个实例可能有也可能没有要序列化的属性(未指定)
  • B的每个实例可能有也可能没有要序列化的属性(未指定)。
  • 同样在整个图中,还有许多对 B 实例的引用。 ,但在所有情况下,这些引用实际上指向 B 的一个实例在 A 的实例之一内.
  • 反序列化图形时,需要对 B 实例的所有引用。 to 指向 B 的一个实例在 A 的实例中对应于原始实例,按数组索引。
  • 您没有任何代码来收集和发现 A 的所有实例在您的对象图中。
  • 您不能以任何方式触及类的 c# 代码,甚至不能添加数据协定属性或私有(private)属性。

  • 让我们使用以下类对这种情况进行建模:
    public abstract class B
    {
        public int Index { get; set; } // Some property that could be modified.
    }
    
    public class A
    {
        public class BActual : B
        {
        }
    
        static int nextId = -1;
    
        readonly B[] items; // A private read-only array that is never changed.
    
        public A()
        {
            items = Enumerable.Range(101 + 10 * Interlocked.Increment(ref nextId), 2).Select(i => new BActual { Index = i }).ToArray();
        }
    
        public string SomeProperty { get; set; }
    
        public IEnumerable<B> Items
        {
            get
            {
                foreach (var b in items)
                    yield return b;
            }
        }
    
        public string SomeOtherProperty { get; set; }
    }
    
    public class MidClass
    {
        public MidClass()
        {
            AnotherA = new A();
        }
    
        public A AnotherA { get; set; }
    }
    
    public class MainClass
    {
        public MainClass()
        {
            A1 = new A();
            MidClass = new MidClass();
            A2 = new A();
        }
    
        public List<B> ListOfB { get; set; }
    
        public A A2 { get; set; }
    
        public MidClass MidClass { get; set; }
    
        public A A1 { get; set; }
    }
    

    然后,要序列化,需要使用Json.NET 收集A 的所有实例。在您的对象图中。接下来,用 PreserveReferencesHandling = PreserveReferencesHandling.Objects 设置、序列化一个包含 A 的所有实例的表的代理类作为第一项,然后您的根对象作为第二项。

    反序列化,使用 PreserveReferencesHandling.Objects您必须使用 JsonConverter 反序列化您的代理类为 A反序列化 A 的属性(如果有)和 B , 和 adds a reference对于序列化 "$ref"引用 BB 的新实例在 A 的构造函数中分配.

    因此:
    // Used to enable Json.NET to traverse an object hierarchy without actually writing any data.
    public class NullJsonWriter : JsonWriter
    {
        public NullJsonWriter()
            : base()
        {
        }
    
        public override void Flush()
        {
            // Do nothing.
        }
    }
    
    public class TypeInstanceCollector<T> : JsonConverter where T : class
    {
        readonly List<T> instanceList = new List<T>();
        readonly HashSet<T> instances = new HashSet<T>();
    
        public List<T> InstanceList { get { return instanceList; } }
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(T).IsAssignableFrom(objectType);
        }
    
        public override bool CanRead { get { return false; } }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            T instance = (T)value;
            if (!instances.Contains(instance))
            {
                instanceList.Add(instance);
                instances.Add(instance);
            }
            // It's necessary to write SOMETHING here.  Null suffices.
            writer.WriteNull();
        }
    }
    
    public class ADeserializer : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(A).IsAssignableFrom(objectType);
        }
    
        public override bool CanWrite { get { return false; } }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var obj = JObject.Load(reader);
            if (obj == null)
                return existingValue;
            A a;
    
            var refId = (string)obj["$ref"];
            if (refId != null)
            {
                a = (A)serializer.ReferenceResolver.ResolveReference(serializer, refId);
                if (a != null)
                    return a;
            }
    
            a = ((A)existingValue) ?? new A();
    
            var items = obj["Items"];
            obj.Remove("Items");
    
            // Populate properties other than the items, if any
            // This also updates the ReferenceResolver table.
            using (var objReader = obj.CreateReader())
                serializer.Populate(objReader, a);
    
            // Populate properties of the B items, if any
            if (items != null)
            {
                if (items.Type != JTokenType.Array)
                    throw new JsonSerializationException("Items were not an array");
                var itemsArray = (JArray)items;
                if (a.Items.Count() < itemsArray.Count)
                    throw new JsonSerializationException("too few items constructucted"); // Item counts must match
                foreach (var pair in a.Items.Zip(itemsArray, (b, o) => new { ItemB = b, JObj = o }))
                {
    #if false
                    // If your B class has NO properties to deserialize, do this
                    var id = (string)pair.JObj["$id"];
                    if (id != null)
                        serializer.ReferenceResolver.AddReference(serializer, id, pair.ItemB);
    #else
                    // If your B class HAS SOME properties to deserialize, do this
                    using (var objReader = pair.JObj.CreateReader())
                    {
                        // Again, Populate also updates the ReferenceResolver table
                        serializer.Populate(objReader, pair.ItemB);
                    }
    #endif
                }
            }
    
            return a;
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    
    public class RootProxy<TRoot, TTableItem>
    {
        [JsonProperty("table", Order = 1)]
        public List<TTableItem> Table { get; set; }
    
        [JsonProperty("data", Order = 2)]
        public TRoot Data { get; set; }
    }
    
    public class TestClass
    {
        public static string Serialize(MainClass main)
        {
            // First, collect all instances of A 
            var collector = new TypeInstanceCollector<A>();
    
            var collectionSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, Converters = new JsonConverter[] { collector } };
            using (var jsonWriter = new NullJsonWriter())
            {
                JsonSerializer.CreateDefault(collectionSettings).Serialize(jsonWriter, main);
            }
    
            // Now serialize a proxt class with the collected instances of A at the beginning, to establish reference ids for all instances of B.
            var proxy = new RootProxy<MainClass, A> { Data = main, Table = collector.InstanceList };
            var serializationSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects };
    
            return JsonConvert.SerializeObject(proxy, Formatting.Indented, serializationSettings);
        }
    
        public static MainClass Deserialize(string json)
        {
            var serializationSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, Converters = new JsonConverter[] { new ADeserializer() } };
            var proxy = JsonConvert.DeserializeObject<RootProxy<MainClass, A>>(json, serializationSettings);
    
            return proxy.Data;
        }
    
        static IEnumerable<A> GetAllA(MainClass main)
        {
            // For testing.  In your case apparently you can't do this manually.
            if (main.A1 != null)
                yield return main.A1;
            if (main.A2 != null)
                yield return main.A2;
            if (main.MidClass != null && main.MidClass.AnotherA != null)
                yield return main.MidClass.AnotherA;
        }
    
        static IEnumerable<B> GetAllB(MainClass main)
        {
            return GetAllA(main).SelectMany(a => a.Items);
        }
    
        public static void Test()
        {
            var main = new MainClass();
            main.A1.SomeProperty = "main.A1.SomeProperty";
            main.A1.SomeOtherProperty = "main.A1.SomeOtherProperty";
    
            main.A2.SomeProperty = "main.A2.SomeProperty";
            main.A2.SomeOtherProperty = "main.A2.SomeOtherProperty";
    
            main.MidClass.AnotherA.SomeProperty = "main.MidClass.AnotherA.SomeProperty";
            main.MidClass.AnotherA.SomeOtherProperty = "main.MidClass.AnotherA.SomeOtherProperty";
    
            main.ListOfB = GetAllB(main).Reverse().ToList();
    
            var json = Serialize(main);
    
            var main2 = Deserialize(json);
    
            var json2 = Serialize(main2);
    
            foreach (var b in main2.ListOfB)
                Debug.Assert(GetAllB(main2).Contains(b)); // No assert
            Debug.Assert(json == json2); // No assert
            Debug.Assert(main.ListOfB.Select(b => b.Index).SequenceEqual(main2.ListOfB.Select(b => b.Index))); // No assert
            Debug.Assert(GetAllA(main).Select(a => a.SomeProperty + a.SomeOtherProperty).SequenceEqual(GetAllA(main2).Select(a => a.SomeProperty + a.SomeOtherProperty))); // No assert
        }
    }
    

    原答案

    首先,您可以使用 [JsonConstructor] 属性指定 Json.NET 应使用非默认构造函数来反序列化您的类 A .这样做将允许您反序列化为您的不可变集合。此构造函数可以是私有(private)的,以便您可以继续创建 B 的实例。在预先存在的公共(public)构造函数中。请注意,构造函数参数名称必须与原始属性名称匹配。

    其次,如果你设置 PreserveReferencesHandling = PreserveReferencesHandling.Objects ,然后是对象图中直接引用 B 实例的任何其他对象不可变数组所持有的将在序列化和反序列化时继续直接引用反序列化的不可变数组中的实例。即,它应该可以正常工作。

    考虑以下测试用例:
    public class B
    {
        public int Index { get; set; }
    }
    
    public class A
    {
        static int nextId = -1;
    
        readonly B [] items; // A private read-only array that is never changed.
    
        [JsonConstructor]
        private A(IEnumerable<B> Items, string SomeProperty)
        {
            this.items = (Items ?? Enumerable.Empty<B>()).ToArray();
            this.SomeProperty = SomeProperty;
        }
    
        // // Create instances of "B" with different properties each time the default constructor is called.
        public A() : this(Enumerable.Range(101 + 10*Interlocked.Increment(ref nextId), 2).Select(i => new B { Index = i }), "foobar") 
        {
        }
    
        public IEnumerable<B> Items
        {
            get
            {
                foreach (var b in items)
                    yield return b;
            }
        }
    
        [JsonIgnore]
        public int Count { get { return items.Length; } }
    
        public B GetItem(int index)
        {
            return items[index];
        }
    
        public string SomeProperty { get; set; }
    
        public string SomeOtherProperty { get; set; }
    }
    
    public class TestClass
    {
        public A A { get; set; }
    
        public List<B> ListOfB { get; set; }
    
        public static void Test()
        {
            var a = new A() { SomeOtherProperty = "something else" };
            var test = new TestClass { A = a, ListOfB = a.Items.Reverse().ToList() };
    
            var settings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects };
    
            var json = JsonConvert.SerializeObject(test, Formatting.Indented, settings);
            Debug.WriteLine(json);
            var test2 = JsonConvert.DeserializeObject<TestClass>(json, settings);
    
            // Assert that pointers in "ListOfB" are equal to pointers in A.Items
            Debug.Assert(test2.ListOfB.All(i2 => test2.A.Items.Contains(i2, new ReferenceEqualityComparer<B>())));
    
            // Assert deserialized data is the same as the original data.
            Debug.Assert(test2.A.SomeProperty == test.A.SomeProperty);
            Debug.Assert(test2.A.SomeOtherProperty == test.A.SomeOtherProperty);
            Debug.Assert(test2.A.Items.Select(i => i.Index).SequenceEqual(test.A.Items.Select(i => i.Index)));
    
            var json2 = JsonConvert.SerializeObject(test2, Formatting.Indented, settings);
            Debug.WriteLine(json2);
            Debug.Assert(json2 == json);
        }
    }
    

    在这种情况下,我创建了类 B有一些数据,类(class) A其中包含一个不可变的集合 B它在其公共(public)构造函数中创建,以及一个包含类 TestClass包含 A 的实例和项目列表 B取自 A .当我序列化它时,我得到以下 JSON:

    {
      "$id": "1",
      "A": {
        "$id": "2",
        "Items": [
          {
            "$id": "3",
            "Index": 101
          },
          {
            "$id": "4",
            "Index": 102
          }
        ],
        "SomeProperty": "foobar",
        "SomeOtherProperty": "something else"
      },
      "ListOfB": [
        {
          "$ref": "4"
        },
        {
          "$ref": "3"
        }
      ]
    }
    


    然后,当我反序列化它时,我断言所有反序列化的项目 BListOfBB 的实例之一具有指针相等性在 a.Items .我还断言所有反序列化的属性都具有与原始属性相同的值,从而确认调用了非默认私有(private)构造函数来反序列化不可变集合。

    这是你想要的吗?

    用于检查 B 实例的指针相等性, 我用:
    public class ReferenceEqualityComparer<T> : IEqualityComparer<T> where T : class
    {
        #region IEqualityComparer<T> Members
    
        public bool Equals(T x, T y)
        {
            return object.ReferenceEquals(x, y);
        }
    
        public int GetHashCode(T obj)
        {
            return (obj == null ? 0 : obj.GetHashCode());
        }
    
        #endregion
    }
    

    关于c# - 引用自动创建的对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30648718/

    相关文章:

    c# - 调用从 c# 返回 char* 的 c++ dll 函数。不能使用 DllImport()

    c# - 遍历两个列表

    c# - 是否有一种通用方法来定义既不接收 Action 也不接收 Func<Task> 的方法?

    c# - 如何反序列化嵌套的 JSON 数组?

    c# - Newtonsoft Json.NET InvalidCastException

    c# - 无法使用 NewtonSoft JSONConvert 解析 JSON

    c# - 将 JSON 反序列化为 List<T> 时出错

    c# - 为什么 Split from System.String 不接受字符串作为重载?

    c# - 如何使两个图像与 WPF 重叠?

    c# - 使用 C# 将 JSON 字符串直接添加到 Azure Blob 存储容器