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

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


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

commit 10139dc989bb8806328ef8c12d5c1d98f99c52f9
Author: Brian Lachniet <[email protected]>
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.
    
    (cherry picked commit from 62bdc83e46fbd65d34a1715c1bcc53fa5d1919fa)
---
 .../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

Reply via email to