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 0fe596a  AVRO-2499: Cache classes referenced in a union for C# reflect 
API (#652)
0fe596a is described below

commit 0fe596a123d03e3a2ada9d34a79685bc9d4be9de
Author: paddypawprints <[email protected]>
AuthorDate: Sun Sep 29 05:32:05 2019 -0700

    AVRO-2499: Cache classes referenced in a union for C# reflect API (#652)
---
 lang/csharp/src/apache/main/Reflect/ClassCache.cs  |  28 ++-
 lang/csharp/src/apache/main/Reflect/README.md      | 126 ++++++++++++-
 .../src/apache/test/Reflect/TestFromAvroProject.cs |  13 ++
 lang/csharp/src/apache/test/Reflect/TestReflect.cs |   7 +-
 lang/csharp/src/apache/test/Reflect/TestUnion.cs   | 198 +++++++++++++++++++++
 5 files changed, 361 insertions(+), 11 deletions(-)

diff --git a/lang/csharp/src/apache/main/Reflect/ClassCache.cs 
b/lang/csharp/src/apache/main/Reflect/ClassCache.cs
index 27ac8de..9237e18 100644
--- a/lang/csharp/src/apache/main/Reflect/ClassCache.cs
+++ b/lang/csharp/src/apache/main/Reflect/ClassCache.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
@@ -253,6 +253,32 @@ namespace Avro.Reflect
                 case NamedSchema ns:
                     EnumCache.AddEnumNameMapItem(ns, objType);
                     break;
+                case UnionSchema us:
+                    if (us.Schemas.Count == 2 && (us.Schemas[0].Tag == 
Schema.Type.Null || us.Schemas[1].Tag == Schema.Type.Null) && objType.IsClass)
+                    {
+                        // in this case objType will match the non null type 
in the union
+                        foreach (var o in us.Schemas)
+                        {
+                            if (o.Tag != Schema.Type.Null)
+                            {
+                                LoadClassCache(objType, o);
+                            }
+                        }
+
+                    }
+                    else
+                    {
+                        // check the schema types are registered
+                        foreach (var o in us.Schemas)
+                        {
+                            if (o.Tag == Schema.Type.Record && GetClass(o as 
RecordSchema) == null)
+                            {
+                                throw new AvroException($"Class for union 
record type {o.Fullname} is not registered. Create a ClassCache object and call 
LoadClassCache");
+                            }
+                        }
+                    }
+
+                    break;
             }
         }
     }
diff --git a/lang/csharp/src/apache/main/Reflect/README.md 
b/lang/csharp/src/apache/main/Reflect/README.md
index dfb4836..3573c6a 100644
--- a/lang/csharp/src/apache/main/Reflect/README.md
+++ b/lang/csharp/src/apache/main/Reflect/README.md
@@ -4,7 +4,7 @@ This namespace contains classes that implement Avro 
serialization and deserializ
 
 ## Serialization
 
-The approach starts with the schema and interates both the schema and the 
dotnet object together in a depth first manner per the specification. 
Serialization is the same as the Generic serializer except where the serializer 
encounters:
+The approach starts with the schema and iterates both the schema and the 
dotnet type together in a depth first manner per the specification. 
Serialization is the same as the Generic serializer except where the serializer 
encounters:
 - *A fixed type*: if the corresponding dotnet object type is a byte[] of the 
correct length then the object is serialized, otherwise an exception is thrown.
 - *A record type*: the serializer matches the schema property name to the 
dotnet object property name and then reursively serializes the schema property 
and the dotnet object property
 - *An array type*: See array serialization/deserialization.
@@ -35,13 +35,13 @@ public Func<Type, object> RecordFactory {get;set;}
 ```
 You might want to do this if your class contains interfaces and/or if you use 
an IoC container.
 
-See the section on Arrays. The ArrayHelper specifies the type of object 
created when an array is deserialized. The default is List<T>.
+See the section on Arrays. The ArrayHelper specifies the type of object 
created when an array is deserialized. The default is List\<T>.
 
-The type created for Map objects is specified by the Deserializer property 
MapType. *This must be a two (or more) parameter generic type where the first 
type paramater is string and the second is undefined* e.g. List<string,>. 
+The type created for Map objects is specified by the Deserializer property 
MapType. *This must be a two (or more) parameter generic type where the first 
type paramater is string and the second is undefined* e.g. Dictionary<string,>. 
 ```csharp
 public Type MapType { get; set; }
 ```
-By default the MapType is List<string,>
+By default the MapType is Dictionary<string,>
 ```
 
 Basic deserialization is performed as in the following example:
@@ -196,3 +196,121 @@ _Example_: ConcurrentQueue
     var reader = new 
ReflectReader<ConcurrentQueue<ConcurrentQueueRec>>(schema, schema, cache);
 
 ```
+## Unions
+
+All union constructs are supported however record types that are first defined 
in unions may need manual type registration.
+
+### Automatic Type Registration
+
+Types associated with unions of this form can be automatically registered and 
no special handling is needed.
+
+```json
+    ["null", { "type": "record", "name": "X"}]
+```
+
+_Example_: 
+
+```csharp
+    public class MyClass
+    {
+        public string A { get; set; }
+        public double C { get; set; }
+    }
+    
+    // ...
+
+    var nullableSchema = @"
+    [
+        ""null"",
+        { ""type"" : ""record"", ""name"" : ""Dervied2"", ""fields"" :
+            [
+                { ""name"" : ""A"", ""type"" : ""string""},
+                { ""name"" : ""C"", ""type"" : ""double""}
+            ]
+        },
+
+    ]
+    ";
+    var schema = Schema.Parse(nullableSchema);
+    var derived2write = new MyClass() { A = "derived2", C = 3.14 };
+
+    var writer = new ReflectWriter<MyClass>(schema);
+    var reader = new ReflectReader<MyClass>(schema, schema);
+
+    // etc.
+```
+
+### Manual Registration
+
+Where a record type is defined inside a union and the union does not 
+follow the "nullable construct" above, the CSharp type and schema need to be 
manually registered. Registration is done using the ClassCache method 
LoadClassCache.
+
+```csharp
+    cache.LoadClassCache(typeof(MyClass), recordSchema);
+```
+
+Note that the `recordSchema` used here is the schema corresponding to the 
`MyClass` type within the overall union schema. See the example below.
+
+```csharp
+        public class BaseClass
+        {
+            public string A { get; set; }
+        }
+
+        public class Derived1 : BaseClass
+        {
+            public int B { get; set; }
+        }
+
+        public class Derived2 : BaseClass
+        {
+            public double C { get; set; }
+        }
+
+        public void SerializeExample()
+        {
+            var baseClassSchema = @"
+            [
+                { ""type"" : ""record"", ""name"" : ""Dervied1"", ""fields"" :
+                    [
+                        { ""name"" : ""A"", ""type"" : ""string""},
+                        { ""name"" : ""B"", ""type"" : ""int""}
+                    ]
+                },
+                { ""type"" : ""record"", ""name"" : ""Dervied2"", ""fields"" :
+                    [
+                        { ""name"" : ""A"", ""type"" : ""string""},
+                        { ""name"" : ""C"", ""type"" : ""double""}
+                    ]
+                },
+
+            ]
+            ";
+
+            var schema = Schema.Parse(baseClassSchema);
+            var derived1write = new Derived1() { A = "derived1", B = 7 };
+            var derived2write = new Derived2() { A = "derived2", C = 3.14 };
+
+            // union types (except for [null, type]) need to be manually 
registered
+            var unionSchema = schema as UnionSchema;
+            var cache = new ClassCache();
+            cache.LoadClassCache(typeof(Derived1), unionSchema[0]);
+            cache.LoadClassCache(typeof(Derived2), unionSchema[1]);
+            var x = schema as RecordSchema;
+
+            var writer = new ReflectWriter<BaseClass>(schema, cache);
+            var reader = new ReflectReader<BaseClass>(schema, schema, cache);
+
+            using (var stream = new MemoryStream(256))
+            {
+                var encoder = new BinaryEncoder(stream);
+                writer.Write(derived1write, encoder);
+                writer.Write(derived2write, encoder);
+                stream.Seek(0, SeekOrigin.Begin);
+
+                var decoder = new BinaryDecoder(stream);
+                var derived1read = (Derived1)reader.Read(decoder);
+                var derived2read = (Derived2)reader.Read(decoder);
+            }
+        }
+```
diff --git a/lang/csharp/src/apache/test/Reflect/TestFromAvroProject.cs 
b/lang/csharp/src/apache/test/Reflect/TestFromAvroProject.cs
index dff2a69..dc17bcc 100644
--- a/lang/csharp/src/apache/test/Reflect/TestFromAvroProject.cs
+++ b/lang/csharp/src/apache/test/Reflect/TestFromAvroProject.cs
@@ -79,6 +79,8 @@ namespace Avro.Test
 
         public A myA { get; set; }
 
+        public A myNullableA { get; set; }
+
         public MyEnum myE { get; set; }
 
         public List<byte[]> myArray { get; set; }
@@ -138,6 +140,7 @@ namespace Avro.Test
                     { ""name"" : ""myNull"", ""type"" : ""null"" },
                     { ""name"" : ""myFixed"", ""type"" : ""MyFixed"" },
                     { ""name"" : ""myA"", ""type"" : ""A"" },
+                    { ""name"" : ""myNullableA"", ""type"" : [ ""null"", ""A"" 
] },
                     { ""name"" : ""myE"", ""type"" : ""MyEnum"" },
                     { ""name"" : ""myArray"", ""type"" : { ""type"" : 
""array"", ""items"" : ""bytes"" } },
                     { ""name"" : ""myArray2"", ""type"" : { ""type"" : 
""array"", ""items"" : { ""type"" : ""record"", ""name"" : ""newRec"", 
""fields"" : [ { ""name"" : ""f1"", ""type"" : ""long""} ] } } },
@@ -264,6 +267,15 @@ namespace Avro.Test
                 Assert.IsNotNull(zz.myA);
                 Assert.AreEqual(z.myA.f1, zz.myA.f1);
             }
+            if (z.myNullableA == null)
+            {
+                Assert.IsNull(zz.myNullableA);
+            }
+            else
+            {
+                Assert.IsNotNull(zz.myNullableA);
+                Assert.AreEqual(z.myNullableA.f1, zz.myNullableA.f1);
+            }
             Assert.AreEqual(z.myE, zz.myE);
             if (z.myArray == null)
             {
@@ -347,6 +359,7 @@ namespace Avro.Test
                 myNull = null,
                 myFixed = new byte[16] { 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 
0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04 },
                 myA = new A() { f1 = 3L },
+                myNullableA = new A() { f1 = 4L },
                 myE = MyEnum.B,
                 myArray = new List<byte[]>() { new byte[] { 0x01, 0x02, 0x03, 
0x04 } },
                 myArray2 = new List<newRec>() { new newRec() { f1 = 4L } },
diff --git a/lang/csharp/src/apache/test/Reflect/TestReflect.cs 
b/lang/csharp/src/apache/test/Reflect/TestReflect.cs
index 67d9b3e..bea5ef2 100644
--- a/lang/csharp/src/apache/test/Reflect/TestReflect.cs
+++ b/lang/csharp/src/apache/test/Reflect/TestReflect.cs
@@ -15,17 +15,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-using System;
+
 using System.Collections;
 using System.IO;
-using System.Linq;
 using NUnit.Framework;
 using Avro.IO;
-using System.CodeDom;
-using System.CodeDom.Compiler;
-using Avro;
 using Avro.Reflect;
-using System.Reflection;
 
 namespace Avro.Test
 {
diff --git a/lang/csharp/src/apache/test/Reflect/TestUnion.cs 
b/lang/csharp/src/apache/test/Reflect/TestUnion.cs
new file mode 100644
index 0000000..db0d4a9
--- /dev/null
+++ b/lang/csharp/src/apache/test/Reflect/TestUnion.cs
@@ -0,0 +1,198 @@
+/**
+ * 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.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Concurrent;
+using System.IO;
+using Avro.IO;
+using Avro.Reflect;
+using NUnit.Framework;
+using System.Collections;
+
+namespace Avro.Test
+{
+    [TestFixture]
+    public class TestUnion
+    {
+        public const string BaseClassSchema = @"
+        [
+            { ""type"" : ""record"", ""name"" : ""Dervied1"", ""fields"" :
+                [
+                    { ""name"" : ""A"", ""type"" : ""string""},
+                    { ""name"" : ""B"", ""type"" : ""int""}
+                ]
+            },
+            { ""type"" : ""record"", ""name"" : ""Dervied2"", ""fields"" :
+                [
+                    { ""name"" : ""A"", ""type"" : ""string""},
+                    { ""name"" : ""C"", ""type"" : ""double""}
+                ]
+            },
+
+        ]
+        ";
+
+        public class BaseClass
+        {
+            public string A { get; set; }
+        }
+
+        public class Derived1 : BaseClass
+        {
+            public int B { get; set; }
+        }
+
+        public class Derived2 : BaseClass
+        {
+            public double C { get; set; }
+        }
+
+        /// <summary>
+        /// Test with a union that represents derived classes.
+        /// </summary>
+        [TestCase]
+        public void BaseClassTest()
+        {
+            var schema = Schema.Parse(BaseClassSchema);
+            var derived1write = new Derived1() { A = "derived1", B = 7 };
+            var derived2write = new Derived2() { A = "derived2", C = 3.14 };
+
+            // union types (except for [null, type]) need to be manually 
registered
+            var unionSchema = schema as UnionSchema;
+            var cache = new ClassCache();
+            cache.LoadClassCache(typeof(Derived1), unionSchema[0]);
+            cache.LoadClassCache(typeof(Derived2), unionSchema[1]);
+            var x = schema as RecordSchema;
+
+            var writer = new ReflectWriter<BaseClass>(schema, cache);
+            var reader = new ReflectReader<BaseClass>(schema, schema, cache);
+
+            using (var stream = new MemoryStream(256))
+            {
+                var encoder = new BinaryEncoder(stream);
+                writer.Write(derived1write, encoder);
+                writer.Write(derived2write, encoder);
+                stream.Seek(0, SeekOrigin.Begin);
+
+                var decoder = new BinaryDecoder(stream);
+                var derived1read = (Derived1)reader.Read(decoder);
+                var derived2read = (Derived2)reader.Read(decoder);
+                Assert.AreEqual(derived1read.A, derived1write.A);
+                Assert.AreEqual(derived1read.B, derived1write.B);
+                Assert.AreEqual(derived2read.A, derived2write.A);
+                Assert.AreEqual(derived2read.C, derived2write.C);
+            }
+        }
+
+        /// <summary>
+        /// If you fail to manually register types within a union that has 
more than one non-null
+        /// schema, creating a <see cref="ReflectWriter{T}"/> throws an 
exception.
+        /// </summary>
+        [TestCase]
+        public void ThrowsIfClassesNotLoadedTest()
+        {
+            var schema = Schema.Parse(BaseClassSchema);
+            var cache = new ClassCache();
+            Assert.Throws<AvroException>(() => new 
ReflectWriter<BaseClass>(schema, cache));
+        }
+
+        [TestCase]
+        public void NullableTest()
+        {
+            var nullableSchema = @"
+            [
+                ""null"",
+                { ""type"" : ""record"", ""name"" : ""Dervied2"", ""fields"" :
+                    [
+                        { ""name"" : ""A"", ""type"" : ""string""},
+                        { ""name"" : ""C"", ""type"" : ""double""}
+                    ]
+                },
+
+            ]
+            ";
+            var schema = Schema.Parse(nullableSchema);
+            var derived2write = new Derived2() { A = "derived2", C = 3.14 };
+
+            var writer = new ReflectWriter<Derived2>(schema);
+            var reader = new ReflectReader<Derived2>(schema, schema);
+
+            using (var stream = new MemoryStream(256))
+            {
+                var encoder = new BinaryEncoder(stream);
+                writer.Write(derived2write, encoder);
+                writer.Write(null, encoder);
+                stream.Seek(0, SeekOrigin.Begin);
+
+                var decoder = new BinaryDecoder(stream);
+                var derived2read = reader.Read(decoder);
+                var derived2null = reader.Read(decoder);
+                Assert.AreEqual(derived2read.A, derived2write.A);
+                Assert.AreEqual(derived2read.C, derived2write.C);
+                Assert.IsNull(derived2null);
+            }
+        }
+
+        [TestCase]
+        public void HeterogeneousTest()
+        {
+            var heterogeneousSchema = @"
+            [
+                ""string"",
+                ""null"",
+                { ""type"" : ""record"", ""name"" : ""Dervied2"", ""fields"" :
+                    [
+                        { ""name"" : ""A"", ""type"" : ""string""},
+                        { ""name"" : ""C"", ""type"" : ""double""}
+                    ]
+                },
+
+            ]
+            ";
+            var schema = Schema.Parse(heterogeneousSchema);
+            var derived2write = new Derived2() { A = "derived2", C = 3.14 };
+
+            // union types (except for [null, type]) need to be manually 
registered
+            var unionSchema = schema as UnionSchema;
+            var cache = new ClassCache();
+            cache.LoadClassCache(typeof(Derived2), unionSchema[2]);
+
+            var writer = new ReflectWriter<object>(schema, cache);
+            var reader = new ReflectReader<object>(schema, schema, cache);
+
+            using (var stream = new MemoryStream(256))
+            {
+                var encoder = new BinaryEncoder(stream);
+                writer.Write(derived2write, encoder);
+                writer.Write("string value", encoder);
+                writer.Write(null, encoder);
+                stream.Seek(0, SeekOrigin.Begin);
+
+                var decoder = new BinaryDecoder(stream);
+                var derived2read = (Derived2)reader.Read(decoder);
+                var stringRead = (string)reader.Read(decoder);
+                var nullRead = reader.Read(decoder);
+                Assert.AreEqual(derived2read.A, derived2write.A);
+                Assert.AreEqual(derived2read.C, derived2write.C);
+                Assert.AreEqual(stringRead, "string value");
+                Assert.IsNull(nullRead);
+            }
+        }
+    }
+}

Reply via email to