AVRO-3434: support logical schemas in reflect reader and writer (#1718)

* avro-3434 support logical schemas in reflect reader and writer

* avro-3434 add apache licence to files

* avro-3434 update unit tests
diff --git a/lang/csharp/src/apache/main/Reflect/DotnetClass.cs b/lang/csharp/src/apache/main/Reflect/DotnetClass.cs
index 0d6d435..78eaca5 100644
--- a/lang/csharp/src/apache/main/Reflect/DotnetClass.cs
+++ b/lang/csharp/src/apache/main/Reflect/DotnetClass.cs
@@ -52,14 +52,14 @@
                     if (avroAttr != null)
                     {
                         hasAttribute = true;
-                        _propertyMap.TryAdd(f.Name, new DotnetProperty(prop, f.Schema.Tag, avroAttr.Converter, cache));
+                        _propertyMap.TryAdd(f.Name, new DotnetProperty(prop, f.Schema, avroAttr.Converter, cache));
                         break;
                     }
                 }
 
                 if (!hasAttribute)
                 {
-                    _propertyMap.TryAdd(f.Name, new DotnetProperty(prop, f.Schema.Tag, cache));
+                    _propertyMap.TryAdd(f.Name, new DotnetProperty(prop, f.Schema, cache));
                 }
             }
         }
diff --git a/lang/csharp/src/apache/main/Reflect/DotnetProperty.cs b/lang/csharp/src/apache/main/Reflect/DotnetProperty.cs
index 4ddcdc6..42ae766 100644
--- a/lang/csharp/src/apache/main/Reflect/DotnetProperty.cs
+++ b/lang/csharp/src/apache/main/Reflect/DotnetProperty.cs
@@ -28,9 +28,10 @@
 
         public IAvroFieldConverter Converter { get; set; }
 
-        private bool IsPropertyCompatible(Avro.Schema.Type schemaTag)
+        private bool IsPropertyCompatible(Avro.Schema schema)
         {
             Type propType;
+            var schemaTag = schema.Tag;
 
             if (Converter == null)
             {
@@ -74,21 +75,25 @@
                     return propType == typeof(byte[]);
                 case Avro.Schema.Type.Error:
                     return propType.IsClass;
+                case Avro.Schema.Type.Logical:
+                    var logicalSchema = (LogicalSchema)schema;
+                    var type = logicalSchema.LogicalType.GetCSharpType(false);
+                    return type == propType;
             }
 
             return false;
         }
 
-        public DotnetProperty(PropertyInfo property, Avro.Schema.Type schemaTag,  IAvroFieldConverter converter, ClassCache cache)
+        public DotnetProperty(PropertyInfo property, Avro.Schema schema, IAvroFieldConverter converter, ClassCache cache)
         {
             _property = property;
             Converter = converter;
 
-            if (!IsPropertyCompatible(schemaTag))
+            if (!IsPropertyCompatible(schema))
             {
                 if (Converter == null)
                 {
-                    var c = cache.GetDefaultConverter(schemaTag, _property.PropertyType);
+                    var c = cache.GetDefaultConverter(schema.Tag, _property.PropertyType);
                     if (c != null)
                     {
                         Converter = c;
@@ -96,12 +101,12 @@
                     }
                 }
 
-                throw new AvroException($"Property {property.Name} in object {property.DeclaringType} isn't compatible with Avro schema type {schemaTag}");
+                throw new AvroException($"Property {property.Name} in object {property.DeclaringType} isn't compatible with Avro schema type {schema.Tag}");
             }
         }
 
-        public DotnetProperty(PropertyInfo property, Avro.Schema.Type schemaTag, ClassCache cache)
-            : this(property, schemaTag, null, cache)
+        public DotnetProperty(PropertyInfo property, Avro.Schema schema, ClassCache cache)
+            : this(property, schema, null, cache)
         {
         }
 
diff --git a/lang/csharp/src/apache/main/Reflect/ReflectDefaultWriter.cs b/lang/csharp/src/apache/main/Reflect/ReflectDefaultWriter.cs
index a6397c6..e5ef4a2 100644
--- a/lang/csharp/src/apache/main/Reflect/ReflectDefaultWriter.cs
+++ b/lang/csharp/src/apache/main/Reflect/ReflectDefaultWriter.cs
@@ -199,6 +199,8 @@
                     return false;   // Union directly within another union not allowed!
                 case Schema.Type.Fixed:
                     return obj is byte[];
+                case Schema.Type.Logical:
+                    return ((LogicalSchema)sc).LogicalType.IsInstanceOfLogicalType(obj);
                 default:
                     throw new AvroException("Unknown schema type: " + sc.Tag);
             }
diff --git a/lang/csharp/src/apache/test/Reflect/TestLogicalSchema.cs b/lang/csharp/src/apache/test/Reflect/TestLogicalSchema.cs
new file mode 100644
index 0000000..c18da40
--- /dev/null
+++ b/lang/csharp/src/apache/test/Reflect/TestLogicalSchema.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.IO;
+using Avro.IO;
+using Avro.Reflect;
+using NUnit.Framework;
+
+namespace Avro.test.Reflect
+{
+    public class TestLogicalSchema
+    {
+        [TestCase]
+        public void WriteAndReadObjectsWithLogicalSchemaFields_WithNullValues()
+        {
+            //Arrange
+            var obj = new TestObject
+            {
+                AvroDecimalNullableProperty = null,
+                AvroDecimalProperty = 13.42m,
+                GuidNullableProperty = null,
+                GuidProperty = Guid.NewGuid(),
+                DateNullableProperty = null,
+                DateProperty = new DateTime(2022, 05, 26, 14, 57, 24, 123),
+                DateTimeMicrosecondNullableProperty = null,
+                DateTimeMicrosecondProperty = DateTime.UtcNow,
+                DateTimeMillisecondNullableProperty = null,
+                DateTimeMillisecondProperty = DateTime.UtcNow,
+                TimeSpanMicrosecondNullableProperty = null,
+                TimeSpanMicrosecondProperty = new TimeSpan(23, 59, 59),
+                TimeSpanMillisecondNullableProperty = null,
+                TimeSpanMillisecondProperty = new TimeSpan(23, 59, 59),
+            };
+
+            var schema = Schema.Parse(SchemaJson);
+            var writer = new ReflectWriter<TestObject>(schema);
+            var reader = new ReflectReader<TestObject>(schema, schema);
+            var writeStream = new MemoryStream();
+            var writeBinaryEncoder = new BinaryEncoder(writeStream);
+
+            //Act
+            writer.Write(obj, writeBinaryEncoder);
+            var data = writeStream.ToArray();
+
+            var readStream = new MemoryStream(data);
+            var result = reader.Read(null, new BinaryDecoder(readStream));
+
+            //Assert
+            Assert.NotNull(result);
+
+            Assert.IsNull(result.AvroDecimalNullableProperty);
+            Assert.AreEqual(obj.AvroDecimalProperty, result.AvroDecimalProperty);
+
+            Assert.IsNull(result.GuidNullableProperty);
+            Assert.AreEqual(obj.GuidProperty, result.GuidProperty);
+
+            Assert.IsNull(obj.DateNullableProperty);
+            Assert.AreEqual(obj.DateProperty.Date, result.DateProperty);
+
+            Assert.IsNull(result.DateTimeMicrosecondNullableProperty);
+            Assert.AreEqual((obj.DateTimeMicrosecondProperty.Ticks / 10 ) * 10, result.DateTimeMicrosecondProperty.Ticks);
+
+            Assert.IsNull(result.DateTimeMillisecondNullableProperty);
+            Assert.AreEqual((obj.DateTimeMillisecondProperty.Ticks / 10000) * 10000, result.DateTimeMillisecondProperty.Ticks);
+
+            Assert.IsNull(result.TimeSpanMicrosecondNullableProperty);
+            Assert.AreEqual(obj.TimeSpanMicrosecondProperty, result.TimeSpanMicrosecondProperty);
+
+            Assert.IsNull(result.TimeSpanMillisecondNullableProperty);
+            Assert.AreEqual(obj.TimeSpanMillisecondProperty, result.TimeSpanMillisecondProperty);
+        }
+
+        [TestCase]
+        public void WriteAndReadObjectsWithLogicalSchemaFields_WithoutNullValues()
+        {
+            //Arrange
+            var obj = new TestObject
+            {
+                AvroDecimalNullableProperty = 136.42m,
+                AvroDecimalProperty = 13.42m,
+                GuidNullableProperty = Guid.NewGuid(),
+                GuidProperty = Guid.NewGuid(),
+                DateNullableProperty = new DateTime(2022, 05, 26, 14, 57, 24, 123),
+                DateProperty = new DateTime(2022, 05, 26, 14, 57, 24, 123),
+                DateTimeMicrosecondNullableProperty = DateTime.UtcNow,
+                DateTimeMicrosecondProperty = DateTime.UtcNow,
+                DateTimeMillisecondNullableProperty = DateTime.UtcNow,
+                DateTimeMillisecondProperty = DateTime.UtcNow,
+                TimeSpanMicrosecondNullableProperty = new TimeSpan(23, 59, 59),
+                TimeSpanMicrosecondProperty = new TimeSpan(23, 59, 59),
+                TimeSpanMillisecondNullableProperty = new TimeSpan(23, 59, 59),
+                TimeSpanMillisecondProperty = new TimeSpan(23, 59, 59),
+            };
+
+            var schema = Schema.Parse(SchemaJson);
+            var writer = new ReflectWriter<TestObject>(schema);
+            var reader = new ReflectReader<TestObject>(schema, schema);
+            var writeStream = new MemoryStream();
+            var writeBinaryEncoder = new BinaryEncoder(writeStream);
+
+            //Act
+            writer.Write(obj, writeBinaryEncoder);
+            var data = writeStream.ToArray();
+
+            var readStream = new MemoryStream(data);
+            var result = reader.Read(null, new BinaryDecoder(readStream));
+
+            //Assert
+            Assert.NotNull(result);
+
+            Assert.NotNull(result.AvroDecimalNullableProperty);
+            Assert.AreEqual(obj.AvroDecimalNullableProperty, result.AvroDecimalNullableProperty);
+            Assert.AreEqual(obj.AvroDecimalProperty, result.AvroDecimalProperty);
+
+            Assert.NotNull(result.GuidNullableProperty);
+            Assert.AreEqual(obj.GuidNullableProperty, result.GuidNullableProperty);
+            Assert.AreEqual(obj.GuidProperty, result.GuidProperty);
+
+            Assert.NotNull(result.DateProperty);
+            Assert.AreEqual(obj.DateNullableProperty?.Date, result.DateProperty);
+            Assert.AreEqual(obj.DateProperty.Date, result.DateProperty);
+
+            Assert.NotNull(result.DateTimeMicrosecondNullableProperty);
+            Assert.AreEqual((obj.DateTimeMicrosecondNullableProperty?.Ticks / 10) * 10, result.DateTimeMicrosecondNullableProperty?.Ticks);
+            Assert.AreEqual((obj.DateTimeMicrosecondProperty.Ticks / 10) * 10, result.DateTimeMicrosecondProperty.Ticks);
+
+            Assert.NotNull(result.DateTimeMillisecondNullableProperty);
+            Assert.AreEqual((obj.DateTimeMillisecondNullableProperty?.Ticks / 10000) * 10000, result.DateTimeMillisecondNullableProperty?.Ticks);
+            Assert.AreEqual((obj.DateTimeMillisecondProperty.Ticks / 10000) * 10000, result.DateTimeMillisecondProperty.Ticks);
+
+            Assert.NotNull(result.TimeSpanMicrosecondNullableProperty);
+            Assert.AreEqual(obj.TimeSpanMicrosecondNullableProperty, result.TimeSpanMicrosecondNullableProperty);
+            Assert.AreEqual(obj.TimeSpanMicrosecondProperty, result.TimeSpanMicrosecondProperty);
+
+            Assert.NotNull(result.TimeSpanMillisecondNullableProperty);
+            Assert.AreEqual(obj.TimeSpanMillisecondNullableProperty, result.TimeSpanMillisecondNullableProperty);
+            Assert.AreEqual(obj.TimeSpanMillisecondProperty, result.TimeSpanMillisecondProperty);
+        }
+
+        private const string SchemaJson = @"
+{
+  ""type"" : ""record"",
+  ""namespace"" : ""Avro.test.Reflect.Converters"",
+  ""name"" : ""TestObject"",
+  ""fields"" : [
+    { ""name"" : ""AvroDecimalNullableProperty"" , ""type"" : [""null"", { ""type"": ""bytes"", ""logicalType"": ""decimal"", ""precision"": 6, ""scale"": 2 }] },
+    { ""name"" : ""AvroDecimalProperty"" , ""type"" : { ""type"": ""bytes"", ""logicalType"": ""decimal"", ""precision"": 6, ""scale"": 2 } },
+    { ""name"" : ""GuidNullableProperty"" , ""type"" : [""null"", { ""type"": ""string"", ""logicalType"": ""uuid""}] },
+    { ""name"" : ""GuidProperty"" , ""type"" : { ""type"": ""string"", ""logicalType"": ""uuid""} },
+    { ""name"" : ""DateNullableProperty"" , ""type"" : [""null"", { ""type"": ""int"", ""logicalType"": ""date""}] },
+    { ""name"" : ""DateProperty"" , ""type"" : { ""type"": ""int"", ""logicalType"": ""date""} },
+    { ""name"" : ""DateTimeMicrosecondNullableProperty"" , ""type"" : [""null"", { ""type"": ""long"", ""logicalType"": ""timestamp-micros""}] },
+    { ""name"" : ""DateTimeMicrosecondProperty"" , ""type"" : { ""type"": ""long"", ""logicalType"": ""timestamp-micros""} },
+    { ""name"" : ""DateTimeMillisecondNullableProperty"" , ""type"" : [""null"", { ""type"": ""long"", ""logicalType"": ""timestamp-millis""}] },
+    { ""name"" : ""DateTimeMillisecondProperty"" , ""type"" : { ""type"": ""long"", ""logicalType"": ""timestamp-millis""} },
+    { ""name"" : ""TimeSpanMicrosecondNullableProperty"" , ""type"" : [""null"", { ""type"": ""long"", ""logicalType"": ""time-micros""}] },
+    { ""name"" : ""TimeSpanMicrosecondProperty"" , ""type"" : { ""type"": ""long"", ""logicalType"": ""time-micros""} },
+    { ""name"" : ""TimeSpanMillisecondNullableProperty"" , ""type"" : [""null"", { ""type"": ""int"", ""logicalType"": ""time-millis""}] },
+    { ""name"" : ""TimeSpanMillisecondProperty"" , ""type"" : { ""type"": ""int"", ""logicalType"": ""time-millis""} }
+  ]
+}
+";
+
+        public class TestObject
+        {
+            public AvroDecimal? AvroDecimalNullableProperty { get; set; }
+            public AvroDecimal AvroDecimalProperty { get; set; }
+            public Guid? GuidNullableProperty { get; set; }
+            public Guid GuidProperty { get; set; }
+            public DateTime? DateNullableProperty { get; set; }
+            public DateTime DateProperty { get; set; }
+            public DateTime? DateTimeMicrosecondNullableProperty { get; set; }
+            public DateTime DateTimeMicrosecondProperty { get; set; }
+            public DateTime? DateTimeMillisecondNullableProperty { get; set; }
+            public DateTime DateTimeMillisecondProperty { get; set; }
+            public TimeSpan? TimeSpanMicrosecondNullableProperty { get; set; }
+            public TimeSpan TimeSpanMicrosecondProperty { get; set; }
+            public TimeSpan? TimeSpanMillisecondNullableProperty { get; set; }
+            public TimeSpan TimeSpanMillisecondProperty { get; set; }
+        }
+    }
+}