You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2021/01/29 13:14:37 UTC

[ignite] branch master updated: IGNITE-13639 .NET: Fix detached semantics for array and collection elements

This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new a7f35ac  IGNITE-13639 .NET: Fix detached semantics for array and collection elements
a7f35ac is described below

commit a7f35ac0670aed074ef88054ee17c0da6b47068c
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Fri Jan 29 16:14:19 2021 +0300

    IGNITE-13639 .NET: Fix detached semantics for array and collection elements
    
    Arrays and non-generic collections are written as Java-compatible types, and elements of those collections are deserialized individually on Java side, even in binary mode. Therefore we should detach every collection element, so that handles to the same object instances are not shared.
---
 .../Binary/BinarySelfTest.cs                       |  69 +++--
 .../Binary/JavaBinaryInteropTest.cs                | 283 ++++++++++++++++++---
 .../Apache.Ignite.Core/Impl/Binary/BinaryUtils.cs  |   8 +-
 .../Apache.Ignite.Core/Impl/Binary/BinaryWriter.cs |  32 ++-
 4 files changed, 313 insertions(+), 79 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/BinarySelfTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/BinarySelfTest.cs
index bf3b474..009a15f 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/BinarySelfTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/BinarySelfTest.cs
@@ -45,7 +45,7 @@ namespace Apache.Ignite.Core.Tests.Binary
     /// Binary tests.
     /// </summary>
     [TestFixture]
-    public class BinarySelfTest { 
+    public class BinarySelfTest {
         /** */
         private Marshaller _marsh;
 
@@ -59,7 +59,7 @@ namespace Apache.Ignite.Core.Tests.Binary
         };
 
         /// <summary>
-        /// 
+        ///
         /// </summary>
         [TestFixtureSetUp]
         public void BeforeTest()
@@ -86,7 +86,7 @@ namespace Apache.Ignite.Core.Tests.Binary
         {
             return BinaryBasicNameMapper.FullNameInstance;
         }
-        
+
         /**
          * <summary>Check write of primitive boolean.</summary>
          */
@@ -627,7 +627,7 @@ namespace Apache.Ignite.Core.Tests.Binary
 
             Assert.AreEqual(vals, newVals);
         }
-        
+
         /// <summary>
         /// Test object with dates.
         /// </summary>
@@ -650,7 +650,7 @@ namespace Apache.Ignite.Core.Tests.Binary
             DateTimeType otherObj = marsh.Unmarshal<DateTimeType>(marsh.Marshal(obj));
 
             Assert.AreEqual(obj.Utc, otherObj.Utc);
-            Assert.AreEqual(obj.UtcNull, otherObj.UtcNull);            
+            Assert.AreEqual(obj.UtcNull, otherObj.UtcNull);
             Assert.AreEqual(obj.UtcArr, otherObj.UtcArr);
 
             Assert.AreEqual(obj.UtcRaw, otherObj.UtcRaw);
@@ -723,7 +723,7 @@ namespace Apache.Ignite.Core.Tests.Binary
                 },
                 Objects = new object[] {1, 2, "3", 4.4}
             };
-            
+
             var data = marsh.Marshal(obj);
 
             var result = marsh.Unmarshal<GenericCollectionsType<PrimitiveFieldType, SerializableObject>>(data);
@@ -786,7 +786,7 @@ namespace Apache.Ignite.Core.Tests.Binary
         [Test]
         public void TestProperty()
         {
-            ICollection<BinaryTypeConfiguration> typeCfgs = 
+            ICollection<BinaryTypeConfiguration> typeCfgs =
                 new List<BinaryTypeConfiguration>();
 
             typeCfgs.Add(new BinaryTypeConfiguration(typeof(PropertyType)));
@@ -1080,7 +1080,7 @@ namespace Apache.Ignite.Core.Tests.Binary
                 },
                 CompactFooter = GetCompactFooter()
             });
-            
+
             var obj = new CollectionsType
             {
                 Hashtable = new Hashtable {{1, 2}, {3, 4}},
@@ -1571,7 +1571,7 @@ namespace Apache.Ignite.Core.Tests.Binary
 
             PropertyType[,] objs = {{new PropertyType {Field1 = 123}}};
             Assert.AreEqual(123, TestUtils.SerializeDeserialize(objs)[0, 0].Field1);
-            
+
             var obj = new MultidimArrays { MultidimInt = ints, MultidimUInt = uints };
             var resObj = TestUtils.SerializeDeserialize(obj);
             Assert.AreEqual(obj.MultidimInt, resObj.MultidimInt);
@@ -1615,7 +1615,7 @@ namespace Apache.Ignite.Core.Tests.Binary
             };
 
             var res = TestUtils.SerializeDeserialize(ptrs, raw);
-            
+
             Assert.IsTrue(ptrs.ByteP == res.ByteP);
             Assert.IsTrue(ptrs.IntP == res.IntP);
             Assert.IsTrue(ptrs.VoidP == res.VoidP);
@@ -1645,18 +1645,39 @@ namespace Apache.Ignite.Core.Tests.Binary
 
             IBinaryObject innerObject = _marsh.Unmarshal<IBinaryObject>(dataInner, BinaryMode.ForceBinary);
             BinaryObjectWrapper inner = innerObject.Deserialize<BinaryObjectWrapper>();
-            
+
             Assert.NotNull(inner);
 
             byte[] dataOuter = _marsh.Marshal(new BinaryObjectWrapper() { Val = innerObject });
 
             IBinaryObject outerObject = _marsh.Unmarshal<IBinaryObject>(dataOuter, BinaryMode.ForceBinary);
             BinaryObjectWrapper outer = outerObject.Deserialize<BinaryObjectWrapper>();
-            
+
             Assert.NotNull(outer);
             Assert.IsTrue(outer.Val.Equals(innerObject));
         }
 
+        /// <summary>
+        /// Tests serializing/deserializing object lists with nested lists.
+        /// </summary>
+        [Test]
+        public void TestNestedLists()
+        {
+            var list = new[]
+            {
+                new NestedList {Inner = new List<object>()},
+                new NestedList {Inner = new List<object>()}
+            };
+
+            var bytes = _marsh.Marshal(list);
+            var res = _marsh.Unmarshal<NestedList[]>(bytes);
+
+            Assert.AreEqual(2, res.Length);
+            Assert.AreEqual(0, res[0].Inner.Count);
+            Assert.AreEqual(0, res[1].Inner.Count);
+            Assert.AreNotSame(res[0].Inner, res[1].Inner);
+        }
+
         private static void CheckKeepSerialized(BinaryConfiguration cfg, bool expKeep)
         {
             if (cfg.TypeConfigurations == null)
@@ -1681,7 +1702,7 @@ namespace Apache.Ignite.Core.Tests.Binary
             Assert.AreEqual(expKeep, deserialized1 == deserialized2);
         }
 
-        private void CheckHandlesConsistency(HandleOuter outer, HandleInner inner, HandleOuter newOuter, 
+        private void CheckHandlesConsistency(HandleOuter outer, HandleInner inner, HandleOuter newOuter,
             HandleInner newInner)
         {
             Assert.True(newOuter != null);
@@ -1698,7 +1719,7 @@ namespace Apache.Ignite.Core.Tests.Binary
             Assert.AreEqual(inner.After, newInner.After);
             Assert.AreEqual(inner.RawBefore, newInner.RawBefore);
             Assert.True(newInner.RawOuter == newOuter);
-            Assert.AreEqual(inner.RawAfter, newInner.RawAfter);            
+            Assert.AreEqual(inner.RawAfter, newInner.RawAfter);
         }
 
         private static void CheckObject(Marshaller marsh, OuterObjectType outObj, InnerObjectType inObj)
@@ -1728,7 +1749,7 @@ namespace Apache.Ignite.Core.Tests.Binary
                     return true;
 
                 var type = obj as OuterObjectType;
-                
+
                 return type != null && Equals(InObj, type.InObj);
             }
 
@@ -1775,7 +1796,7 @@ namespace Apache.Ignite.Core.Tests.Binary
             public ICollection Col1 { get; set; }
 
             public ArrayList Col2 { get; set; }
-            
+
             public TestList Col3 { get; set; }
 
             public Hashtable Hashtable { get; set; }
@@ -1798,8 +1819,8 @@ namespace Apache.Ignite.Core.Tests.Binary
 
                 var that = obj as CollectionsType;
 
-                return that != null 
-                    && CompareCollections(Col1, that.Col1) 
+                return that != null
+                    && CompareCollections(Col1, that.Col1)
                     && CompareCollections(Col2, that.Col2)
                     && CompareCollections(Hashtable, that.Hashtable)
                     && CompareCollections(Dict, that.Dict)
@@ -1972,7 +1993,7 @@ namespace Apache.Ignite.Core.Tests.Binary
         }
 
         [Serializable]
-        public class PrimitiveFieldType 
+        public class PrimitiveFieldType
         {
             public PrimitiveFieldType()
             {
@@ -2456,7 +2477,7 @@ namespace Apache.Ignite.Core.Tests.Binary
                 writer.WriteString("before", Before);
 
                 writer0.WriteObject("inner", Inner);
-                
+
                 writer.WriteString("after", After);
 
                 IBinaryRawWriter rawWriter = writer.GetRawWriter();
@@ -2738,7 +2759,7 @@ namespace Apache.Ignite.Core.Tests.Binary
 
             public int[,] MultidimInt { get; set; }
             public uint[,,] MultidimUInt { get; set; }
-            
+
             public void WriteBinary(IBinaryWriter writer)
             {
                 writer.WriteObject("JaggedInt", JaggedInt);
@@ -2773,5 +2794,11 @@ namespace Apache.Ignite.Core.Tests.Binary
         {
             public IBinaryObject Val;
         }
+
+        private class NestedList
+        {
+            // ReSharper disable once CollectionNeverUpdated.Local
+            public IList<object> Inner { get; set; }
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/JavaBinaryInteropTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/JavaBinaryInteropTest.cs
index 2faed93..3206838 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/JavaBinaryInteropTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/JavaBinaryInteropTest.cs
@@ -29,66 +29,247 @@ namespace Apache.Ignite.Core.Tests.Binary
     /// </summary>
     public class JavaBinaryInteropTest
     {
+        /** */
+        private const string CacheName = "default";
+
+        /// <summary>
+        /// Sets up the fixture.
+        /// </summary>
+        [TestFixtureSetUp]
+        public void FixtureSetUp()
+        {
+            Ignition.Start(TestUtils.GetTestConfiguration());
+        }
+
+        /// <summary>
+        /// Tears down the fixture.
+        /// </summary>
+        [TestFixtureTearDown]
+        public void FixtureTearDown()
+        {
+            Ignition.StopAll(true);
+        }
+
         /// <summary>
         /// Tests that all kinds of values from .NET can be handled properly on Java side.
         /// </summary>
         [Test]
         public void TestValueRoundtrip()
         {
-            using (var ignite = Ignition.Start(TestUtils.GetTestConfiguration()))
+            Ignition.GetIgnite().CreateCache<int, object>(CacheName);
+
+            // Basic types.
+            // Types which map directly to Java are returned properly when retrieved as object.
+            // Non-directly mapped types are returned as their counterpart.
+            CheckValueCaching((char) 128);
+            CheckValueCaching((byte) 255);
+            CheckValueCaching((sbyte) -10, false);
+            CheckValueCaching((short) -32000);
+            CheckValueCaching((ushort) 65350, false);
+            CheckValueCaching(int.MinValue);
+            CheckValueCaching(uint.MaxValue, false);
+            CheckValueCaching(long.MinValue);
+            CheckValueCaching(ulong.MaxValue, false);
+
+            CheckValueCaching((float) 1.1);
+            CheckValueCaching(2.2);
+
+            CheckValueCaching((decimal) 3.3, asArray: false);
+            CheckValueCaching(Guid.NewGuid(), asArray: false);
+            CheckValueCaching(DateTime.Now, asArray: false);
+
+            CheckValueCaching("foobar");
+
+            // Special arrays.
+            CheckValueCaching(new[] {Guid.Empty, Guid.NewGuid()}, false);
+            CheckValueCaching(new Guid?[] {Guid.Empty, Guid.NewGuid()});
+
+            CheckValueCaching(new[] {1.2m, -3.4m}, false);
+            CheckValueCaching(new decimal?[] {1.2m, -3.4m});
+
+            CheckValueCaching(new[] {DateTime.Now}, false);
+
+            // Custom types.
+            CheckValueCaching(new Foo {X = 10}, asArray: false);
+            CheckValueCaching(new Bar {X = 20}, asArray: false);
+
+            // Collections.
+            CheckValueCaching(new List<Foo>(GetFoo()));
+            CheckValueCaching(new List<Bar>(GetBar()));
+
+            CheckValueCaching(new HashSet<Foo>(GetFoo()));
+            CheckValueCaching(new HashSet<Bar>(GetBar()));
+
+            CheckValueCaching(GetFoo().ToDictionary(x => x.X, x => x));
+            CheckValueCaching(GetBar().ToDictionary(x => x.X, x => x));
+
+            // Custom type arrays.
+            // Array type is lost, because in binary mode on Java side we receive the value as Object[].
+            CheckValueCaching(new[] {new Foo {X = -1}, new Foo {X = 1}}, false);
+            CheckValueCaching(new[] {new Bar {X = -10}, new Bar {X = 10}}, false);
+        }
+
+        /// <summary>
+        /// Tests array of objects with shared list instance.
+        /// </summary>
+        [Test]
+        public void TestArrayOfObjectsWithSharedListProperty()
+        {
+            var cache = Ignition.GetIgnite().GetOrCreateCache<int, InnerList[]>(TestUtils.TestName);
+            var inner = new List<object>();
+
+            cache.Put(1, new[]
+            {
+                new InnerList {Inner = inner},
+                new InnerList {Inner = inner}
+            });
+
+            var res = cache.Get(1);
+            Assert.AreEqual(2, res.Length);
+            Assert.AreNotSame(res[0].Inner, res[1].Inner);
+        }
+
+        /// <summary>
+        /// Tests array of objects with shared object instance.
+        /// </summary>
+        [Test]
+        public void TestArrayOfObjectsWithSharedObjectProperty()
+        {
+            var cache = Ignition.GetIgnite().GetOrCreateCache<int, InnerObject[]>(TestUtils.TestName);
+            var inner = new object();
+
+            cache.Put(1, new[]
+            {
+                new InnerObject {Inner = inner},
+                new InnerObject {Inner = inner}
+            });
+
+            var res = cache.Get(1);
+            Assert.AreEqual(2, res.Length);
+            Assert.AreNotSame(res[0].Inner, res[1].Inner);
+        }
+
+        /// <summary>
+        /// Tests ArrayList of objects with shared object instance.
+        /// </summary>
+        [Test]
+        public void TestArrayListOfObjectsWithSharedObjectProperty()
+        {
+            var cache = Ignition.GetIgnite().GetOrCreateCache<int, ArrayList>(TestUtils.TestName);
+            var inner = new object();
+
+            cache.Put(1, new ArrayList
             {
-                ignite.CreateCache<int, object>("default");
+                new InnerObject {Inner = inner},
+                new InnerObject {Inner = inner}
+            });
+
+            var res = cache.Get(1);
+            Assert.AreEqual(2, res.Count);
+            Assert.AreNotSame(((InnerObject)res[0]).Inner, ((InnerObject)res[1]).Inner);
+        }
 
-                // Basic types.
-                // Types which map directly to Java are returned properly when retrieved as object.
-                // Non-directly mapped types are returned as their counterpart.
-                CheckValueCaching((char) 128);
-                CheckValueCaching((byte) 255);
-                CheckValueCaching((sbyte) -10, false);
-                CheckValueCaching((short) -32000);
-                CheckValueCaching((ushort) 65350, false);
-                CheckValueCaching(int.MinValue);
-                CheckValueCaching(uint.MaxValue, false);
-                CheckValueCaching(long.MinValue);
-                CheckValueCaching(ulong.MaxValue, false);
+        /// <summary>
+        /// Tests ArrayList of objects with shared object instance.
+        /// </summary>
+        [Test]
+        public void TestListOfObjectsWithSharedObjectProperty()
+        {
+            var cache = Ignition.GetIgnite().GetOrCreateCache<int, List<InnerObject>>(TestUtils.TestName);
+            var inner = new object();
 
-                CheckValueCaching((float) 1.1);
-                CheckValueCaching(2.2);
+            cache.Put(1, new List<InnerObject>
+            {
+                new InnerObject {Inner = inner},
+                new InnerObject {Inner = inner}
+            });
 
-                CheckValueCaching((decimal) 3.3, asArray: false);
-                CheckValueCaching(Guid.NewGuid(), asArray: false);
-                CheckValueCaching(DateTime.Now, asArray: false);
+            var res = cache.Get(1);
+            Assert.AreEqual(2, res.Count);
+            Assert.AreSame(res[0].Inner, res[1].Inner);
+        }
 
-                CheckValueCaching("foobar");
+        /// <summary>
+        /// Tests ArrayList of objects with shared object instance.
+        /// </summary>
+        [Test]
+        public void TestHashtableOfObjectsWithSharedObjectProperty()
+        {
+            var cache = Ignition.GetIgnite().GetOrCreateCache<int, Hashtable>(TestUtils.TestName);
+            var inner = new object();
+
+            cache.Put(1, new Hashtable
+            {
+                {0, new InnerObject {Inner = inner}},
+                {1, new InnerObject {Inner = inner}},
+            });
+
+            var res = cache.Get(1);
+            Assert.AreEqual(2, res.Count);
+            Assert.AreNotSame(((InnerObject)res[0]).Inner, ((InnerObject)res[1]).Inner);
+        }
+
+        /// <summary>
+        /// Tests ArrayList of objects with shared object instance.
+        /// </summary>
+        [Test]
+        public void TestDictionaryOfObjectsWithSharedObjectProperty()
+        {
+            var cache = Ignition.GetIgnite().GetOrCreateCache<int, Dictionary<int, InnerObject>>(TestUtils.TestName);
+            var inner = new object();
 
-                // Special arrays.
-                CheckValueCaching(new[] {Guid.Empty, Guid.NewGuid()}, false);
-                CheckValueCaching(new Guid?[] {Guid.Empty, Guid.NewGuid()});
+            cache.Put(1, new Dictionary<int, InnerObject>
+            {
+                {0, new InnerObject {Inner = inner}},
+                {1, new InnerObject {Inner = inner}},
+            });
 
-                CheckValueCaching(new[] {1.2m, -3.4m}, false);
-                CheckValueCaching(new decimal?[] {1.2m, -3.4m});
+            var res = cache.Get(1);
+            Assert.AreEqual(2, res.Count);
+            Assert.AreSame(res[0].Inner, res[1].Inner);
+        }
 
-                CheckValueCaching(new[] {DateTime.Now}, false);
+        /// <summary>
+        /// Tests array of objects with a nested array with a shared element.
+        /// </summary>
+        [Test]
+        public void TestArrayOfObjectsWithSharedArrayElement()
+        {
+            var cache = Ignition.GetIgnite().GetOrCreateCache<int, InnerArray[]>(TestUtils.TestName);
+            var innerObj = new object();
+            var inner = new[] {innerObj};
 
-                // Custom types.
-                CheckValueCaching(new Foo {X = 10}, asArray: false);
-                CheckValueCaching(new Bar {X = 20}, asArray: false);
+            cache.Put(1, new[]
+            {
+                new InnerArray {Inner = inner},
+                new InnerArray {Inner = inner}
+            });
 
-                // Collections.
-                CheckValueCaching(new List<Foo>(GetFoo()));
-                CheckValueCaching(new List<Bar>(GetBar()));
+            var res = cache.Get(1);
+            Assert.AreEqual(2, res.Length);
+            Assert.AreNotSame(res[0].Inner[0], res[1].Inner[0]);
+        }
 
-                CheckValueCaching(new HashSet<Foo>(GetFoo()));
-                CheckValueCaching(new HashSet<Bar>(GetBar()));
+        /// <summary>
+        /// Tests array of objects with a nested array with a reference loop.
+        /// </summary>
+        [Test]
+        public void TestArrayOfObjectsWithSharedArrayElementAndReferenceLoop()
+        {
+            var cache = Ignition.GetIgnite().GetOrCreateCache<int, InnerArray[]>(TestUtils.TestName);
+            var inner = new object[] {null};
+            inner[0] = inner;
 
-                CheckValueCaching(GetFoo().ToDictionary(x => x.X, x => x));
-                CheckValueCaching(GetBar().ToDictionary(x => x.X, x => x));
+            cache.Put(1, new[]
+            {
+                new InnerArray {Inner = inner},
+                new InnerArray {Inner = inner}
+            });
 
-                // Custom type arrays.
-                // Array type is lost, because in binary mode on Java side we receive the value as Object[].
-                CheckValueCaching(new[] {new Foo {X = -1}, new Foo {X = 1}}, false);
-                CheckValueCaching(new[] {new Bar {X = -10}, new Bar {X = 10}}, false);
-            }
+            var res = cache.Get(1);
+            Assert.AreEqual(2, res.Length);
+            Assert.AreNotSame(res[0], res[1]);
+            Assert.AreSame(res[0].Inner, res[0].Inner[0]);
         }
 
         /// <summary>
@@ -96,7 +277,7 @@ namespace Apache.Ignite.Core.Tests.Binary
         /// </summary>
         private static void CheckValueCaching<T>(T val, bool asObject = true, bool asArray = true)
         {
-            var cache = Ignition.GetIgnite().GetCache<int, T>("default");
+            var cache = Ignition.GetIgnite().GetCache<int, T>(CacheName);
 
             cache[1] = val;
             Assert.AreEqual(val, cache[1]);
@@ -179,5 +360,23 @@ namespace Apache.Ignite.Core.Tests.Binary
                 return X;
             }
         }
+
+        /** */
+        private class InnerList
+        {
+            public IList<object> Inner { get; set; }
+        }
+
+        /** */
+        private class InnerObject
+        {
+            public object Inner { get; set; }
+        }
+
+        /** */
+        private class InnerArray
+        {
+            public object[] Inner { get; set; }
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryUtils.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryUtils.cs
index 92c6eaa..caab8b1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryUtils.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryUtils.cs
@@ -1084,7 +1084,7 @@ namespace Apache.Ignite.Core.Impl.Binary
             stream.WriteInt(val.Length);
 
             for (int i = 0; i < val.Length; i++)
-                ctx.Write(val.GetValue(i));
+                ctx.WriteObjectDetached(val.GetValue(i), parentCollection: val);
         }
 
         /// <summary>
@@ -1261,7 +1261,7 @@ namespace Apache.Ignite.Core.Impl.Binary
             ctx.Stream.WriteByte(colType);
 
             foreach (object elem in val)
-                ctx.Write(elem);
+                ctx.WriteObjectDetached(elem, parentCollection: val);
         }
 
         /**
@@ -1342,8 +1342,8 @@ namespace Apache.Ignite.Core.Impl.Binary
 
             foreach (DictionaryEntry entry in val)
             {
-                ctx.Write(entry.Key);
-                ctx.Write(entry.Value);
+                ctx.WriteObjectDetached(entry.Key, parentCollection: val);
+                ctx.WriteObjectDetached(entry.Value, parentCollection: val);
             }
         }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryWriter.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryWriter.cs
index a39bcd7..a9b0fdf 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryWriter.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryWriter.cs
@@ -51,8 +51,8 @@ namespace Apache.Ignite.Core.Impl.Binary
         /** Current stack frame. */
         private Frame _frame;
 
-        /** Whether we are currently detaching an object. */
-        private bool _detaching;
+        /** Whether we are currently detaching an object: detachment root when true, null otherwise. */
+        private object _detaching;
 
         /** Whether we are directly within peer loading object holder. */
         private bool _isInWrapper;
@@ -627,7 +627,7 @@ namespace Apache.Ignite.Core.Impl.Binary
                 BinaryUtils.WriteDecimalArray(val, _stream);
             }
         }
-        
+
         /// <summary>
         /// Write decimal array.
         /// </summary>
@@ -660,7 +660,7 @@ namespace Apache.Ignite.Core.Impl.Binary
                 BinaryUtils.WriteTimestamp(val.Value, _stream, _marsh.TimestampConverter);
             }
         }
-        
+
         /// <summary>
         /// Write date value.
         /// </summary>
@@ -908,7 +908,7 @@ namespace Apache.Ignite.Core.Impl.Binary
                 // Save enum fields only once - they can't change locally at runtime.
                 var metaHnd = _marsh.GetBinaryTypeHandler(desc);
                 var binaryFields = metaHnd.OnObjectWriteFinished();
-                
+
                 SaveMetadata(desc, binaryFields);
             }
         }
@@ -1163,7 +1163,7 @@ namespace Apache.Ignite.Core.Impl.Binary
                 return;
             }
 
-            // We use GetType() of a real object instead of typeof(T) to take advantage of 
+            // We use GetType() of a real object instead of typeof(T) to take advantage of
             // automatic Nullable'1 unwrapping.
             Type type = obj.GetType();
 
@@ -1250,7 +1250,7 @@ namespace Apache.Ignite.Core.Impl.Binary
                 var schemaOffset = dataEnd - pos;
 
                 int schemaId;
-                    
+
                 var flags = desc.UserType
                     ? BinaryObjectHeader.Flag.UserType
                     : BinaryObjectHeader.Flag.None;
@@ -1460,17 +1460,25 @@ namespace Apache.Ignite.Core.Impl.Binary
         /// <summary>
         /// Perform action with detached semantics.
         /// </summary>
-        internal void WriteObjectDetached<T>(T o)
+        /// <param name="o">Object to write.</param>
+        /// <param name="parentCollection">
+        /// Hack for collections. When the root object for the current writer is a known collection type
+        /// (<see cref="BinaryTypeId.Array"/>, <see cref="BinaryTypeId.Collection"/>,
+        /// <see cref="BinaryTypeId.Dictionary"/>), we want to detach every element of that collection, because
+        /// Java side handles every element as a separate BinaryObject - they can't share handles.
+        /// </param>
+        internal void WriteObjectDetached<T>(T o, object parentCollection = null)
         {
-            if (_detaching)
+            if (_detaching != parentCollection)
             {
                 Write(o);
             }
             else
             {
-                _detaching = true;
+                var oldDetaching = _detaching;
+                _detaching = _detaching ?? o;
 
-                BinaryHandleDictionary<object, long> oldHnds = _hnds;
+                var oldHnds = _hnds;
                 _hnds = null;
 
                 try
@@ -1479,7 +1487,7 @@ namespace Apache.Ignite.Core.Impl.Binary
                 }
                 finally
                 {
-                    _detaching = false;
+                    _detaching = oldDetaching;
 
                     if (oldHnds != null)
                     {