You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@avro.apache.org by fo...@apache.org on 2020/02/05 09:53:18 UTC

[avro] branch branch-1.9 updated: AVRO-2359: Support Logical Types in C# (#492)

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

fokko 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 d43c0a4  AVRO-2359: Support Logical Types in C# (#492)
d43c0a4 is described below

commit d43c0a4d7ca2f8c22e9292c003ca91261aa577d2
Author: Tim Roberts <ti...@timjroberts.com>
AuthorDate: Wed Feb 5 09:51:28 2020 +0000

    AVRO-2359: Support Logical Types in C# (#492)
    
    * AVRO-2359: Support Logical Types in CSharp
    
    * fix more warnings
    
    * remove timezone change in TestTimestamp tests
    
    * update logical Date and Time tests to be local time zone aware
    
    * ensure TimeZone is set relative to a source TimeZone
    
    * reparse Dates assuming local if they're not UTC
    
    * try setting specific TimeZone offset
    
    * actions for code review comments
    
    * seperate the Timestamp* tests
    
    * add missing license to AvroDecimalTests
    
    * modify LogicalType to return .NET type rather than string
    
    * add Register method to LogicalTypeFactory
    
    * Fixup code-review comments
    
    * Make Sign internal and add xmldocs
    
    Co-authored-by: Fokko Driesprong <fo...@driesprong.frl>
---
 .gitignore                                         |   3 +-
 lang/csharp/.gitignore                             |   3 +-
 lang/csharp/src/apache/main/AvroDecimal.cs         | 786 +++++++++++++++++++++
 lang/csharp/src/apache/main/CodeGen/CodeGen.cs     |   8 +
 .../src/apache/main/Generic/GenericDatumWriter.cs  |   2 +
 .../src/apache/main/Generic/GenericReader.cs       |  21 +
 .../src/apache/main/Generic/GenericWriter.cs       |  17 +
 .../apache/main/Generic/PreresolvingDatumReader.cs |  11 +
 .../apache/main/Generic/PreresolvingDatumWriter.cs |  13 +
 .../csharp/src/apache/main/Schema/LogicalSchema.cs | 118 ++++
 lang/csharp/src/apache/main/Schema/Property.cs     |   2 +-
 lang/csharp/src/apache/main/Schema/Schema.cs       |  11 +-
 .../apache/main/Specific/SpecificDatumWriter.cs    |   2 +
 .../src/apache/main/Specific/SpecificWriter.cs     |   2 +
 lang/csharp/src/apache/main/Util/Date.cs           |  61 ++
 lang/csharp/src/apache/main/Util/Decimal.cs        | 131 ++++
 lang/csharp/src/apache/main/Util/LogicalType.cs    |  77 ++
 .../src/apache/main/Util/LogicalTypeFactory.cs     |  85 +++
 .../src/apache/main/Util/LogicalUnixEpochType.cs   |  54 ++
 .../csharp/src/apache/main/Util/TimeMicrosecond.cs |  66 ++
 .../csharp/src/apache/main/Util/TimeMillisecond.cs |  66 ++
 .../src/apache/main/Util/TimestampMicrosecond.cs   |  60 ++
 .../src/apache/main/Util/TimestampMillisecond.cs   |  59 ++
 .../csharp/src/apache/test/Generic/GenericTests.cs |  44 ++
 lang/csharp/src/apache/test/Schema/SchemaTests.cs  |  21 +
 .../src/apache/test/Util/LogicalTypeTests.cs       | 182 +++++
 26 files changed, 1901 insertions(+), 4 deletions(-)

diff --git a/.gitignore b/.gitignore
index 4bb7412..916e51a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,4 +18,5 @@ test-output
 /lang/java/compiler/nbproject/
 **/.vscode/**/*
 .factorypath
-
+.vs/
+.DS_Store
diff --git a/lang/csharp/.gitignore b/lang/csharp/.gitignore
index eed99c6..8030457 100644
--- a/lang/csharp/.gitignore
+++ b/lang/csharp/.gitignore
@@ -17,7 +17,6 @@
 /*.user
 /*.suo
 /_ReSharper.Avro
-/.vs
 obj/
 
 ## Ignore Visual Studio temporary files, build results, and
@@ -53,3 +52,5 @@ obj/
 
 #Test results
 TestResult.xml
+
+.vs/
diff --git a/lang/csharp/src/apache/main/AvroDecimal.cs b/lang/csharp/src/apache/main/AvroDecimal.cs
new file mode 100644
index 0000000..0ab0f05
--- /dev/null
+++ b/lang/csharp/src/apache/main/AvroDecimal.cs
@@ -0,0 +1,786 @@
+/*
+ * 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.Globalization;
+using System.Numerics;
+
+namespace Avro
+{
+    /// <summary>
+    /// Represents a big decimal.
+    /// </summary>
+    #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+    #pragma warning disable CA2225 // Operator overloads have named alternates
+    public struct AvroDecimal : IConvertible, IFormattable, IComparable, IComparable<AvroDecimal>, IEquatable<AvroDecimal>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given double.
+        /// </summary>
+        /// <param name="value">The double value.</param>
+        public AvroDecimal(double value)
+            : this((decimal)value)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given float.
+        /// </summary>
+        /// <param name="value">The float value.</param>
+        public AvroDecimal(float value)
+            : this((decimal)value)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given decimal.
+        /// </summary>
+        /// <param name="value">The decimal value.</param>
+        public AvroDecimal(decimal value)
+        {
+            var bytes = GetBytesFromDecimal(value);
+
+            var unscaledValueBytes = new byte[12];
+            Array.Copy(bytes, unscaledValueBytes, unscaledValueBytes.Length);
+
+            var unscaledValue = new BigInteger(unscaledValueBytes);
+            var scale = bytes[14];
+
+            if (bytes[15] == 128)
+                unscaledValue *= BigInteger.MinusOne;
+
+            UnscaledValue = unscaledValue;
+            Scale = scale;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given int.
+        /// </summary>
+        /// <param name="value">The int value.</param>
+        public AvroDecimal(int value)
+            : this(new BigInteger(value), 0) { }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given long.
+        /// </summary>
+        /// <param name="value">The long value.</param>
+        public AvroDecimal(long value)
+            : this(new BigInteger(value), 0) { }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given unsigned int.
+        /// </summary>
+        /// <param name="value">The unsigned int value.</param>
+        public AvroDecimal(uint value)
+            : this(new BigInteger(value), 0) { }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given unsigned long.
+        /// </summary>
+        /// <param name="value">The unsigned long value.</param>
+        public AvroDecimal(ulong value)
+            : this(new BigInteger(value), 0) { }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given <see cref="BigInteger"/>
+        /// and a scale.
+        /// </summary>
+        /// <param name="unscaledValue">The double value.</param>
+        /// <param name="scale">The scale.</param>
+        public AvroDecimal(BigInteger unscaledValue, int scale)
+        {
+            UnscaledValue = unscaledValue;
+            Scale = scale;
+        }
+
+        /// <summary>
+        /// Gets the unscaled integer value represented by the current <see cref="AvroDecimal"/>.
+        /// </summary>
+        public BigInteger UnscaledValue { get; }
+
+        /// <summary>
+        /// Gets the scale of the current <see cref="AvroDecimal"/>.
+        /// </summary>
+        public int Scale { get; }
+
+        /// <summary>
+        /// Gets the sign of the current <see cref="AvroDecimal"/>.
+        /// </summary>
+        internal int Sign
+        {
+            get { return UnscaledValue.Sign; }
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a string.
+        /// </summary>
+        /// <returns>A string representation of the numeric value.</returns>
+        public override string ToString()
+        {
+            var number = UnscaledValue.ToString(CultureInfo.CurrentCulture);
+
+            if (Scale > 0)
+                return number.Insert(number.Length - Scale, CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);
+
+            return number;
+        }
+
+        public static bool operator ==(AvroDecimal left, AvroDecimal right)
+        {
+            return left.Equals(right);
+        }
+
+        public static bool operator !=(AvroDecimal left, AvroDecimal right)
+        {
+            return !left.Equals(right);
+        }
+
+        public static bool operator >(AvroDecimal left, AvroDecimal right)
+        {
+            return left.CompareTo(right) > 0;
+        }
+
+        public static bool operator >=(AvroDecimal left, AvroDecimal right)
+        {
+            return left.CompareTo(right) >= 0;
+        }
+
+        public static bool operator <(AvroDecimal left, AvroDecimal right)
+        {
+            return left.CompareTo(right) < 0;
+        }
+
+        public static bool operator <=(AvroDecimal left, AvroDecimal right)
+        {
+            return left.CompareTo(right) <= 0;
+        }
+
+        public static bool operator ==(AvroDecimal left, decimal right)
+        {
+            return left.Equals(right);
+        }
+
+        public static bool operator !=(AvroDecimal left, decimal right)
+        {
+            return !left.Equals(right);
+        }
+
+        public static bool operator >(AvroDecimal left, decimal right)
+        {
+            return left.CompareTo(right) > 0;
+        }
+
+        public static bool operator >=(AvroDecimal left, decimal right)
+        {
+            return left.CompareTo(right) >= 0;
+        }
+
+        public static bool operator <(AvroDecimal left, decimal right)
+        {
+            return left.CompareTo(right) < 0;
+        }
+
+        public static bool operator <=(AvroDecimal left, decimal right)
+        {
+            return left.CompareTo(right) <= 0;
+        }
+
+        public static bool operator ==(decimal left, AvroDecimal right)
+        {
+            return left.Equals(right);
+        }
+
+        public static bool operator !=(decimal left, AvroDecimal right)
+        {
+            return !left.Equals(right);
+        }
+
+        public static bool operator >(decimal left, AvroDecimal right)
+        {
+            return left.CompareTo(right) > 0;
+        }
+
+        public static bool operator >=(decimal left, AvroDecimal right)
+        {
+            return left.CompareTo(right) >= 0;
+        }
+
+        public static bool operator <(decimal left, AvroDecimal right)
+        {
+            return left.CompareTo(right) < 0;
+        }
+
+        public static bool operator <=(decimal left, AvroDecimal right)
+        {
+            return left.CompareTo(right) <= 0;
+        }
+
+        public static explicit operator byte(AvroDecimal value)
+        {
+            return ToByte(value);
+        }
+
+        /// <summary>
+        /// Creates a byte from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>A byte.</returns>
+        public static byte ToByte(AvroDecimal value)
+        {
+            return value.ToType<byte>();
+        }
+
+        public static explicit operator sbyte(AvroDecimal value)
+        {
+            return ToSByte(value);
+        }
+
+        /// <summary>
+        /// Creates a signed byte from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>A signed byte.</returns>
+        public static sbyte ToSByte(AvroDecimal value)
+        {
+            return value.ToType<sbyte>();
+        }
+
+        public static explicit operator short(AvroDecimal value)
+        {
+            return ToInt16(value);
+        }
+
+        /// <summary>
+        /// Creates a short from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>A short.</returns>
+        public static short ToInt16(AvroDecimal value)
+        {
+            return value.ToType<short>();
+        }
+
+        public static explicit operator int(AvroDecimal value)
+        {
+            return ToInt32(value);
+        }
+
+        /// <summary>
+        /// Creates an int from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>An int.</returns>
+        public static int ToInt32(AvroDecimal value)
+        {
+            return value.ToType<int>();
+        }
+
+        public static explicit operator long(AvroDecimal value)
+        {
+            return ToInt64(value);
+        }
+
+        /// <summary>
+        /// Creates a long from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>A long.</returns>
+        public static long ToInt64(AvroDecimal value)
+        {
+            return value.ToType<long>();
+        }
+
+        public static explicit operator ushort(AvroDecimal value)
+        {
+            return ToUInt16(value);
+        }
+
+        /// <summary>
+        /// Creates an unsigned short from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>An unsigned short.</returns>
+        public static ushort ToUInt16(AvroDecimal value)
+        {
+            return value.ToType<ushort>();
+        }
+
+        public static explicit operator uint(AvroDecimal value)
+        {
+            return ToUInt32(value);
+        }
+
+        /// <summary>
+        /// Creates an unsigned int from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>An unsigned int.</returns>
+        public static uint ToUInt32(AvroDecimal value)
+        {
+            return value.ToType<uint>();
+        }
+
+        public static explicit operator ulong(AvroDecimal value)
+        {
+            return ToUInt64(value);
+        }
+
+        /// <summary>
+        /// Creates an unsigned long from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>An unsigned long.</returns>
+        public static ulong ToUInt64(AvroDecimal value)
+        {
+            return value.ToType<ulong>();
+        }
+
+        public static explicit operator float(AvroDecimal value)
+        {
+            return ToSingle(value);
+        }
+
+        /// <summary>
+        /// Creates a double from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>A double.</returns>
+        public static float ToSingle(AvroDecimal value)
+        {
+            return value.ToType<float>();
+        }
+
+        public static explicit operator double(AvroDecimal value)
+        {
+            return ToDouble(value);
+        }
+
+        /// <summary>
+        /// Creates a double from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>A double.</returns>
+        public static double ToDouble(AvroDecimal value)
+        {
+            return value.ToType<double>();
+        }
+
+        public static explicit operator decimal(AvroDecimal value)
+        {
+            return ToDecimal(value);
+        }
+
+        /// <summary>
+        /// Creates a decimal from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>A decimal.</returns>
+        public static decimal ToDecimal(AvroDecimal value)
+        {
+            return value.ToType<decimal>();
+        }
+
+        public static explicit operator BigInteger(AvroDecimal value)
+        {
+            return ToBigInteger(value);
+        }
+
+        /// <summary>
+        /// Creates a <see cref="BigInteger"/> from a given <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+        /// <returns>A <see cref="BigInteger"/>.</returns>
+        public static BigInteger ToBigInteger(AvroDecimal value)
+        {
+            var scaleDivisor = BigInteger.Pow(new BigInteger(10), value.Scale);
+            var scaledValue = BigInteger.Divide(value.UnscaledValue, scaleDivisor);
+            return scaledValue;
+        }
+
+        public static implicit operator AvroDecimal(byte value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(sbyte value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(short value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(int value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(long value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(ushort value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(uint value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(ulong value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(float value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(double value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(decimal value)
+        {
+            return new AvroDecimal(value);
+        }
+
+        public static implicit operator AvroDecimal(BigInteger value)
+        {
+            return new AvroDecimal(value, 0);
+        } 
+
+        /// <summary>
+        /// Converts the numeric value of the current <see cref="AvroDecimal"/> to a given type.
+        /// </summary>
+        /// <typeparam name="T">The type to which the value of the current <see cref="AvroDecimal"/> should be converted.</typeparam>
+        /// <returns>A value of type <typeparamref name="T"/> converted from the current <see cref="AvroDecimal"/>.</returns>
+        public T ToType<T>()
+            where T : struct
+        {
+            return (T)((IConvertible)this).ToType(typeof(T), null);
+        }
+
+        /// <summary>
+        /// Converts the numeric value of the current <see cref="AvroDecimal"/> to a given type.
+        /// </summary>
+        /// <param name="conversionType">The type to which the value of the current <see cref="AvroDecimal"/> should be converted.</param>
+        /// <param name="provider">An System.IFormatProvider interface implementation that supplies culture-specific formatting information.</param>
+        /// <returns></returns>
+        object IConvertible.ToType(Type conversionType, IFormatProvider provider)
+        {
+            var scaleDivisor = BigInteger.Pow(new BigInteger(10), Scale);
+            var remainder = BigInteger.Remainder(UnscaledValue, scaleDivisor);
+            var scaledValue = BigInteger.Divide(UnscaledValue, scaleDivisor);
+
+            if (scaledValue > new BigInteger(Decimal.MaxValue))
+                throw new ArgumentOutOfRangeException("value", "The value " + UnscaledValue + " cannot fit into " + conversionType.Name + ".");
+
+            var leftOfDecimal = (decimal)scaledValue;
+            var rightOfDecimal = ((decimal)remainder) / ((decimal)scaleDivisor);
+
+            var value = leftOfDecimal + rightOfDecimal;
+            return Convert.ChangeType(value, conversionType, provider);
+        }
+
+        /// <summary>
+        /// Returns a value that indicates whether the current <see cref="AvroDecimal"/> and a specified object
+        /// have the same value.
+        /// </summary>
+        /// <param name="obj">The object to compare.</param>
+        /// <returns>true if the obj argument is an <see cref="AvroDecimal"/> object, and its value
+        /// is equal to the value of the current <see cref="AvroDecimal"/> instance; otherwise false.
+        /// </returns>
+        public override bool Equals(object obj)
+        {
+            return (obj is AvroDecimal) && Equals((AvroDecimal)obj);
+        }
+
+        /// <summary>
+        /// Returns the hash code for the current <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <returns>The hash code.</returns>
+        public override int GetHashCode()
+        {
+            return UnscaledValue.GetHashCode() ^ Scale.GetHashCode();
+        }
+
+        /// <summary>
+        /// Returns the <see cref="TypeCode"/> for the current <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <returns><see cref="TypeCode.Object"/>.</returns>
+        TypeCode IConvertible.GetTypeCode()
+        {
+            return TypeCode.Object;
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a boolean.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>true or false, which reflects the value of the current <see cref="AvroDecimal"/>.</returns>
+        bool IConvertible.ToBoolean(IFormatProvider provider)
+        {
+            return Convert.ToBoolean(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a byte.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>A byte.</returns>
+        byte IConvertible.ToByte(IFormatProvider provider)
+        {
+            return Convert.ToByte(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a char.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>This method always throws an <see cref="InvalidCastException"/>.</returns>
+        char IConvertible.ToChar(IFormatProvider provider)
+        {
+            throw new InvalidCastException("Cannot cast BigDecimal to Char");
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a <see cref="DateTime"/>.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>This method always throws an <see cref="InvalidCastException"/>.</returns>
+        DateTime IConvertible.ToDateTime(IFormatProvider provider)
+        {
+            throw new InvalidCastException("Cannot cast BigDecimal to DateTime");
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a decimal.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>A decimal.</returns>
+        decimal IConvertible.ToDecimal(IFormatProvider provider)
+        {
+            return Convert.ToDecimal(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a double.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>A double.</returns>
+        double IConvertible.ToDouble(IFormatProvider provider)
+        {
+            return Convert.ToDouble(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a short.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>A short.</returns>
+        short IConvertible.ToInt16(IFormatProvider provider)
+        {
+            return Convert.ToInt16(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to an int.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>An int.</returns>
+        int IConvertible.ToInt32(IFormatProvider provider)
+        {
+            return Convert.ToInt32(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a long.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>A long.</returns>
+        long IConvertible.ToInt64(IFormatProvider provider)
+        {
+            return Convert.ToInt64(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a signed byte.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>A signed byte.</returns>
+        sbyte IConvertible.ToSByte(IFormatProvider provider)
+        {
+            return Convert.ToSByte(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a float.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>A float.</returns>
+        float IConvertible.ToSingle(IFormatProvider provider)
+        {
+            return Convert.ToSingle(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a string.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>A string.</returns>
+        string IConvertible.ToString(IFormatProvider provider)
+        {
+            return Convert.ToString(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to an unsigned short.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>An unsigned short.</returns>
+        ushort IConvertible.ToUInt16(IFormatProvider provider)
+        {
+            return Convert.ToUInt16(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to an unsigned int.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>An unsigned int.</returns>
+        uint IConvertible.ToUInt32(IFormatProvider provider)
+        {
+            return Convert.ToUInt32(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to an unsigned long.
+        /// </summary>
+        /// <param name="provider">The format provider.</param>
+        /// <returns>An unsigned long.</returns>
+        ulong IConvertible.ToUInt64(IFormatProvider provider)
+        {
+            return Convert.ToUInt64(this, provider);
+        }
+
+        /// <summary>
+        /// Converts the current <see cref="AvroDecimal"/> to a string.
+        /// </summary>
+        /// <param name="format"></param>
+        /// <param name="formatProvider">The format provider.</param>
+        /// <returns>A string representation of the numeric value.</returns>
+        public string ToString(string format, IFormatProvider formatProvider)
+        {
+            return ToString();
+        }
+
+        /// <summary>
+        /// Compares the value of the current <see cref="AvroDecimal"/> to the value of another object.
+        /// </summary>
+        /// <param name="obj">The object to compare.</param>
+        /// <returns>A value that indicates the relative order of the objects being compared.</returns>
+        public int CompareTo(object obj)
+        {
+            if (obj == null)
+                return 1;
+
+            if (!(obj is AvroDecimal))
+                throw new ArgumentException("Compare to object must be a BigDecimal", nameof(obj));
+
+            return CompareTo((AvroDecimal)obj);
+        }
+
+        /// <summary>
+        /// Compares the value of the current <see cref="AvroDecimal"/> to the value of another
+        /// <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="other">The <see cref="AvroDecimal"/> to compare.</param>
+        /// <returns>A value that indicates the relative order of the <see cref="AvroDecimal"/>
+        /// instances being compared.</returns>
+        public int CompareTo(AvroDecimal other)
+        {
+            var unscaledValueCompare = UnscaledValue.CompareTo(other.UnscaledValue);
+            var scaleCompare = Scale.CompareTo(other.Scale);
+
+            // if both are the same value, return the value
+            if (unscaledValueCompare == scaleCompare)
+                return unscaledValueCompare;
+
+            // if the scales are both the same return unscaled value
+            if (scaleCompare == 0)
+                return unscaledValueCompare;
+
+            var scaledValue = BigInteger.Divide(UnscaledValue, BigInteger.Pow(new BigInteger(10), Scale));
+            var otherScaledValue = BigInteger.Divide(other.UnscaledValue, BigInteger.Pow(new BigInteger(10), other.Scale));
+
+            return scaledValue.CompareTo(otherScaledValue);
+        }
+
+        /// <summary>
+        /// Returns a value that indicates whether the current <see cref="AvroDecimal"/> has the same
+        /// value as another <see cref="AvroDecimal"/>.
+        /// </summary>
+        /// <param name="other">The <see cref="AvroDecimal"/> to compare.</param>
+        /// <returns>true if the current <see cref="AvroDecimal"/> has the same value as <paramref name="other"/>;
+        /// otherwise false.</returns>
+        public bool Equals(AvroDecimal other)
+        {
+            return Scale == other.Scale && UnscaledValue == other.UnscaledValue;
+        }
+
+        private static byte[] GetBytesFromDecimal(decimal d)
+        {
+            byte[] bytes = new byte[16];
+
+            int[] bits = decimal.GetBits(d);
+            int lo = bits[0];
+            int mid = bits[1];
+            int hi = bits[2];
+            int flags = bits[3];
+
+            bytes[0] = (byte)lo;
+            bytes[1] = (byte)(lo >> 8);
+            bytes[2] = (byte)(lo >> 0x10);
+            bytes[3] = (byte)(lo >> 0x18);
+            bytes[4] = (byte)mid;
+            bytes[5] = (byte)(mid >> 8);
+            bytes[6] = (byte)(mid >> 0x10);
+            bytes[7] = (byte)(mid >> 0x18);
+            bytes[8] = (byte)hi;
+            bytes[9] = (byte)(hi >> 8);
+            bytes[10] = (byte)(hi >> 0x10);
+            bytes[11] = (byte)(hi >> 0x18);
+            bytes[12] = (byte)flags;
+            bytes[13] = (byte)(flags >> 8);
+            bytes[14] = (byte)(flags >> 0x10);
+            bytes[15] = (byte)(flags >> 0x18);
+
+            return bytes;
+        }
+    }
+    #pragma warning restore CA2225 // Operator overloads have named alternates
+    #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+}
diff --git a/lang/csharp/src/apache/main/CodeGen/CodeGen.cs b/lang/csharp/src/apache/main/CodeGen/CodeGen.cs
index fe7791e..a38ddcc 100644
--- a/lang/csharp/src/apache/main/CodeGen/CodeGen.cs
+++ b/lang/csharp/src/apache/main/CodeGen/CodeGen.cs
@@ -232,6 +232,7 @@ namespace Avro
                 case Schema.Type.Double:
                 case Schema.Type.Bytes:
                 case Schema.Type.String:
+                case Schema.Type.Logical:
                     break;
 
                 case Schema.Type.Enumeration:
@@ -771,6 +772,13 @@ namespace Avro
                         return CodeGenUtil.Object;
                     else
                         return getType(nullibleType, true, ref nullibleEnum);
+
+                case Schema.Type.Logical:
+                    var logicalSchema = schema as LogicalSchema;
+                    if (null == logicalSchema)
+                        throw new CodeGenException("Unable to cast schema into a logical schema");
+                    return logicalSchema.LogicalType.GetCSharpType(nullible).ToString();
+
             }
             throw new CodeGenException("Unable to generate CodeTypeReference for " + schema.Name + " type " + schema.Tag);
         }
diff --git a/lang/csharp/src/apache/main/Generic/GenericDatumWriter.cs b/lang/csharp/src/apache/main/Generic/GenericDatumWriter.cs
index e7d1138..eb8c81f 100644
--- a/lang/csharp/src/apache/main/Generic/GenericDatumWriter.cs
+++ b/lang/csharp/src/apache/main/Generic/GenericDatumWriter.cs
@@ -132,6 +132,8 @@ namespace Avro.Generic
                 case Schema.Type.Fixed:
                     //return obj is GenericFixed && (obj as GenericFixed).Schema.Equals(s);
                     return obj is GenericFixed && (obj as GenericFixed).Schema.SchemaName.Equals((sc as FixedSchema).SchemaName);
+                case Schema.Type.Logical:
+                    return (sc as LogicalSchema).LogicalType.IsInstanceOfLogicalType(obj);
                 default:
                     throw new AvroException("Unknown schema type: " + sc.Tag);
             }
diff --git a/lang/csharp/src/apache/main/Generic/GenericReader.cs b/lang/csharp/src/apache/main/Generic/GenericReader.cs
index b6813fa..74a1b67 100644
--- a/lang/csharp/src/apache/main/Generic/GenericReader.cs
+++ b/lang/csharp/src/apache/main/Generic/GenericReader.cs
@@ -226,6 +226,8 @@ namespace Avro.Generic
                     return ReadMap(reuse, (MapSchema)writerSchema, readerSchema, d);
                 case Schema.Type.Union:
                     return ReadUnion(reuse, (UnionSchema)writerSchema, readerSchema, d);
+                case Schema.Type.Logical:
+                    return ReadLogical(reuse, (LogicalSchema)writerSchema, readerSchema, d);
                 default:
                     throw new AvroException("Unknown schema type: " + writerSchema);
             }
@@ -551,6 +553,22 @@ namespace Avro.Generic
         }
 
         /// <summary>
+        /// Deserializes an object based on the writer's logical schema. Uses the underlying logical type to convert
+        /// the value to the logical type.
+        /// </summary>
+        /// <param name="reuse">If appropriate, uses this object instead of creating a new one.</param>
+        /// <param name="writerSchema">The UnionSchema that the writer used.</param>
+        /// <param name="readerSchema">The schema the reader uses.</param>
+        /// <param name="d">The decoder for serialization.</param>
+        /// <returns>The deserialized object.</returns>
+        protected virtual object ReadLogical(object reuse, LogicalSchema writerSchema, Schema readerSchema, Decoder d)
+        {
+            LogicalSchema ls = (LogicalSchema)readerSchema;
+
+            return writerSchema.LogicalType.ConvertToLogicalValue(Read(reuse, writerSchema.BaseSchema, ls.BaseSchema, d), ls);
+        }
+
+        /// <summary>
         /// Deserializes a fixed object and returns the object. The default implementation uses CreateFixed()
         /// and GetFixedBuffer() and returns what CreateFixed() returned.
         /// </summary>
@@ -661,6 +679,9 @@ namespace Avro.Generic
                 case Schema.Type.Union:
                     Skip((writerSchema as UnionSchema)[d.ReadUnionIndex()], d);
                     break;
+                case Schema.Type.Logical:
+                    Skip((writerSchema as LogicalSchema).BaseSchema, d);
+                    break;
                 default:
                     throw new AvroException("Unknown schema type: " + writerSchema);
             }
diff --git a/lang/csharp/src/apache/main/Generic/GenericWriter.cs b/lang/csharp/src/apache/main/Generic/GenericWriter.cs
index 5f439bb..3c0d5a9 100644
--- a/lang/csharp/src/apache/main/Generic/GenericWriter.cs
+++ b/lang/csharp/src/apache/main/Generic/GenericWriter.cs
@@ -160,6 +160,9 @@ namespace Avro.Generic
                 case Schema.Type.Union:
                     WriteUnion(schema as UnionSchema, value, encoder);
                     break;
+                case Schema.Type.Logical:
+                    WriteLogical(schema as LogicalSchema, value, encoder);
+                    break;
                 default:
                     Error(schema, value);
                     break;
@@ -404,6 +407,18 @@ namespace Avro.Generic
         }
 
         /// <summary>
+        /// Serializes a logical value object by using the underlying logical type to convert the value
+        /// to its base value.
+        /// </summary>
+        /// <param name="ls">The schema for serialization</param>
+        /// <param name="value">The value to be serialized</param>
+        /// <param name="encoder">The encoder for serialization</param>
+        protected virtual void WriteLogical(LogicalSchema ls, object value, Encoder encoder)
+        {
+            Write(ls.BaseSchema, ls.LogicalType.ConvertToBaseValue(value, ls), encoder);
+        }
+
+        /// <summary>
         /// Serialized a fixed object. The default implementation requires that the value is
         /// a GenericFixed object with an identical schema as es.
         /// </summary>
@@ -486,6 +501,8 @@ namespace Avro.Generic
                 case Schema.Type.Fixed:
                     //return obj is GenericFixed && (obj as GenericFixed).Schema.Equals(s);
                     return obj is GenericFixed && (obj as GenericFixed).Schema.SchemaName.Equals((sc as FixedSchema).SchemaName);
+                case Schema.Type.Logical:
+                    return (sc as LogicalSchema).LogicalType.IsInstanceOfLogicalType(obj);
                 default:
                     throw new AvroException("Unknown schema type: " + sc.Tag);
             }
diff --git a/lang/csharp/src/apache/main/Generic/PreresolvingDatumReader.cs b/lang/csharp/src/apache/main/Generic/PreresolvingDatumReader.cs
index 8b0cffa..c55957a 100644
--- a/lang/csharp/src/apache/main/Generic/PreresolvingDatumReader.cs
+++ b/lang/csharp/src/apache/main/Generic/PreresolvingDatumReader.cs
@@ -177,6 +177,8 @@ namespace Avro.Generic
                     return ResolveMap((MapSchema)writerSchema, (MapSchema)readerSchema);
                 case Schema.Type.Union:
                     return ResolveUnion((UnionSchema)writerSchema, readerSchema);
+                case Schema.Type.Logical:
+                    return ResolveLogical((LogicalSchema)writerSchema, (LogicalSchema)readerSchema);
                 default:
                     throw new AvroException("Unknown schema type: " + writerSchema);
             }
@@ -390,6 +392,12 @@ namespace Avro.Generic
             return array;
         }
 
+        private ReadItem ResolveLogical(LogicalSchema writerSchema, LogicalSchema readerSchema)
+        {
+            var baseReader = ResolveReader(writerSchema.BaseSchema, readerSchema.BaseSchema);
+            return (r, d) => readerSchema.LogicalType.ConvertToLogicalValue(baseReader(r, d), readerSchema);
+        }
+
         private ReadItem ResolveFixed(FixedSchema writerSchema, FixedSchema readerSchema)
         {
             if (readerSchema.Size != writerSchema.Size)
@@ -496,6 +504,9 @@ namespace Avro.Generic
                         lookup[i] = GetSkip( unionSchema[i] );
                     }
                     return d => lookup[d.ReadUnionIndex()](d);
+                case Schema.Type.Logical:
+                    var logicalSchema = (LogicalSchema)writerSchema;
+                    return GetSkip(logicalSchema.BaseSchema);
                 default:
                     throw new AvroException("Unknown schema type: " + writerSchema);
             }
diff --git a/lang/csharp/src/apache/main/Generic/PreresolvingDatumWriter.cs b/lang/csharp/src/apache/main/Generic/PreresolvingDatumWriter.cs
index e4e96d3..a90ac34 100644
--- a/lang/csharp/src/apache/main/Generic/PreresolvingDatumWriter.cs
+++ b/lang/csharp/src/apache/main/Generic/PreresolvingDatumWriter.cs
@@ -99,6 +99,8 @@ namespace Avro.Generic
                     return ResolveMap((MapSchema)schema);
                 case Schema.Type.Union:
                     return ResolveUnion((UnionSchema)schema);
+                case Schema.Type.Logical:
+                    return ResolveLogical((LogicalSchema)schema);
                 default:
                     return (v, e) => Error(schema, v);
             }
@@ -232,6 +234,17 @@ namespace Avro.Generic
             encoder.WriteArrayEnd();
         }
 
+        /// <summary>
+        /// Serializes a logical value object by using the underlying logical type to convert the value
+        /// to its base value.
+        /// </summary>
+        /// <param name="schema">The logical schema.</param>
+        protected WriteItem ResolveLogical(LogicalSchema schema)
+        {
+            var baseWriter = ResolveWriter(schema.BaseSchema);
+            return (d, e) => baseWriter(schema.LogicalType.ConvertToBaseValue(d, schema), e);
+        }
+
         private WriteItem ResolveMap(MapSchema mapSchema)
         {
             var itemWriter = ResolveWriter(mapSchema.ValueSchema);
diff --git a/lang/csharp/src/apache/main/Schema/LogicalSchema.cs b/lang/csharp/src/apache/main/Schema/LogicalSchema.cs
new file mode 100644
index 0000000..3c1928e
--- /dev/null
+++ b/lang/csharp/src/apache/main/Schema/LogicalSchema.cs
@@ -0,0 +1,118 @@
+/*
+ * 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 Avro.Util;
+using Newtonsoft.Json.Linq;
+
+namespace Avro
+{
+    /// <summary>
+    /// Class for logical type schemas.
+    /// </summary>
+    public class LogicalSchema : UnnamedSchema
+    {
+        /// <summary>
+        /// Schema for the underlying type that the logical type is based on.
+        /// </summary>
+        public Schema BaseSchema { get; private set; }
+
+        /// <summary>
+        /// The logical type name.
+        /// </summary>
+        public string LogicalTypeName { get; private set; }
+
+        /// <summary>
+        /// The logical type implementation that supports this logical type.
+        /// </summary>
+        public LogicalType LogicalType { get; private set; }
+
+        internal static LogicalSchema NewInstance(JToken jtok, PropertyMap props, SchemaNames names, string encspace)
+        {
+            JToken jtype = jtok["type"];
+            if (null == jtype) throw new AvroTypeException("Logical Type does not have 'type'");
+
+            return new LogicalSchema(Schema.ParseJson(jtype, names, encspace), JsonHelper.GetRequiredString(jtok, "logicalType"),  props);
+        }
+
+        private LogicalSchema(Schema baseSchema, string logicalTypeName,  PropertyMap props) : base(Type.Logical, props)
+        {
+            if (null == baseSchema) throw new ArgumentNullException(nameof(baseSchema));
+            BaseSchema = baseSchema;
+            LogicalTypeName = logicalTypeName;
+            LogicalType = LogicalTypeFactory.Instance.GetFromLogicalSchema(this);
+        }
+
+        /// <summary>
+        /// Writes logical schema in JSON format
+        /// </summary>
+        /// <param name="writer">JSON writer</param>
+        /// <param name="names">list of named schemas already written</param>
+        /// <param name="encspace">enclosing namespace of the schema</param>
+        protected internal override void WriteJson(Newtonsoft.Json.JsonTextWriter writer, SchemaNames names, string encspace)
+        {
+            writer.WriteStartObject();
+            writer.WritePropertyName("type");
+            BaseSchema.WriteJson(writer, names, encspace);
+            writer.WritePropertyName("logicalType");
+            writer.WriteValue(LogicalTypeName);
+            if (null != Props)
+                Props.WriteJson(writer);
+            writer.WriteEndObject();
+        }
+
+        /// <summary>
+        /// Checks if this schema can read data written by the given schema. Used for decoding data.
+        /// </summary>
+        /// <param name="writerSchema">writer schema</param>
+        /// <returns>true if this and writer schema are compatible based on the AVRO specification, false otherwise</returns>
+        public override bool CanRead(Schema writerSchema)
+        {
+            if (writerSchema.Tag != Tag) return false;
+
+            LogicalSchema that = writerSchema as LogicalSchema;
+            return BaseSchema.CanRead(that.BaseSchema);
+        }
+
+        /// <summary>
+        /// Function to compare equality of two logical schemas
+        /// </summary>
+        /// <param name="obj">other logical schema</param>
+        /// <returns>true if two schemas are equal, false otherwise</returns>
+        public override bool Equals(object obj)
+        {
+            if (this == obj) return true;
+
+            if (obj != null && obj is LogicalSchema that)
+            {
+                if (BaseSchema.Equals(that.BaseSchema))
+                    return areEqual(that.Props, Props);
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Hashcode function
+        /// </summary>
+        /// <returns></returns>
+        public override int GetHashCode()
+        {
+            return 29 * BaseSchema.GetHashCode() + getHashCode(Props);
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/main/Schema/Property.cs b/lang/csharp/src/apache/main/Schema/Property.cs
index 8ffd6bf..5fdb9ef 100644
--- a/lang/csharp/src/apache/main/Schema/Property.cs
+++ b/lang/csharp/src/apache/main/Schema/Property.cs
@@ -30,7 +30,7 @@ namespace Avro
         /// <summary>
         /// Set of reserved schema property names, any other properties not defined in this set are custom properties and can be added to this map
         /// </summary>
-        private static readonly HashSet<string> ReservedProps = new HashSet<string>() { "type", "name", "namespace", "fields", "items", "size", "symbols", "values", "aliases", "order", "doc", "default" };
+        private static readonly HashSet<string> ReservedProps = new HashSet<string>() { "type", "name", "namespace", "fields", "items", "size", "symbols", "values", "aliases", "order", "doc", "default", "logicalType" };
 
         /// <summary>
         /// Parses the custom properties from the given JSON object and stores them
diff --git a/lang/csharp/src/apache/main/Schema/Schema.cs b/lang/csharp/src/apache/main/Schema/Schema.cs
index aba4e72..284b574 100644
--- a/lang/csharp/src/apache/main/Schema/Schema.cs
+++ b/lang/csharp/src/apache/main/Schema/Schema.cs
@@ -104,7 +104,12 @@ namespace Avro
             /// <summary>
             /// A protocol error.
             /// </summary>
-            Error
+            Error,
+
+            /// <summary>
+            /// A logical type.
+            /// </summary>
+            Logical
         }
 
         /// <summary>
@@ -187,6 +192,8 @@ namespace Avro
                         return ArraySchema.NewInstance(jtok, props, names, encspace);
                     if (type.Equals("map", StringComparison.Ordinal))
                         return MapSchema.NewInstance(jtok, props, names, encspace);
+                    if (null != jo["logicalType"]) // logical type based on a primitive
+                        return LogicalSchema.NewInstance(jtok, props, names, encspace);
 
                     Schema schema = PrimitiveSchema.NewInstance((string)type, props);
                     if (null != schema) return schema;
@@ -195,6 +202,8 @@ namespace Avro
                 }
                 else if (jtype.Type == JTokenType.Array)
                     return UnionSchema.NewInstance(jtype as JArray, props, names, encspace);
+                else if (jtype.Type == JTokenType.Object && null != jo["logicalType"]) // logical type based on a complex type
+                    return LogicalSchema.NewInstance(jtok, props, names, encspace);
             }
             throw new AvroTypeException($"Invalid JSON for schema: {jtok} at '{jtok.Path}'");
         }
diff --git a/lang/csharp/src/apache/main/Specific/SpecificDatumWriter.cs b/lang/csharp/src/apache/main/Specific/SpecificDatumWriter.cs
index ebf1121..bfc8884 100644
--- a/lang/csharp/src/apache/main/Specific/SpecificDatumWriter.cs
+++ b/lang/csharp/src/apache/main/Specific/SpecificDatumWriter.cs
@@ -149,6 +149,8 @@ namespace Avro.Specific
                 case Schema.Type.Fixed:
                     return obj is SpecificFixed &&
                            ((obj as SpecificFixed).Schema as FixedSchema).SchemaName.Equals((sc as FixedSchema).SchemaName);
+                case Schema.Type.Logical:
+                    return (sc as LogicalSchema).LogicalType.IsInstanceOfLogicalType(obj);
                 default:
                     throw new AvroException("Unknown schema type: " + sc.Tag);
             }
diff --git a/lang/csharp/src/apache/main/Specific/SpecificWriter.cs b/lang/csharp/src/apache/main/Specific/SpecificWriter.cs
index 89f256b..b595241 100644
--- a/lang/csharp/src/apache/main/Specific/SpecificWriter.cs
+++ b/lang/csharp/src/apache/main/Specific/SpecificWriter.cs
@@ -219,6 +219,8 @@ namespace Avro.Specific
                 case Schema.Type.Fixed:
                     return obj is SpecificFixed &&
                            ((obj as SpecificFixed).Schema as FixedSchema).SchemaName.Equals((sc as FixedSchema).SchemaName);
+                case Schema.Type.Logical:
+                    return (sc as LogicalSchema).LogicalType.IsInstanceOfLogicalType(obj);
                 default:
                     throw new AvroException("Unknown schema type: " + sc.Tag);
             }
diff --git a/lang/csharp/src/apache/main/Util/Date.cs b/lang/csharp/src/apache/main/Util/Date.cs
new file mode 100644
index 0000000..0f52b58
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/Date.cs
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// The 'date' logical type.
+    /// </summary>
+    public class Date : LogicalUnixEpochType<DateTime>
+    {
+        /// <summary>
+        /// The logical type name for Date.
+        /// </summary>
+        public static readonly string LogicalTypeName = "date";
+
+        /// <summary>
+        /// Initializes a new Date logical type.
+        /// </summary>
+        public Date() : base(LogicalTypeName)
+        { }
+
+
+        /// <inheritdoc/>
+        public override void ValidateSchema(LogicalSchema schema)
+        {
+            if (Schema.Type.Int != schema.BaseSchema.Tag)
+                throw new AvroTypeException("'date' can only be used with an underlying int type");
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+        {
+            var date = ((DateTime)logicalValue).Date;
+            return (date - UnixEpochDateTime).Days;
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+        {
+            var noDays = (int)baseValue;
+            return UnixEpochDateTime.AddDays(noDays);
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/main/Util/Decimal.cs b/lang/csharp/src/apache/main/Util/Decimal.cs
new file mode 100644
index 0000000..3714bd1
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/Decimal.cs
@@ -0,0 +1,131 @@
+/*
+ * 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.Globalization;
+using System.Numerics;
+using Avro.Generic;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// The 'decimal' logical type.
+    /// </summary>
+    public class Decimal : LogicalType
+    {
+        /// <summary>
+        /// The logical type name for Decimal.
+        /// </summary>
+        public static readonly string LogicalTypeName = "decimal";
+
+        /// <summary>
+        /// Initializes a new Decimal logical type.
+        /// </summary>
+        public Decimal() : base(LogicalTypeName)
+        { }
+
+        /// <inheritdoc/>
+        public override void ValidateSchema(LogicalSchema schema)
+        {
+            if (Schema.Type.Bytes != schema.BaseSchema.Tag && Schema.Type.Fixed != schema.BaseSchema.Tag)
+                throw new AvroTypeException("'decimal' can only be used with an underlying bytes or fixed type");
+
+            var precisionVal = schema.GetProperty("precision");
+
+            if (string.IsNullOrEmpty(precisionVal))
+                throw new AvroTypeException("'decimal' requires a 'precision' property");
+
+            var precision = int.Parse(precisionVal, CultureInfo.CurrentCulture);
+
+            if (precision <= 0)
+                throw new AvroTypeException("'decimal' requires a 'precision' property that is greater than zero");
+
+            var scale = GetScalePropertyValueFromSchema(schema);
+
+            if (scale < 0 || scale > precision)
+                throw new AvroTypeException("'decimal' requires a 'scale' property that is zero or less than or equal to 'precision'");
+        }
+
+        /// <inheritdoc/>      
+        public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+        {
+            var decimalValue = (AvroDecimal)logicalValue;
+            var logicalScale = GetScalePropertyValueFromSchema(schema);
+            var scale = decimalValue.Scale;
+
+            if (scale != logicalScale)
+                throw new ArgumentOutOfRangeException(nameof(logicalValue), $"The decimal value has a scale of {scale} which cannot be encoded against a logical 'decimal' with a scale of {logicalScale}");
+
+            var buffer = decimalValue.UnscaledValue.ToByteArray();
+
+            Array.Reverse(buffer);
+
+            return Schema.Type.Bytes == schema.BaseSchema.Tag
+                ? (object)buffer
+                : (object)new GenericFixed(
+                    (FixedSchema)schema.BaseSchema,
+                    GetDecimalFixedByteArray(buffer, ((FixedSchema)schema.BaseSchema).Size,
+                    decimalValue.Sign < 0 ? (byte)0xFF : (byte)0x00));
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+        {
+            var buffer = Schema.Type.Bytes == schema.BaseSchema.Tag
+                ? (byte[])baseValue
+                : ((GenericFixed)baseValue).Value;
+
+            Array.Reverse(buffer);
+
+            return new AvroDecimal(new BigInteger(buffer), GetScalePropertyValueFromSchema(schema));
+        }
+
+        /// <inheritdoc/>
+        public override Type GetCSharpType(bool nullible)
+        {
+            return nullible ? typeof(AvroDecimal?) : typeof(AvroDecimal);
+        }
+
+        /// <inheritdoc/>
+        public override bool IsInstanceOfLogicalType(object logicalValue)
+        {
+            return logicalValue is AvroDecimal;
+        }
+
+        private static int GetScalePropertyValueFromSchema(Schema schema, int defaultVal = 0)
+        {
+            var scaleVal = schema.GetProperty("scale");
+
+            return string.IsNullOrEmpty(scaleVal) ? defaultVal : int.Parse(scaleVal, CultureInfo.CurrentCulture);
+        }
+
+        private static byte[] GetDecimalFixedByteArray(byte[] sourceBuffer, int size, byte fillValue)
+        {
+            var paddedBuffer = new byte[size];
+
+            var offset = size - sourceBuffer.Length;
+
+            for (var idx = 0; idx < size; idx++)
+            {
+                paddedBuffer[idx] = idx < offset ? fillValue : sourceBuffer[idx - offset];
+            }
+
+            return paddedBuffer;
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/main/Util/LogicalType.cs b/lang/csharp/src/apache/main/Util/LogicalType.cs
new file mode 100644
index 0000000..fec6bda
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/LogicalType.cs
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// Base for all logical type implementations.
+    /// </summary>
+    public abstract class LogicalType
+    {
+        /// <summary>
+        /// The logical type name.
+        /// </summary>
+        public string Name { get; }
+
+        /// <summary>
+        /// Initializes the base logical type.
+        /// </summary>
+        /// <param name="name">The logical type name.</param>
+        protected LogicalType(string name)
+        {
+            Name = name;
+        }
+
+        /// <summary>
+        /// Applies logical type validation for a given logical schema.
+        /// </summary>
+        /// <param name="schema">The schema to be validated.</param>
+        public virtual void ValidateSchema(LogicalSchema schema)
+        { }
+
+        /// <summary>
+        /// Converts a logical value to an instance of its base type.
+        /// </summary>
+        /// <param name="logicalValue">The logical value to convert.</param>
+        /// <param name="schema">The schema that represents the target of the conversion.</param>
+        /// <returns>An object representing the encoded value of the base type.</returns>
+        public abstract object ConvertToBaseValue(object logicalValue, LogicalSchema schema);
+
+        /// <summary>
+        /// Converts a base value to an instance of the logical type.
+        /// </summary>
+        /// <param name="baseValue">The base value to convert.</param>
+        /// <param name="schema">The schema that represents the target of the conversion.</param>
+        /// <returns>An object representing the encoded value of the logical type.</returns>
+        public abstract object ConvertToLogicalValue(object baseValue, LogicalSchema schema);
+
+        /// <summary>
+        /// Retrieve the .NET type that is represented by the logical type implementation.
+        /// </summary>
+        /// <param name="nullible">A flag indicating whether it should be nullible.</param>
+        public abstract Type GetCSharpType(bool nullible);
+
+        /// <summary>
+        /// Determines if a given object is an instance of the logical type.
+        /// </summary>
+        /// <param name="logicalValue">The logical value to test.</param>
+        public abstract bool IsInstanceOfLogicalType(object logicalValue);
+    }
+}
diff --git a/lang/csharp/src/apache/main/Util/LogicalTypeFactory.cs b/lang/csharp/src/apache/main/Util/LogicalTypeFactory.cs
new file mode 100644
index 0000000..3dcae79
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/LogicalTypeFactory.cs
@@ -0,0 +1,85 @@
+/*
+ * 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.Collections.Generic;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// A factory for logical type implementations.
+    /// </summary>
+    public class LogicalTypeFactory
+    {
+        private readonly IDictionary<string, LogicalType> _logicalTypes;
+
+        /// <summary>
+        /// Returns the <see cref="LogicalTypeFactory" /> singleton.
+        /// </summary>
+        /// <returns>The <see cref="LogicalTypeFactory" /> singleton. </returns>
+        public static LogicalTypeFactory Instance { get; } = new LogicalTypeFactory();
+
+        private LogicalTypeFactory()
+        {
+            _logicalTypes = new Dictionary<string, LogicalType>()
+            {
+                { Decimal.LogicalTypeName, new Decimal() },
+                { Date.LogicalTypeName, new Date() },
+                { TimeMillisecond.LogicalTypeName, new TimeMillisecond() },
+                { TimeMicrosecond.LogicalTypeName, new TimeMicrosecond() },
+                { TimestampMillisecond.LogicalTypeName, new TimestampMillisecond() },
+                { TimestampMicrosecond.LogicalTypeName, new TimestampMicrosecond() }
+            };
+        }
+
+        /// <summary>
+        /// Registers or replaces a logical type implementation.
+        /// </summary>
+        /// <param name="logicalType">The <see cref="LogicalType"/> implementation that should be registered.</param>
+        public void Register(LogicalType logicalType)
+        {
+            _logicalTypes[logicalType.Name] = logicalType;
+        }
+
+        /// <summary>
+        /// Retrieves a logical type implementation for a given logical schema.
+        /// </summary>
+        /// <param name="schema">The schema.</param>
+        /// <param name="ignoreInvalidOrUnknown">A flag to indicate if an exception should be thrown for invalid
+        /// or unknown logical types.</param>
+        /// <returns>A <see cref="LogicalType" />.</returns>
+        public LogicalType GetFromLogicalSchema(LogicalSchema schema, bool ignoreInvalidOrUnknown = false)
+        {
+            try
+            {
+                if (!_logicalTypes.TryGetValue(schema.LogicalTypeName, out LogicalType logicalType))
+                    throw new AvroTypeException("Logical type '" + schema.LogicalTypeName + "' is not supported.");
+
+                logicalType.ValidateSchema(schema);
+
+                return logicalType;
+            }
+            catch (AvroTypeException)
+            {
+                if (!ignoreInvalidOrUnknown)
+                    throw;
+            }
+
+            return null;
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/main/Util/LogicalUnixEpochType.cs b/lang/csharp/src/apache/main/Util/LogicalUnixEpochType.cs
new file mode 100644
index 0000000..f4187d0
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/LogicalUnixEpochType.cs
@@ -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
+ *
+ *     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;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// Base for all logical type implementations that are based on the Unix Epoch date/time.
+    /// </summary>
+    public abstract class LogicalUnixEpochType<T> : LogicalType
+        where T : struct
+    {
+        /// <summary>
+        /// The date and time of the Unix Epoch.
+        /// </summary>
+        protected static readonly DateTime UnixEpochDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+        /// <summary>
+        /// Initializes the base logical type.
+        /// </summary>
+        /// <param name="name">The logical type name.</param>
+        protected LogicalUnixEpochType(string name)
+            : base(name)
+        { }
+
+        /// <inheritdoc/>
+        public override Type GetCSharpType(bool nullible)
+        {
+            return nullible ? typeof(T?) : typeof(T);
+        }
+
+        /// <inheritdoc/>
+        public override bool IsInstanceOfLogicalType(object logicalValue)
+        {
+            return logicalValue is T;
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/main/Util/TimeMicrosecond.cs b/lang/csharp/src/apache/main/Util/TimeMicrosecond.cs
new file mode 100644
index 0000000..f561d6f
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/TimeMicrosecond.cs
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// The 'time-micros' logical type.
+    /// </summary>
+    public class TimeMicrosecond : LogicalUnixEpochType<TimeSpan>
+    {
+        private static readonly TimeSpan _maxTime = new TimeSpan(23, 59, 59);
+        
+        /// <summary>
+        /// The logical type name for TimeMicrosecond.
+        /// </summary>
+        public static readonly string LogicalTypeName = "time-micros";
+
+        /// <summary>
+        /// Initializes a new TimeMicrosecond logical type.
+        /// </summary>
+        public TimeMicrosecond() : base(LogicalTypeName)
+        { }
+
+        /// <inheritdoc/>
+        public override void ValidateSchema(LogicalSchema schema)
+        {
+            if (Schema.Type.Long != schema.BaseSchema.Tag)
+                throw new AvroTypeException("'time-micros' can only be used with an underlying long type");
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+        {
+            var time = (TimeSpan)logicalValue;
+
+            if (time > _maxTime)
+                throw new ArgumentOutOfRangeException(nameof(logicalValue), "A 'time-micros' value can only have the range '00:00:00' to '23:59:59'.");
+
+            return (long)(time - UnixEpochDateTime.TimeOfDay).TotalMilliseconds * 1000;
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+        {
+            var noMs = (long)baseValue / 1000;
+            return UnixEpochDateTime.TimeOfDay.Add(TimeSpan.FromMilliseconds(noMs));
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/main/Util/TimeMillisecond.cs b/lang/csharp/src/apache/main/Util/TimeMillisecond.cs
new file mode 100644
index 0000000..9008fa3
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/TimeMillisecond.cs
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// The 'time-millis' logical type.
+    /// </summary>
+    public class TimeMillisecond : LogicalUnixEpochType<TimeSpan>
+    {
+        private static readonly TimeSpan _maxTime = new TimeSpan(23, 59, 59);
+
+        /// <summary>
+        /// The logical type name for TimeMillisecond.
+        /// </summary>
+        public static readonly string LogicalTypeName = "time-millis";
+
+        /// <summary>
+        /// Initializes a new TimeMillisecond logical type.
+        /// </summary>
+        public TimeMillisecond() : base(LogicalTypeName)
+        { }
+
+        /// <inheritdoc/>
+        public override void ValidateSchema(LogicalSchema schema)
+        {
+            if (Schema.Type.Int != schema.BaseSchema.Tag)
+                throw new AvroTypeException("'time-millis' can only be used with an underlying int type");
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+        {
+            var time = (TimeSpan)logicalValue;
+
+            if (time > _maxTime)
+                throw new ArgumentOutOfRangeException(nameof(logicalValue), "A 'time-millis' value can only have the range '00:00:00' to '23:59:59'.");
+
+            return (int)(time - UnixEpochDateTime.TimeOfDay).TotalMilliseconds;
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+        {
+            var noMs = (int)baseValue;
+            return UnixEpochDateTime.TimeOfDay.Add(TimeSpan.FromMilliseconds(noMs));
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/main/Util/TimestampMicrosecond.cs b/lang/csharp/src/apache/main/Util/TimestampMicrosecond.cs
new file mode 100644
index 0000000..54a421a
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/TimestampMicrosecond.cs
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// The 'timestamp-micros' logical type.
+    /// </summary>
+    public class TimestampMicrosecond : LogicalUnixEpochType<DateTime>
+    {
+        /// <summary>
+        /// The logical type name for TimestampMicrosecond.
+        /// </summary>
+        public static readonly string LogicalTypeName = "timestamp-micros";
+
+        /// <summary>
+        /// Initializes a new TimestampMicrosecond logical type.
+        /// </summary>
+        public TimestampMicrosecond() : base(LogicalTypeName)
+        { }
+
+        /// <inheritdoc/>
+        public override void ValidateSchema(LogicalSchema schema)
+        {
+            if (Schema.Type.Long != schema.BaseSchema.Tag)
+                throw new AvroTypeException("'timestamp-micros' can only be used with an underlying long type");
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+        {
+            var date = ((DateTime)logicalValue).ToUniversalTime();
+            return (long)((date - UnixEpochDateTime).TotalMilliseconds * 1000);
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+        {
+            var noMs = (long)baseValue / 1000;
+            return UnixEpochDateTime.AddMilliseconds(noMs);
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/main/Util/TimestampMillisecond.cs b/lang/csharp/src/apache/main/Util/TimestampMillisecond.cs
new file mode 100644
index 0000000..481d474
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/TimestampMillisecond.cs
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+namespace Avro.Util
+{
+    /// <summary>
+    /// The 'timestamp-millis' logical type.
+    /// </summary>
+    public class TimestampMillisecond : LogicalUnixEpochType<DateTime>
+    {
+        /// <summary>
+        /// The logical type name for TimestampMillisecond.
+        /// </summary>
+        public static readonly string LogicalTypeName = "timestamp-millis";
+
+        /// <summary>
+        /// Initializes a new TimestampMillisecond logical type.
+        /// </summary>
+        public TimestampMillisecond() : base(LogicalTypeName)
+        { }
+
+        /// <inheritdoc/>
+        public override void ValidateSchema(LogicalSchema schema)
+        {
+            if (Schema.Type.Long != schema.BaseSchema.Tag)
+                throw new AvroTypeException("'timestamp-millis' can only be used with an underlying long type");
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+        {
+            var date = ((DateTime)logicalValue).ToUniversalTime();
+            return (long)(date - UnixEpochDateTime).TotalMilliseconds;
+        }
+
+        /// <inheritdoc/>
+        public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+        {
+            var noMs = (long)baseValue;
+            return UnixEpochDateTime.AddMilliseconds(noMs);
+        }
+    }
+}
diff --git a/lang/csharp/src/apache/test/Generic/GenericTests.cs b/lang/csharp/src/apache/test/Generic/GenericTests.cs
index 2f6e45d..e17215d 100644
--- a/lang/csharp/src/apache/test/Generic/GenericTests.cs
+++ b/lang/csharp/src/apache/test/Generic/GenericTests.cs
@@ -108,6 +108,50 @@ namespace Avro.Test.Generic
             test(schema, mkMap(values));
         }
 
+        [TestCase()]
+        public void TestLogical_Date()
+        {
+            test("{\"type\": \"int\", \"logicalType\": \"date\"}", DateTime.UtcNow.Date);
+        }
+
+        [TestCase()]
+        public void TestLogical_TimeMillisecond()
+        {
+            test("{\"type\": \"int\", \"logicalType\": \"time-millis\"}", new TimeSpan(3, 0, 0));
+        }
+
+        [TestCase()]
+        public void TestLogical_TimeMicrosecond()
+        {
+            test("{\"type\": \"long\", \"logicalType\": \"time-micros\"}", new TimeSpan(3, 0, 0));
+        }
+
+        [TestCase()]
+        public void TestLogical_TimestampMillisecond()
+        {
+            test("{\"type\": \"long\", \"logicalType\": \"timestamp-millis\"}", new DateTime(1990, 1, 1, 14, 15, 30));
+        }
+
+        [TestCase()]
+        public void TestLogical_TimestampMicrosecond()
+        {
+            test("{\"type\": \"long\", \"logicalType\": \"timestamp-micros\"}", new DateTime(1990, 1, 1, 14, 15, 30));
+        }
+
+        [TestCase()]
+        public void TestLogical_Decimal_Bytes()
+        {
+            test("{\"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 30, \"scale\": 2}",
+                (AvroDecimal)12345678912345.55M);
+        }
+
+        [TestCase()]
+        public void TestLogical_Decimal_Fixed()
+        {
+            test("{\"type\": {\"type\": \"fixed\", \"size\": 16, \"name\": \"n\"}, \"logicalType\": \"decimal\", \"precision\": 30, \"scale\": 2}",
+                (AvroDecimal)12345678912345.55M);
+        }
+
         [TestCase("[{\"type\":\"record\", \"name\":\"n\", \"fields\":[{\"name\":\"f1\", \"type\":\"string\"}]}, \"string\"]",
             "{\"type\":\"record\", \"name\":\"n\", \"fields\":[{\"name\":\"f1\", \"type\":\"string\"}]}",
             new object[] { "f1", "v1" })]
diff --git a/lang/csharp/src/apache/test/Schema/SchemaTests.cs b/lang/csharp/src/apache/test/Schema/SchemaTests.cs
index ba07f5a..50d54b7 100644
--- a/lang/csharp/src/apache/test/Schema/SchemaTests.cs
+++ b/lang/csharp/src/apache/test/Schema/SchemaTests.cs
@@ -261,6 +261,27 @@ namespace Avro.Test
             testToString(sc);
         }
 
+        [TestCase("{\"type\": \"int\", \"logicalType\": \"date\"}", "int", "date")]
+        public void TestLogicalPrimitive(string s, string baseType, string logicalType)
+        {
+            Schema sc = Schema.Parse(s);
+            Assert.AreEqual(Schema.Type.Logical, sc.Tag);
+            LogicalSchema logsc = sc as LogicalSchema;
+            Assert.AreEqual(baseType, logsc.BaseSchema.Name);
+            Assert.AreEqual(logicalType, logsc.LogicalType.Name);
+
+            testEquality(s, sc);
+            testToString(sc);
+        }
+
+        [TestCase("{\"type\": \"int\", \"logicalType\": \"unknown\"}", "unknown")]
+        public void TestUnknownLogical(string s, string unknownType)
+        {
+            var err = Assert.Throws<AvroTypeException>(() => Schema.Parse(s));
+
+            Assert.AreEqual("Logical type '" + unknownType + "' is not supported.", err.Message);
+        }
+
         [TestCase("{\"type\": \"map\", \"values\": \"long\"}", "long")]
         public void TestMap(string s, string value)
         {
diff --git a/lang/csharp/src/apache/test/Util/LogicalTypeTests.cs b/lang/csharp/src/apache/test/Util/LogicalTypeTests.cs
new file mode 100644
index 0000000..358177d
--- /dev/null
+++ b/lang/csharp/src/apache/test/Util/LogicalTypeTests.cs
@@ -0,0 +1,182 @@
+/*
+ * 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.Globalization;
+using Avro.Util;
+using NUnit.Framework;
+
+namespace Avro.Test
+{
+    [TestFixture]
+    class LogicalTypeTests
+    {
+        [TestCase("1234.56")]
+        [TestCase("-1234.56")]
+        [TestCase("123456789123456789.56")]
+        [TestCase("-123456789123456789.56")]
+        [TestCase("000000000000000001.01")]
+        [TestCase("-000000000000000001.01")]
+        public void TestDecimal(string s)
+        {
+            var schema = (LogicalSchema)Schema.Parse("{\"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 4, \"scale\": 2 }");
+
+            var avroDecimal = new Avro.Util.Decimal();
+            var decimalVal = (AvroDecimal)decimal.Parse(s);
+
+            var convertedDecimalVal = (AvroDecimal)avroDecimal.ConvertToLogicalValue(avroDecimal.ConvertToBaseValue(decimalVal, schema), schema);
+
+            Assert.AreEqual(decimalVal, convertedDecimalVal);
+        }
+
+        [TestCase]
+        public void TestDecimalMinMax()
+        {
+            var schema = (LogicalSchema)Schema.Parse("{\"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 4, \"scale\": 0 }");
+
+            var avroDecimal = new Avro.Util.Decimal();
+
+            foreach (var decimalVal in new AvroDecimal[] { decimal.MinValue, decimal.MaxValue })
+            {
+                var convertedDecimalVal = (AvroDecimal)avroDecimal.ConvertToLogicalValue(avroDecimal.ConvertToBaseValue(decimalVal, schema), schema);
+
+                Assert.AreEqual(decimalVal, convertedDecimalVal);
+            }
+        }
+
+        [TestCase]
+        public void TestDecimalOutOfRangeException()
+        {
+            var schema = (LogicalSchema)Schema.Parse("{\"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 4, \"scale\": 2 }");
+
+            var avroDecimal = new Avro.Util.Decimal();
+            var decimalVal = (AvroDecimal)1234.567M; // scale of 3 should throw ArgumentOutOfRangeException
+
+            Assert.Throws<ArgumentOutOfRangeException>(() => avroDecimal.ConvertToBaseValue(decimalVal, schema));
+        }
+
+        [TestCase("01/01/2019")]
+        [TestCase("05/05/2019")]
+        [TestCase("05/05/2019 00:00:00Z")]
+        [TestCase("05/05/2019 01:00:00Z")]
+        [TestCase("05/05/2019 01:00:00+01:00")]
+        public void TestDate(string s)
+        {
+            var schema = (LogicalSchema)Schema.Parse("{\"type\": \"int\", \"logicalType\": \"date\"}");
+
+            var date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+            if (date.Kind != DateTimeKind.Utc)
+            {
+                date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.AssumeLocal);
+            }
+
+            var avroDate = new Date();
+
+            var convertedDate = (DateTime)avroDate.ConvertToLogicalValue(avroDate.ConvertToBaseValue(date, schema), schema);
+
+            Assert.AreEqual(new TimeSpan(0, 0, 0), convertedDate.TimeOfDay); // the time should always be 00:00:00
+            Assert.AreEqual(date.Date, convertedDate.Date);
+        }
+
+        [TestCase("01/01/2019 14:20:00Z", "01/01/2019 14:20:00Z")]
+        [TestCase("01/01/2019 14:20:00", "01/01/2019 14:20:00Z")]
+        [TestCase("05/05/2019 14:20:00Z", "05/05/2019 14:20:00Z")]
+        [TestCase("05/05/2019 14:20:00+01:00", "05/05/2019 13:20:00Z")]
+        [TestCase("05/05/2019 00:00:00Z", "05/05/2019 00:00:00Z")]
+        [TestCase("05/05/2019 00:00:00+01:00", "05/04/2019 23:00:00Z")] // adjusted to UTC
+        public void TestTimestampMillisecond(string s, string e)
+        {
+            var schema = (LogicalSchema)Schema.Parse("{\"type\": \"long\", \"logicalType\": \"timestamp-millis\"}");
+
+            var date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+            if (date.Kind != DateTimeKind.Utc)
+            {
+                date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.AssumeLocal);
+            }
+
+            var expectedDate = DateTime.Parse(e, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+            var avroTimestampMilli = new TimestampMillisecond();
+            var convertedDate = (DateTime)avroTimestampMilli.ConvertToLogicalValue(avroTimestampMilli.ConvertToBaseValue(date, schema), schema);
+            Assert.AreEqual(expectedDate, convertedDate);
+        }
+
+        [TestCase("01/01/2019 14:20:00Z", "01/01/2019 14:20:00Z")]
+        [TestCase("01/01/2019 14:20:00", "01/01/2019 14:20:00Z")]
+        [TestCase("05/05/2019 14:20:00Z", "05/05/2019 14:20:00Z")]
+        [TestCase("05/05/2019 14:20:00+01:00", "05/05/2019 13:20:00Z")]
+        [TestCase("05/05/2019 00:00:00Z", "05/05/2019 00:00:00Z")]
+        [TestCase("05/05/2019 00:00:00+01:00", "05/04/2019 23:00:00Z")] // adjusted to UTC
+        public void TestTimestampMicrosecond(string s, string e)
+        {
+            var schema = (LogicalSchema)Schema.Parse("{\"type\": \"long\", \"logicalType\": \"timestamp-micros\"}");
+
+            var date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+            if (date.Kind != DateTimeKind.Utc)
+            {
+                date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.AssumeLocal);
+            }
+
+            var expectedDate = DateTime.Parse(e, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+            var avroTimestampMicro = new TimestampMicrosecond();
+            var convertedDate = (DateTime)avroTimestampMicro.ConvertToLogicalValue(avroTimestampMicro.ConvertToBaseValue(date, schema), schema);
+            Assert.AreEqual(expectedDate, convertedDate);
+        }
+
+        [TestCase("01:20:10", "01:20:10", false)]
+        [TestCase("23:00:00", "23:00:00", false)]
+        [TestCase("01:00:00:00", null, true)]
+        public void TestTime(string s, string e, bool expectRangeError)
+        {
+            var timeMilliSchema = (LogicalSchema)Schema.Parse("{\"type\": \"int\", \"logicalType\": \"time-millis\"}");
+            var timeMicroSchema = (LogicalSchema)Schema.Parse("{\"type\": \"long\", \"logicalType\": \"time-micros\"}");
+
+            var time = TimeSpan.Parse(s);
+            
+            var avroTimeMilli = new TimeMillisecond();
+            var avroTimeMicro = new TimeMicrosecond();
+
+            if (expectRangeError)
+            {
+                Assert.Throws<ArgumentOutOfRangeException>(() =>
+                {
+                    avroTimeMilli.ConvertToLogicalValue(avroTimeMilli.ConvertToBaseValue(time, timeMilliSchema), timeMilliSchema);
+                });
+                Assert.Throws<ArgumentOutOfRangeException>(() =>
+                {
+                    avroTimeMicro.ConvertToLogicalValue(avroTimeMilli.ConvertToBaseValue(time, timeMicroSchema), timeMicroSchema);
+                });
+            }
+            else
+            {
+                var expectedTime = TimeSpan.Parse(e);
+
+                var convertedTime = (TimeSpan)avroTimeMilli.ConvertToLogicalValue(avroTimeMilli.ConvertToBaseValue(time, timeMilliSchema), timeMilliSchema);
+                Assert.AreEqual(expectedTime, convertedTime);
+
+                convertedTime = (TimeSpan)avroTimeMicro.ConvertToLogicalValue(avroTimeMicro.ConvertToBaseValue(time, timeMicroSchema), timeMicroSchema);
+                Assert.AreEqual(expectedTime, convertedTime);
+
+            }
+        }
+    }
+}