IGNITE-6627 .NET: Fix serialization of enums within generic collections
authorAlexey Popov <tank2.alex@gmail.com>
Tue, 17 Oct 2017 11:45:42 +0000 (14:45 +0300)
committerPavel Tupitsyn <ptupitsyn@apache.org>
Tue, 17 Oct 2017 11:45:42 +0000 (14:45 +0300)
* Fix EnumEqualityComparer serialization
* Fix enum arrays serialization
* Fix empty objects missing metadata

This closes #2864

modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/Serializable/GenericCollectionsTest.cs [new file with mode: 0644]
modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTest.cs
modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/EmptyObject.cs [new file with mode: 0644]
modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinarySystemHandlers.cs
modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/BinaryWriter.cs
modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/SerializableSerializer.cs
modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/Structure/BinaryStructureTracker.cs

index ec85ca2..7ec75af 100644 (file)
@@ -78,6 +78,7 @@
     <Compile Include="Binary\BinaryReaderWriterTest.cs" />
     <Compile Include="Binary\BinarySelfTestSimpleName.cs" />
     <Compile Include="Binary\EnumsTestOnline.cs" />
+    <Compile Include="Binary\Serializable\GenericCollectionsTest.cs" />
     <Compile Include="Cache\PersistentStoreTest.cs" />
     <Compile Include="Cache\Query\Linq\CacheLinqTest.CompiledQuery.cs" />
     <Compile Include="Cache\Query\Linq\CacheLinqTest.DateTime.cs" />
@@ -94,6 +95,7 @@
     <Compile Include="Cache\Store\CacheStoreSessionTestSharedFactory.cs" />
     <Compile Include="Client\Cache\CacheTest.cs" />
     <Compile Include="Client\Cache\CacheTestNoMeta.cs" />
+    <Compile Include="Client\Cache\EmptyObject.cs" />
     <Compile Include="Client\Cache\ScanQueryTest.cs" />
     <Compile Include="Client\Cache\Person.cs" />
     <Compile Include="Client\ClientTestBase.cs" />
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/Serializable/GenericCollectionsTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Binary/Serializable/GenericCollectionsTest.cs
new file mode 100644 (file)
index 0000000..cfbe824
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Core.Tests.Binary.Serializable
+{
+    using System.Collections.Generic;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests Generic collections serializtion/deserialization scenarios.
+    /// </summary>
+    public class GenericCollectionsTest
+    {
+        /// <summary>
+        /// Tests Dictionary.
+        /// </summary>
+        [Test]
+        public void TestDictionary()
+        {
+            TestCollection(new Dictionary<int, int> {{1, 1}, {2, 2}});
+            TestCollection(new Dictionary<ByteEnum, int> {{ByteEnum.One, 1}, {ByteEnum.Two, 2}});
+            TestCollection(new Dictionary<IntEnum, int> {{IntEnum.One, 1}, {IntEnum.Two, 2}});
+        }
+
+        /// <summary>
+        /// Tests SortedDictionary.
+        /// </summary>
+        [Test]
+        public void TestSortedDictionary()
+        {
+            TestCollection(new SortedDictionary<int, int> {{1, 1}, {2, 2}});
+            TestCollection(new SortedDictionary<ByteEnum, int> {{ByteEnum.One, 1}, {ByteEnum.Two, 2}});
+            TestCollection(new SortedDictionary<IntEnum, int> {{IntEnum.One, 1}, {IntEnum.Two, 2}});
+        }
+
+        /// <summary>
+        /// Tests List.
+        /// </summary>
+        [Test]
+        public void TestList()
+        {
+            TestCollection(new List<int> {1, 2});
+            TestCollection(new List<ByteEnum> {ByteEnum.One, ByteEnum.Two});
+            TestCollection(new List<IntEnum> {IntEnum.One, IntEnum.Two});
+        }
+
+        /// <summary>
+        /// Tests LinkedList.
+        /// </summary>
+        [Test]
+        public void TestLinkedList()
+        {
+            TestCollection(new LinkedList<int>(new List<int> { 1, 2 }));
+            TestCollection(new LinkedList<ByteEnum>(new List<ByteEnum> {ByteEnum.One, ByteEnum.Two}));
+            TestCollection(new LinkedList<IntEnum>(new List<IntEnum> {IntEnum.One, IntEnum.Two}));
+        }
+
+        /// <summary>
+        /// Tests HashSet.
+        /// </summary>
+        [Test]
+        public void TestHashSet()
+        {
+            TestCollection(new HashSet<int> {1, 2});
+            TestCollection(new HashSet<ByteEnum> {ByteEnum.One, ByteEnum.Two});
+            TestCollection(new HashSet<IntEnum> {IntEnum.One, IntEnum.Two});
+        }
+
+        /// <summary>
+        /// Tests SortedSet.
+        /// </summary>
+        [Test]
+        public void TestSortedSet()
+        {
+            TestCollection(new SortedSet<int> {1, 2});
+            TestCollection(new SortedSet<ByteEnum> {ByteEnum.One, ByteEnum.Two});
+            TestCollection(new SortedSet<IntEnum> {IntEnum.One, IntEnum.Two});
+        }
+
+        private static void TestCollection<T>(ICollection<T> collection)
+        {
+            var res = TestUtils.SerializeDeserialize(collection);
+            Assert.AreEqual(collection, res);
+        }
+
+        private enum ByteEnum : byte
+        {
+            One = 1,
+            Two = 2,
+        }
+
+        private enum IntEnum 
+        {
+            One = 1,
+            Two = 2,
+        }
+    }
+}
index 083038a..f2dd1de 100644 (file)
@@ -68,6 +68,22 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
         }
 
         /// <summary>
+        /// Tests the cache put / get for Empty object type.
+        /// </summary>
+        [Test]
+        public void TestPutGetEmptyObject()
+        {
+            using (var client = GetClient())
+            {
+                var serverCache = GetCache<EmptyObject>();
+                var clientCache = client.GetCache<int, EmptyObject>(CacheName);
+
+                serverCache.Put(1, new EmptyObject());
+                Assert.IsNotNull(clientCache.Get(1));
+            }
+        }
+
+        /// <summary>
         /// Tests the cache put / get with user data types.
         /// </summary>
         [Test]
@@ -116,6 +132,60 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
         }
 
         /// <summary>
+        /// Tests the cache put / get for Dictionary with Enum keys.
+        /// </summary>
+        [Test]
+        public void TestPutGetDictionary([Values(true, false)] bool compactFooter)
+        {
+            var cfg = GetClientConfiguration();
+
+            cfg.BinaryConfiguration = new BinaryConfiguration
+            {
+                CompactFooter = compactFooter
+            };
+
+            using (var client = Ignition.StartClient(cfg))
+            {
+                var dict = new Dictionary<ByteEnum, int> { { ByteEnum.One, 1 }, { ByteEnum.Two, 2 } };
+
+                var serverCache = GetCache<Dictionary<ByteEnum, int>>();
+                var clientCache = client.GetCache<int, Dictionary<ByteEnum, int>>(CacheName);
+
+                serverCache.Put(1, dict);
+                var res = clientCache.Get(1);
+
+                Assert.AreEqual(dict, res);
+            }
+        }
+
+        /// <summary>
+        /// Tests the cache put / get for HashSet with Enum keys.
+        /// </summary>
+        [Test]
+        public void TestPutGetHashSet([Values(true, false)] bool compactFooter)
+        {
+            var cfg = GetClientConfiguration();
+
+            cfg.BinaryConfiguration = new BinaryConfiguration
+            {
+                CompactFooter = compactFooter
+            };
+
+            using (var client = Ignition.StartClient(cfg))
+            {
+                var hashSet = new HashSet<ByteEnum> { ByteEnum.One, ByteEnum.Two };
+
+                var serverCache = GetCache<HashSet<ByteEnum>>();
+                var clientCache = client.GetCache<int, HashSet<ByteEnum>>(CacheName);
+
+                serverCache.Put(1, hashSet);
+                var res = clientCache.Get(1);
+
+                Assert.AreEqual(hashSet, res);
+            }
+        }
+
+        /// <summary>
         /// Tests the TryGet method.
         /// </summary>
         [Test]
@@ -779,5 +849,11 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
         {
             public Container Inner;
         }
+
+        public enum ByteEnum : byte
+        {
+            One = 1,
+            Two = 2,
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/EmptyObject.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/EmptyObject.cs
new file mode 100644 (file)
index 0000000..47db939
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Core.Tests.Client.Cache
+{
+    using System;
+    using System.Runtime.Serialization;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Object with no fields.
+    /// </summary>
+    [Serializable]
+    public class EmptyObject : ISerializable
+    {
+        /// <summary>
+        /// Initializes a new instance of the EmptyObject class.
+        /// </summary>
+        public EmptyObject()
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the EmptyObject class.
+        /// </summary>
+        private EmptyObject(SerializationInfo info, StreamingContext context)
+        {
+            Assert.AreEqual(StreamingContextStates.All, context.State);
+            Assert.IsNull(context.Context);
+        }
+
+        /** <inheritdoc /> */
+        public void GetObjectData(SerializationInfo info, StreamingContext context)
+        {
+            Assert.AreEqual(StreamingContextStates.All, context.State);
+            Assert.IsNull(context.Context);
+        }
+    }
+}
index f55a11f..3f16bc0 100644 (file)
@@ -110,9 +110,9 @@ namespace Apache.Ignite.Core.Impl.Binary
 
             // 13. Arbitrary dictionary.
             ReadHandlers[BinaryTypeId.Dictionary] = new BinarySystemReader(ReadDictionary);
-            
-            // 14. Enum.
-            ReadHandlers[BinaryTypeId.ArrayEnum] = new BinarySystemReader(ReadEnumArray);
+
+            // 14. Enum. Should be read as Array, see WriteEnumArray implementation.
+            ReadHandlers[BinaryTypeId.ArrayEnum] = new BinarySystemReader(ReadArray);
         }
 
         /// <summary>
@@ -473,16 +473,6 @@ namespace Apache.Ignite.Core.Impl.Binary
             ctx.WriteInt(binEnum.EnumValue);
         }
 
-        /**
-         * <summary>Read enum array.</summary>
-         */
-        private static object ReadEnumArray(BinaryReader ctx, Type type)
-        {
-            var elemType = type.GetElementType() ?? typeof(object);
-
-            return BinaryUtils.ReadTypedArray(ctx, true, elemType);
-        }
-
         /// <summary>
         /// Reads the array.
         /// </summary>
index f59f17c..b98ad5f 100644 (file)
@@ -1493,6 +1493,13 @@ namespace Apache.Ignite.Core.Impl.Binary
         {
             Debug.Assert(desc != null);
 
+            if (!desc.UserType && (fields == null || fields.Count == 0))
+            {
+                // System types with no fields (most of them) do not need to be sent.
+                // AffinityKey is an example of system type with metadata.
+                return;
+            }
+
             if (_metas == null)
             {
                 _metas = new Dictionary<int, BinaryType>(1)
index e660cff..80f267a 100644 (file)
@@ -304,9 +304,16 @@ namespace Apache.Ignite.Core.Impl.Binary
             {
                 return new TypeResolver().ResolveType(serInfo.FullTypeName, serInfo.AssemblyName);
             }
-            
-            if (serInfo.ObjectType != serializable.GetType())
+
+            if (serInfo.ObjectType != serializable.GetType() &&
+                typeof(ISerializable).IsAssignableFrom(serInfo.ObjectType))
             {
+                // serInfo.ObjectType should be ISerializable. There is a known case for generic collections:
+                // serializable is EnumEqualityComparer : ISerializable 
+                // and serInfo.ObjectType is ObjectEqualityComparer (does not implement ISerializable interface).
+                // Please read a possible explanation here:
+                // http://dotnetstudio.blogspot.ru/2012/06/net-35-to-net-40-enum.html
+
                 return serInfo.ObjectType;
             }
 
index 8f44e00..3517342 100644 (file)
@@ -110,11 +110,21 @@ namespace Apache.Ignite.Core.Impl.Binary.Structure
 
                     var fields = metaHnd.OnObjectWriteFinished();
 
-                    // A new schema may be added, but no new fields. 
+                    // A new schema may be added, but no new fields.
                     // In this case, we should still call SaveMetadata even if fields are null
                     writer.SaveMetadata(_desc, fields);
                 }
             }
+            else
+            {
+                // Special case when the object is with no properties.
+                // Save meta to Marshaller.
+                writer.Marshaller.GetBinaryTypeHandler(_desc);
+
+                // Save meta to cluster.
+                writer.SaveMetadata(_desc, null);
+                return;
+            }
         }
 
         /// <summary>