You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@avro.apache.org by bl...@apache.org on 2019/11/05 01:36:02 UTC

[avro] branch master updated: AVRO-2606: Fix C# multidimensional array errors (#699)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 62bdc83  AVRO-2606: Fix C# multidimensional array errors (#699)
62bdc83 is described below

commit 62bdc83e46fbd65d34a1715c1bcc53fa5d1919fa
Author: Brian Lachniet <bl...@gmail.com>
AuthorDate: Mon Nov 4 20:35:54 2019 -0500

    AVRO-2606: Fix C# multidimensional array errors (#699)
    
    Fix errors surrounding the use of multidimensional arrays of custom
    record types in the C# specific API.
---
 .../src/apache/main/Specific/ObjectCreator.cs      |  71 +++++++++-
 .../test/Specific/EmbeddedGenericRecordUser.cs     |  73 ++++++++++
 .../apache/test/Specific/EmbeddedGenericsRecord.cs |  54 +++++++-
 .../src/apache/test/Specific/ObjectCreatorTests.cs |  34 +++--
 .../src/apache/test/Specific/SpecificTests.cs      | 148 +++++++++++++++++++--
 5 files changed, 344 insertions(+), 36 deletions(-)

diff --git a/lang/csharp/src/apache/main/Specific/ObjectCreator.cs b/lang/csharp/src/apache/main/Specific/ObjectCreator.cs
index b0cf9ef..950d111 100644
--- a/lang/csharp/src/apache/main/Specific/ObjectCreator.cs
+++ b/lang/csharp/src/apache/main/Specific/ObjectCreator.cs
@@ -44,6 +44,11 @@ namespace Avro.Specific
         private readonly Type GenericListType = typeof(List<>);
 
         /// <summary>
+        /// Static generic list type used for creating new IList instances
+        /// </summary>
+        private readonly Type GenericIListType = typeof(IList<>);
+
+        /// <summary>
         /// Static generic nullable type used for creating new nullable instances
         /// </summary>
         private readonly Type GenericNullableType = typeof(Nullable<>);
@@ -136,11 +141,15 @@ namespace Avro.Specific
             {
                 Type type = null;
 
-                // Modify provided type to ensure it can be discovered.
-                // This is mainly for Generics, and Nullables.
-                name = name.Replace("Nullable<", "Nullable`1[");
-                name = name.Replace("IList<", "System.Collections.Generic.IList`1[");
-                name = name.Replace(">", "]");
+                if (TryGetIListItemTypeName(name, out var itemTypeName))
+                {
+                    return GenericIListType.MakeGenericType(FindType(itemTypeName));
+                }
+
+                if (TryGetNullableItemTypeName(name, out itemTypeName))
+                {
+                    return GenericNullableType.MakeGenericType(FindType(itemTypeName));
+                }
 
                 // if entry assembly different from current assembly, try entry assembly first
                 if (diffAssembly)
@@ -186,6 +195,58 @@ namespace Avro.Specific
             });
         }
 
+        private bool TryGetIListItemTypeName(string name, out string itemTypeName)
+        {
+            const string listPrefix = "IList<";
+            const string fullListPrefix = "System.Collections.Generic.IList<";
+
+            if (!name.EndsWith(">", StringComparison.Ordinal))
+            {
+                itemTypeName = null;
+                return false;
+            }
+
+            if (name.StartsWith(fullListPrefix, StringComparison.Ordinal))
+            {
+                itemTypeName = name.Substring(
+                    fullListPrefix.Length, name.Length - fullListPrefix.Length - 1);
+                return true;
+            }
+
+            if (name.StartsWith(listPrefix, StringComparison.Ordinal))
+            {
+                itemTypeName = name.Substring(
+                    listPrefix.Length, name.Length - listPrefix.Length - 1);
+                return true;
+            }
+
+            itemTypeName = null;
+            return false;
+        }
+
+        private bool TryGetNullableItemTypeName(string name, out string itemTypeName)
+        {
+            const string nullablePrefix = "Nullable<";
+            const string fullNullablePrefix = "System.Nullable<";
+
+            if (name.StartsWith(fullNullablePrefix, StringComparison.Ordinal))
+            {
+                itemTypeName = name.Substring(
+                    fullNullablePrefix.Length, name.Length - fullNullablePrefix.Length - 1);
+                return true;
+            }
+
+            if (name.StartsWith(nullablePrefix, StringComparison.Ordinal))
+            {
+                itemTypeName = name.Substring(
+                    nullablePrefix.Length, name.Length - nullablePrefix.Length - 1);
+                return true;
+            }
+
+            itemTypeName = null;
+            return false;
+        }
+
         /// <summary>
         /// Gets the type for the specified schema
         /// </summary>
diff --git a/lang/csharp/src/apache/test/Specific/EmbeddedGenericRecordUser.cs b/lang/csharp/src/apache/test/Specific/EmbeddedGenericRecordUser.cs
new file mode 100644
index 0000000..149ba94
--- /dev/null
+++ b/lang/csharp/src/apache/test/Specific/EmbeddedGenericRecordUser.cs
@@ -0,0 +1,73 @@
+/**
+ * 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
+ *
+ *     https://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.
+ */
+// ------------------------------------------------------------------------------
+// <auto-generated>
+//    Generated by avrogen, version 1.10.0.0
+//    Changes to this file may cause incorrect behavior and will be lost if code
+//    is regenerated
+// </auto-generated>
+// ------------------------------------------------------------------------------
+namespace Avro.Test.Specific
+{
+	using System;
+	using System.Collections.Generic;
+	using System.Text;
+	using Avro;
+	using Avro.Specific;
+	
+	public partial class EmbeddedGenericRecordUser : ISpecificRecord
+	{
+		public static Schema _SCHEMA = Avro.Schema.Parse("{\"type\":\"record\",\"name\":\"EmbeddedGenericRecordUser\",\"namespace\":\"Avro.Test.Specif" +
+				"ic\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"}]}");
+		private string _name;
+		public virtual Schema Schema
+		{
+			get
+			{
+				return EmbeddedGenericRecordUser._SCHEMA;
+			}
+		}
+		public string name
+		{
+			get
+			{
+				return this._name;
+			}
+			set
+			{
+				this._name = value;
+			}
+		}
+		public virtual object Get(int fieldPos)
+		{
+			switch (fieldPos)
+			{
+			case 0: return this.name;
+			default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Get()");
+			};
+		}
+		public virtual void Put(int fieldPos, object fieldValue)
+		{
+			switch (fieldPos)
+			{
+			case 0: this.name = (System.String)fieldValue; break;
+			default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Put()");
+			};
+		}
+	}
+}
diff --git a/lang/csharp/src/apache/test/Specific/EmbeddedGenericsRecord.cs b/lang/csharp/src/apache/test/Specific/EmbeddedGenericsRecord.cs
index c6595a2..82b5959 100644
--- a/lang/csharp/src/apache/test/Specific/EmbeddedGenericsRecord.cs
+++ b/lang/csharp/src/apache/test/Specific/EmbeddedGenericsRecord.cs
@@ -1,4 +1,4 @@
-/*
+/**
  * 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
@@ -32,11 +32,14 @@ namespace Avro.Test.Specific
 	
 	public partial class EmbeddedGenericsRecord : ISpecificRecord
 	{
-		public static Schema _SCHEMA = Avro.Schema.Parse(@"{""type"":""record"",""name"":""EmbeddedGenericsRecord"",""namespace"":""Avro.Test.Specific"",""fields"":[{""name"":""OptionalInt"",""type"":[""null"",""int""]},{""name"":""OptionalIntList"",""type"":{""type"":""array"",""items"":[""null"",""int""]}},{""name"":""OptionalIntMatrix"",""type"":{""type"":""array"",""items"":{""type"":""array"",""items"":{""type"":""array"",""items"":[""null"",""int""]}}}},{""name"":""IntMatrix"",""type"":{ [...]
+		public static Schema _SCHEMA = Avro.Schema.Parse(@"{""type"":""record"",""name"":""EmbeddedGenericsRecord"",""namespace"":""Avro.Test.Specific"",""fields"":[{""name"":""OptionalInt"",""type"":[""null"",""int""]},{""name"":""OptionalIntList"",""type"":{""type"":""array"",""items"":[""null"",""int""]}},{""name"":""OptionalUserList"",""type"":{""type"":""array"",""items"":[""null"",{""type"":""record"",""name"":""EmbeddedGenericRecordUser"",""namespace"":""Avro.Test.Specific"",""fields"": [...]
 		private System.Nullable<System.Int32> _OptionalInt;
 		private IList<System.Nullable<System.Int32>> _OptionalIntList;
+		private IList<Avro.Test.Specific.EmbeddedGenericRecordUser> _OptionalUserList;
 		private IList<IList<IList<System.Nullable<System.Int32>>>> _OptionalIntMatrix;
+		private IList<IList<IList<Avro.Test.Specific.EmbeddedGenericRecordUser>>> _OptionalUserMatrix;
 		private IList<IList<IList<System.Int32>>> _IntMatrix;
+		private IList<IList<IList<Avro.Test.Specific.EmbeddedGenericRecordUser>>> _UserMatrix;
 		public virtual Schema Schema
 		{
 			get
@@ -66,6 +69,17 @@ namespace Avro.Test.Specific
 				this._OptionalIntList = value;
 			}
 		}
+		public IList<Avro.Test.Specific.EmbeddedGenericRecordUser> OptionalUserList
+		{
+			get
+			{
+				return this._OptionalUserList;
+			}
+			set
+			{
+				this._OptionalUserList = value;
+			}
+		}
 		public IList<IList<IList<System.Nullable<System.Int32>>>> OptionalIntMatrix
 		{
 			get
@@ -77,6 +91,17 @@ namespace Avro.Test.Specific
 				this._OptionalIntMatrix = value;
 			}
 		}
+		public IList<IList<IList<Avro.Test.Specific.EmbeddedGenericRecordUser>>> OptionalUserMatrix
+		{
+			get
+			{
+				return this._OptionalUserMatrix;
+			}
+			set
+			{
+				this._OptionalUserMatrix = value;
+			}
+		}
 		public IList<IList<IList<System.Int32>>> IntMatrix
 		{
 			get
@@ -88,14 +113,28 @@ namespace Avro.Test.Specific
 				this._IntMatrix = value;
 			}
 		}
+		public IList<IList<IList<Avro.Test.Specific.EmbeddedGenericRecordUser>>> UserMatrix
+		{
+			get
+			{
+				return this._UserMatrix;
+			}
+			set
+			{
+				this._UserMatrix = value;
+			}
+		}
 		public virtual object Get(int fieldPos)
 		{
 			switch (fieldPos)
 			{
 			case 0: return this.OptionalInt;
 			case 1: return this.OptionalIntList;
-			case 2: return this.OptionalIntMatrix;
-			case 3: return this.IntMatrix;
+			case 2: return this.OptionalUserList;
+			case 3: return this.OptionalIntMatrix;
+			case 4: return this.OptionalUserMatrix;
+			case 5: return this.IntMatrix;
+			case 6: return this.UserMatrix;
 			default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Get()");
 			};
 		}
@@ -105,8 +144,11 @@ namespace Avro.Test.Specific
 			{
 			case 0: this.OptionalInt = (System.Nullable<System.Int32>)fieldValue; break;
 			case 1: this.OptionalIntList = (IList<System.Nullable<System.Int32>>)fieldValue; break;
-			case 2: this.OptionalIntMatrix = (IList<IList<IList<System.Nullable<System.Int32>>>>)fieldValue; break;
-			case 3: this.IntMatrix = (IList<IList<IList<System.Int32>>>)fieldValue; break;
+			case 2: this.OptionalUserList = (IList<Avro.Test.Specific.EmbeddedGenericRecordUser>)fieldValue; break;
+			case 3: this.OptionalIntMatrix = (IList<IList<IList<System.Nullable<System.Int32>>>>)fieldValue; break;
+			case 4: this.OptionalUserMatrix = (IList<IList<IList<Avro.Test.Specific.EmbeddedGenericRecordUser>>>)fieldValue; break;
+			case 5: this.IntMatrix = (IList<IList<IList<System.Int32>>>)fieldValue; break;
+			case 6: this.UserMatrix = (IList<IList<IList<Avro.Test.Specific.EmbeddedGenericRecordUser>>>)fieldValue; break;
 			default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Put()");
 			};
 		}
diff --git a/lang/csharp/src/apache/test/Specific/ObjectCreatorTests.cs b/lang/csharp/src/apache/test/Specific/ObjectCreatorTests.cs
index f17ff55..d6b29d5 100644
--- a/lang/csharp/src/apache/test/Specific/ObjectCreatorTests.cs
+++ b/lang/csharp/src/apache/test/Specific/ObjectCreatorTests.cs
@@ -62,22 +62,34 @@ namespace Avro.Test.Specific
                 objectCreator.GetType("ThisTypeDoesNotExist", Schema.Type.Record));
         }
 
-        [Test]
-        public void TestGetType()
+        [TestCase("Foo", Schema.Type.Record, typeof(Foo))]
+        public void TestGetTypeEquals(string name, Schema.Type schemaType, Type expected)
         {
             var objectCreator = new ObjectCreator();
+            var actual = objectCreator.GetType(name, Schema.Type.Record);
 
-            // Single Foo
-            Assert.AreEqual(typeof(Foo),
-                objectCreator.GetType("Foo", Schema.Type.Record));
+            Assert.AreEqual(expected, actual);
+        }
 
-            // Array of Foo
-            Assert.True(typeof(IList<Foo>).IsAssignableFrom(
-                objectCreator.GetType("Foo", Schema.Type.Array)));
+        [TestCase("Foo", Schema.Type.Array, typeof(IList<Foo>))]
+        [TestCase("IList<Foo>", Schema.Type.Array, typeof(IList<IList<Foo>>))]
+        [TestCase("IList<IList<IList<Foo>>>", Schema.Type.Array, typeof(IList<IList<IList<IList<Foo>>>>))]
+        [TestCase("System.Collections.Generic.IList<System.Collections.Generic.IList<System.Collections.Generic.IList<Foo>>>", Schema.Type.Array, typeof(IList<IList<IList<IList<Foo>>>>))]
+        [TestCase("Foo", Schema.Type.Map, typeof(IDictionary<string, Foo>))]
+        [TestCase("Nullable<Int32>", Schema.Type.Array, typeof(IList<Nullable<int>>))]
+        [TestCase("System.Nullable<Int32>", Schema.Type.Array, typeof(IList<int?>))]
+        [TestCase("IList<Nullable<Int32>>", Schema.Type.Array, typeof(IList<IList<int?>>))]
+        [TestCase("IList<System.Nullable<Int32>>", Schema.Type.Array, typeof(IList<IList<int?>>))]
+        public void TestGetTypeAssignable(string name, Schema.Type schemaType, Type expected)
+        {
+            var objectCreator = new ObjectCreator();
+            var actual = objectCreator.GetType(name, schemaType);
 
-            // Map of Foo
-            Assert.True(typeof(IDictionary<string, Foo>).IsAssignableFrom(
-                objectCreator.GetType("Foo", Schema.Type.Map)));
+            Assert.True(
+                expected.IsAssignableFrom(actual),
+                "  Expected: assignable from {0}\n    But was: {1}",
+                expected,
+                actual);
         }
 
         [TestCase(typeof(MyNullableFoo), "MyNullableFoo",
diff --git a/lang/csharp/src/apache/test/Specific/SpecificTests.cs b/lang/csharp/src/apache/test/Specific/SpecificTests.cs
index 9da4b83..96d412b 100644
--- a/lang/csharp/src/apache/test/Specific/SpecificTests.cs
+++ b/lang/csharp/src/apache/test/Specific/SpecificTests.cs
@@ -254,6 +254,15 @@ namespace Avro.Test
             var srcRecord = new EmbeddedGenericsRecord
             {
                 OptionalIntList = new List<int?> { 1, 2, null, 3, null, null },
+                OptionalUserList = new List<EmbeddedGenericRecordUser>
+                {
+                    new EmbeddedGenericRecordUser { name = "1" },
+                    new EmbeddedGenericRecordUser { name = "2" },
+                    null,
+                    new EmbeddedGenericRecordUser { name = "3" },
+                    null,
+                    null,
+                },
                 OptionalIntMatrix = new List<IList<IList<int?>>>
                 {
                     new List<IList<int?>>
@@ -267,6 +276,27 @@ namespace Avro.Test
                     },
                     new List<IList<int?>> { },
                 },
+                OptionalUserMatrix = new List<IList<IList<EmbeddedGenericRecordUser>>>
+                {
+                    new List<IList<EmbeddedGenericRecordUser>>
+                    {
+                        new List<EmbeddedGenericRecordUser>
+                        {
+                            null,
+                            new EmbeddedGenericRecordUser { name = "2" },
+                        },
+                        new List<EmbeddedGenericRecordUser> { null, null },
+                    },
+                    new List<IList<EmbeddedGenericRecordUser>>
+                    {
+                        new List<EmbeddedGenericRecordUser>
+                        {
+                            new EmbeddedGenericRecordUser { name = "5" },
+                            new EmbeddedGenericRecordUser { name = "6" },
+                        },
+                    },
+                    new List<IList<EmbeddedGenericRecordUser>> { },
+                },
                 IntMatrix = new List<IList<IList<int>>>
                 {
                     new List<IList<int>>
@@ -279,6 +309,31 @@ namespace Avro.Test
                         new List<int> { 5, 6, },
                     },
                     new List<IList<int>> { },
+                },
+                UserMatrix = new List<IList<IList<EmbeddedGenericRecordUser>>>
+                {
+                    new List<IList<EmbeddedGenericRecordUser>>
+                    {
+                        new List<EmbeddedGenericRecordUser>
+                        {
+                            new EmbeddedGenericRecordUser { name = "1" },
+                            new EmbeddedGenericRecordUser { name = "2" },
+                        },
+                        new List<EmbeddedGenericRecordUser>
+                        {
+                            new EmbeddedGenericRecordUser { name = "3" },
+                            new EmbeddedGenericRecordUser { name = "4" },
+                        },
+                    },
+                    new List<IList<EmbeddedGenericRecordUser>>
+                    {
+                        new List<EmbeddedGenericRecordUser>
+                        {
+                            new EmbeddedGenericRecordUser { name = "5" },
+                            new EmbeddedGenericRecordUser { name = "6" },
+                        },
+                    },
+                    new List<IList<EmbeddedGenericRecordUser>> { },
                 }
             };
             var stream = serialize(EmbeddedGenericsRecord._SCHEMA, srcRecord);
@@ -292,6 +347,14 @@ namespace Avro.Test
             Assert.AreEqual(3, dstRecord.OptionalIntList[3]);
             Assert.AreEqual(null, dstRecord.OptionalIntList[4]);
             Assert.AreEqual(null, dstRecord.OptionalIntList[5]);
+
+            Assert.AreEqual("1", dstRecord.OptionalUserList[0].name);
+            Assert.AreEqual("2", dstRecord.OptionalUserList[1].name);
+            Assert.AreEqual(null, dstRecord.OptionalUserList[2]);
+            Assert.AreEqual("3", dstRecord.OptionalUserList[3].name);
+            Assert.AreEqual(null, dstRecord.OptionalUserList[4]);
+            Assert.AreEqual(null, dstRecord.OptionalUserList[5]);
+
             Assert.AreEqual(null, dstRecord.OptionalIntMatrix[0][0][0]);
             Assert.AreEqual(2, dstRecord.OptionalIntMatrix[0][0][1]);
             Assert.AreEqual(null, dstRecord.OptionalIntMatrix[0][1][0]);
@@ -299,6 +362,15 @@ namespace Avro.Test
             Assert.AreEqual(5, dstRecord.OptionalIntMatrix[1][0][0]);
             Assert.AreEqual(6, dstRecord.OptionalIntMatrix[1][0][1]);
             Assert.AreEqual(0, dstRecord.OptionalIntMatrix[2].Count);
+
+            Assert.AreEqual(null, dstRecord.OptionalUserMatrix[0][0][0]);
+            Assert.AreEqual("2", dstRecord.OptionalUserMatrix[0][0][1].name);
+            Assert.AreEqual(null, dstRecord.OptionalUserMatrix[0][1][0]);
+            Assert.AreEqual(null, dstRecord.OptionalUserMatrix[0][1][1]);
+            Assert.AreEqual("5", dstRecord.OptionalUserMatrix[1][0][0].name);
+            Assert.AreEqual("6", dstRecord.OptionalUserMatrix[1][0][1].name);
+            Assert.AreEqual(0, dstRecord.OptionalUserMatrix[2].Count);
+
             Assert.AreEqual(1, dstRecord.IntMatrix[0][0][0]);
             Assert.AreEqual(2, dstRecord.IntMatrix[0][0][1]);
             Assert.AreEqual(3, dstRecord.IntMatrix[0][1][0]);
@@ -306,6 +378,14 @@ namespace Avro.Test
             Assert.AreEqual(5, dstRecord.IntMatrix[1][0][0]);
             Assert.AreEqual(6, dstRecord.IntMatrix[1][0][1]);
             Assert.AreEqual(0, dstRecord.IntMatrix[2].Count);
+
+            Assert.AreEqual("1", dstRecord.UserMatrix[0][0][0].name);
+            Assert.AreEqual("2", dstRecord.UserMatrix[0][0][1].name);
+            Assert.AreEqual("3", dstRecord.UserMatrix[0][1][0].name);
+            Assert.AreEqual("4", dstRecord.UserMatrix[0][1][1].name);
+            Assert.AreEqual("5", dstRecord.UserMatrix[1][0][0].name);
+            Assert.AreEqual("6", dstRecord.UserMatrix[1][0][1].name);
+            Assert.AreEqual(0, dstRecord.UserMatrix[2].Count);
         }
 
         private static S deserialize<S>(Stream ms, Schema ws, Schema rs) where S : class, ISpecificRecord
@@ -355,6 +435,12 @@ namespace Avro.Test
 
         private static void AssertSpecificRecordEqual(ISpecificRecord rec1, ISpecificRecord rec2)
         {
+            if (rec1 == null && rec2 == null)
+            {
+                // Both are null, that's equivalent.
+                return;
+            }
+
             var recordSchema = (RecordSchema) rec1.Schema;
             for (int i = 0; i < recordSchema.Count; i++)
             {
@@ -366,20 +452,7 @@ namespace Avro.Test
                 }
                 else if (rec1Val is IList)
                 {
-                    var rec1List = (IList) rec1Val;
-                    if( rec1List.Count > 0 && rec1List[0] is ISpecificRecord)
-                    {
-                        var rec2List = (IList) rec2Val;
-                        Assert.AreEqual(rec1List.Count, rec2List.Count);
-                        for (int j = 0; j < rec1List.Count; j++)
-                        {
-                            AssertSpecificRecordEqual((ISpecificRecord)rec1List[j], (ISpecificRecord)rec2List[j]);
-                        }
-                    }
-                    else
-                    {
-                        Assert.AreEqual(rec1Val, rec2Val);
-                    }
+                    AssertListEqual((IList)rec1Val, (IList)rec2Val);
                 }
                 else if (rec1Val is IDictionary)
                 {
@@ -406,6 +479,53 @@ namespace Avro.Test
                 }
             }
         }
+
+        /// <summary>
+        /// Asserts that two lists are equal, delegating the work of comapring
+        /// <see cref="ISpecificRecord"/> entries to
+        /// <see cref="AssertSpecificRecordEqual(ISpecificRecord, ISpecificRecord)"/>.
+        /// </summary>
+        /// <param name="expected">Expected list value.</param>
+        /// <param name="actual">Actual list value.</param>
+        private static void AssertListEqual(IList expected, IList actual)
+        {
+            Assert.AreEqual(expected.Count, actual.Count);
+
+            for (var i = 0; i < expected.Count; ++i)
+            {
+                // Perform null checks first
+                if (expected[i] == null)
+                {
+                    Assert.Null(actual[i]);
+                    continue;
+                }
+                else
+                {
+                    Assert.NotNull(actual[i]);
+                }
+
+                if (expected[i] is ISpecificRecord expectedRecord)
+                {
+                    var actualRecord = actual[i] as ISpecificRecord;
+
+                    Assert.NotNull(actualRecord, "Expected entry that implements ISpecificRecord," +
+                        $" but was {actual[i].GetType().Name}");
+                    AssertSpecificRecordEqual(expectedRecord, actualRecord);
+                }
+                else if (expected[i] is IList expectedList)
+                {
+                    var actualList = actual[i] as IList;
+
+                    Assert.NotNull(actualList, "Expected entry that implements IList," +
+                        $" but was {actual[i].GetType().Name}");
+                    AssertListEqual(expectedList, actualList);
+                }
+                else
+                {
+                    Assert.AreEqual(expected, actual);
+                }
+            }
+        }
     }
 
     enum EnumType