You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by we...@apache.org on 2019/06/22 20:17:41 UTC

[arrow] branch master updated: ARROW-4337: [C#] Implemented Fluent API for building arrays and record batches

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 1515fe1  ARROW-4337: [C#] Implemented Fluent API for building arrays and record batches
1515fe1 is described below

commit 1515fe10c039fb6685df2e282e2e888b773caa86
Author: Eric Erhardt <er...@microsoft.com>
AuthorDate: Sat Jun 22 15:17:33 2019 -0500

    ARROW-4337: [C#] Implemented Fluent API for building arrays and record batches
    
    Implemented Fluent API for building arrays and record batches. Includes some unit tests for builders but more work is needed to improve coverage.
    
    Replaces #3574 by rebasing on latest master and addressing PR feedback. I couldn't push to that branch, so I needed to create a new PR.
    
    /cc @chutchinson @pgovind @wesm
    
    Author: Eric Erhardt <er...@microsoft.com>
    Author: Christopher Hutchinson <cs...@fzcorp.com>
    
    Closes #4640 from eerhardt/4337-array-builder-api and squashes the following commits:
    
    51499da8a <Eric Erhardt> Fix unit tests and rat exclude files.
    235ba4909 <Eric Erhardt> Respond to PR feedback
    5d97cbfaa <Christopher Hutchinson> Implemented Fluent API for building arrays and record batches
---
 csharp/examples/Examples.sln                       |  31 +++
 .../FluentBuilderExample.csproj                    |  12 ++
 csharp/examples/FluentBuilderExample/Program.cs    |  61 ++++++
 csharp/src/Apache.Arrow/Arrays/BinaryArray.cs      | 114 +++++++++++
 csharp/src/Apache.Arrow/Arrays/BooleanArray.cs     | 134 ++++++++++++-
 csharp/src/Apache.Arrow/Arrays/Date32Array.cs      |  29 ++-
 csharp/src/Apache.Arrow/Arrays/Date64Array.cs      |  32 +++-
 csharp/src/Apache.Arrow/Arrays/DoubleArray.cs      |   8 +
 csharp/src/Apache.Arrow/Arrays/FloatArray.cs       |   8 +
 csharp/src/Apache.Arrow/Arrays/Int16Array.cs       |   8 +
 csharp/src/Apache.Arrow/Arrays/Int32Array.cs       |   8 +
 csharp/src/Apache.Arrow/Arrays/Int64Array.cs       |   8 +
 csharp/src/Apache.Arrow/Arrays/Int8Array.cs        |   8 +
 csharp/src/Apache.Arrow/Arrays/PrimitiveArray.cs   |  59 +++---
 .../Apache.Arrow/Arrays/PrimitiveArrayBuilder.cs   | 169 +++++++++++++++++
 csharp/src/Apache.Arrow/Arrays/StringArray.cs      |  34 +++-
 csharp/src/Apache.Arrow/Arrays/TimestampArray.cs   |  95 +++++++++-
 csharp/src/Apache.Arrow/Arrays/UInt16Array.cs      |   8 +
 csharp/src/Apache.Arrow/Arrays/UInt32Array.cs      |   8 +
 csharp/src/Apache.Arrow/Arrays/UInt64Array.cs      |   8 +
 csharp/src/Apache.Arrow/Arrays/UInt8Array.cs       |   8 +
 csharp/src/Apache.Arrow/ArrowBuffer.Builder.cs     |  66 +++----
 csharp/src/Apache.Arrow/BitUtility.cs              |  25 +++
 .../Apache.Arrow/Interfaces/IArrowArrayBuilder.cs  |  46 +++++
 csharp/src/Apache.Arrow/Ipc/ArrowStreamWriter.cs   |  14 +-
 csharp/src/Apache.Arrow/RecordBatch.Builder.cs     | 155 +++++++++++++++
 csharp/src/Apache.Arrow/RecordBatch.cs             |   2 +-
 csharp/src/Apache.Arrow/Schema.Builder.cs          |   7 +
 .../test/Apache.Arrow.Tests/ArrayBuilderTests.cs   |  72 +++++++
 .../Apache.Arrow.Tests/ArrowBufferBuilderTests.cs  |  38 +++-
 .../test/Apache.Arrow.Tests/ArrowReaderVerifier.cs |  14 ++
 csharp/test/Apache.Arrow.Tests/BitUtilityTests.cs  |  15 ++
 .../test/Apache.Arrow.Tests/BooleanArrayTests.cs   | 211 +++++++++++++++++++++
 .../Extensions/DateTimeOffsetExtensions.cs}        |  33 ++--
 dev/release/rat_exclude_files.txt                  |   2 +
 35 files changed, 1433 insertions(+), 117 deletions(-)

diff --git a/csharp/examples/Examples.sln b/csharp/examples/Examples.sln
new file mode 100644
index 0000000..c0a4199
--- /dev/null
+++ b/csharp/examples/Examples.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27703.2042
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentBuilderExample", "FluentBuilderExample\FluentBuilderExample.csproj", "{ECE22119-D91D-44F7-9575-85B98F946289}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apache.Arrow", "..\src\Apache.Arrow\Apache.Arrow.csproj", "{1FE1DE95-FF6E-4895-82E7-909713C53524}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{ECE22119-D91D-44F7-9575-85B98F946289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{ECE22119-D91D-44F7-9575-85B98F946289}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{ECE22119-D91D-44F7-9575-85B98F946289}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{ECE22119-D91D-44F7-9575-85B98F946289}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1FE1DE95-FF6E-4895-82E7-909713C53524}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1FE1DE95-FF6E-4895-82E7-909713C53524}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1FE1DE95-FF6E-4895-82E7-909713C53524}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1FE1DE95-FF6E-4895-82E7-909713C53524}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {C22A81AD-8B64-4D7C-97AC-49E9F118AE78}
+	EndGlobalSection
+EndGlobal
diff --git a/csharp/examples/FluentBuilderExample/FluentBuilderExample.csproj b/csharp/examples/FluentBuilderExample/FluentBuilderExample.csproj
new file mode 100644
index 0000000..575a274
--- /dev/null
+++ b/csharp/examples/FluentBuilderExample/FluentBuilderExample.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Apache.Arrow\Apache.Arrow.csproj" />
+  </ItemGroup>
+
+</Project>
\ No newline at end of file
diff --git a/csharp/examples/FluentBuilderExample/Program.cs b/csharp/examples/FluentBuilderExample/Program.cs
new file mode 100644
index 0000000..a55f841
--- /dev/null
+++ b/csharp/examples/FluentBuilderExample/Program.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
+//
+//     http://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 Apache.Arrow;
+using Apache.Arrow.Ipc;
+using Apache.Arrow.Memory;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace FluentBuilderExample
+{
+    public class Program
+    {
+        public static async Task Main(string[] args)
+        {
+            // Use a specific memory pool from which arrays will be allocated (optional)
+
+            var memoryAllocator = new NativeMemoryAllocator(alignment: 64);
+
+            // Build a record batch using the Fluent API
+
+            var recordBatch = new RecordBatch.Builder(memoryAllocator)
+                .Append("Column A", false, col => col.Int32(array => array.AppendRange(Enumerable.Range(0, 10))))
+                .Append("Column B", false, col => col.Float(array => array.AppendRange(Enumerable.Range(0, 10).Select(x => Convert.ToSingle(x * 2)))))
+                .Append("Column C", false, col => col.String(array => array.AppendRange(Enumerable.Range(0, 10).Select(x => $"Item {x+1}"))))
+                .Append("Column D", false, col => col.Boolean(array => array.AppendRange(Enumerable.Range(0, 10).Select(x => x % 2 == 0))))
+                .Build();
+
+            // Print memory allocation statistics
+
+            Console.WriteLine("Allocations: {0}", memoryAllocator.Statistics.Allocations);
+            Console.WriteLine("Allocated: {0} byte(s)", memoryAllocator.Statistics.BytesAllocated);
+
+            // Write record batch to a file
+
+            using (var stream = File.OpenWrite("test.arrow"))
+            using (var writer = new ArrowFileWriter(stream, recordBatch.Schema))
+            {
+                await writer.WriteRecordBatchAsync(recordBatch);
+                await writer.WriteFooterAsync();
+            }
+
+            Console.WriteLine("Done");
+            Console.ReadKey();
+        }
+    }
+}
diff --git a/csharp/src/Apache.Arrow/Arrays/BinaryArray.cs b/csharp/src/Apache.Arrow/Arrays/BinaryArray.cs
index 12ef5ee..4cd82b6 100644
--- a/csharp/src/Apache.Arrow/Arrays/BinaryArray.cs
+++ b/csharp/src/Apache.Arrow/Arrays/BinaryArray.cs
@@ -15,12 +15,25 @@
 
 using Apache.Arrow.Types;
 using System;
+using System.Collections.Generic;
 using System.Runtime.CompilerServices;
+using Apache.Arrow.Memory;
 
 namespace Apache.Arrow
 {
     public class BinaryArray: Array
     {
+        public class Builder : BuilderBase<BinaryArray, Builder>
+        {
+            public Builder() : base(BinaryType.Default) { }
+            public Builder(IArrowType dataType) : base(dataType) { }
+
+            protected override BinaryArray Build(ArrayData data)
+            {
+                return new BinaryArray(data);
+            }
+        }
+
         public BinaryArray(ArrayData data)
             : base(data)
         {
@@ -35,6 +48,107 @@ namespace Apache.Arrow
             data.EnsureBufferCount(3);
         }
 
+        public abstract class BuilderBase<TArray, TBuilder>: IArrowArrayBuilder<byte, TArray, TBuilder>
+            where TArray: IArrowArray
+            where TBuilder: class, IArrowArrayBuilder<byte, TArray, TBuilder>
+        {
+            protected IArrowType DataType { get; }
+            protected TBuilder Instance => this as TBuilder;
+            protected ArrowBuffer.Builder<int> ValueOffsets { get; }
+            protected ArrowBuffer.Builder<byte> ValueBuffer { get; }
+            protected int Offset { get; set; }
+
+            protected BuilderBase(IArrowType dataType)
+            {
+                DataType = dataType;
+                ValueOffsets = new ArrowBuffer.Builder<int>();
+                ValueBuffer = new ArrowBuffer.Builder<byte>();
+            }
+
+            protected abstract TArray Build(ArrayData data);
+
+            public TArray Build(MemoryAllocator allocator = default)
+            {
+                ValueOffsets.Append(Offset);
+
+                var data = new ArrayData(DataType, ValueOffsets.Length - 1, 0, 0, 
+                    new [] { ArrowBuffer.Empty, ValueOffsets.Build(allocator), ValueBuffer.Build(allocator) });
+
+                return Build(data);
+            }
+
+            public TBuilder Append(byte value)
+            {
+                ValueOffsets.Append(Offset);
+                ValueBuffer.Append(value);
+                Offset++;
+                return Instance;
+            }
+
+            public TBuilder Append(ReadOnlySpan<byte> span)
+            {
+                ValueOffsets.Append(Offset);
+                ValueBuffer.Append(span);
+                Offset += span.Length;
+                return Instance;
+            }
+
+            public TBuilder AppendRange(IEnumerable<byte[]> values)
+            {
+                foreach (var arr in values)
+                {
+                    var len = ValueBuffer.Length;
+                    ValueOffsets.Append(Offset);
+                    ValueBuffer.Append(arr);
+                    Offset += ValueBuffer.Length - len;
+                }
+
+                return Instance;
+            }
+
+            public TBuilder AppendRange(IEnumerable<byte> values)
+            {
+                var len = ValueBuffer.Length;
+                ValueOffsets.Append(Offset);
+                ValueBuffer.AppendRange(values);
+                Offset += ValueBuffer.Length - len;
+                return Instance;
+            }
+
+            public TBuilder Reserve(int capacity)
+            {
+                ValueOffsets.Reserve(capacity + 1);
+                ValueBuffer.Reserve(capacity);
+                return Instance;
+            }
+
+            public TBuilder Resize(int length)
+            {
+                ValueOffsets.Resize(length + 1);
+                ValueBuffer.Resize(length);
+                return Instance;
+            }
+
+            public TBuilder Swap(int i, int j)
+            {
+                // TODO: Implement
+                throw new NotImplementedException();
+            }
+
+            public TBuilder Set(int index, byte value)
+            {
+                // TODO: Implement
+                throw new NotImplementedException();
+            }
+
+            public TBuilder Clear()
+            {
+                ValueOffsets.Clear();
+                ValueBuffer.Clear();
+                return Instance;
+            }
+        }
+
         public BinaryArray(IArrowType dataType, int length,
             ArrowBuffer valueOffsetsBuffer,
             ArrowBuffer dataBuffer,
diff --git a/csharp/src/Apache.Arrow/Arrays/BooleanArray.cs b/csharp/src/Apache.Arrow/Arrays/BooleanArray.cs
index ddee188..cb039fd 100644
--- a/csharp/src/Apache.Arrow/Arrays/BooleanArray.cs
+++ b/csharp/src/Apache.Arrow/Arrays/BooleanArray.cs
@@ -13,12 +13,139 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+using Apache.Arrow.Memory;
 using Apache.Arrow.Types;
+using System;
+using System.Collections.Generic;
 
 namespace Apache.Arrow
 {
-    public class BooleanArray: PrimitiveArray<byte>
+    public class BooleanArray: Array
     {
+        public class Builder : IArrowArrayBuilder<bool, BooleanArray, Builder>
+        {
+            private ArrowBuffer.Builder<byte> ValueBuffer { get; }
+
+            public int Length { get; protected set; }
+            public int Capacity => BitUtility.ByteCount(ValueBuffer.Capacity);
+
+            public Builder()
+            {
+                ValueBuffer = new ArrowBuffer.Builder<byte>();
+                Length = 0;
+            }
+
+            public Builder Append(bool value)
+            {
+                if (Length % 8 == 0)
+                {
+                    // append a new byte to the buffer when needed
+                    ValueBuffer.Append(0);
+                }
+                BitUtility.SetBit(ValueBuffer.Span, Length, value);
+                Length++;
+                return this;
+            }
+
+            public Builder Append(ReadOnlySpan<bool> span)
+            {
+                foreach (var value in span)
+                {
+                    Append(value);
+                }
+                return this;
+            }
+
+            public Builder AppendRange(IEnumerable<bool> values)
+            {
+                foreach (var value in values)
+                {
+                    Append(value);
+                }
+                return this;
+            }
+
+            public BooleanArray Build(MemoryAllocator allocator = default)
+            {
+                return new BooleanArray(
+                    ValueBuffer.Build(allocator), ArrowBuffer.Empty,
+                    Length, 0, 0);
+            }
+
+            public Builder Clear()
+            {
+                ValueBuffer.Clear();
+                Length = 0;
+                return this;
+            }
+
+            public Builder Reserve(int capacity)
+            {
+                if (capacity < 0)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(capacity));
+                }
+
+                ValueBuffer.Reserve(BitUtility.ByteCount(capacity));
+                return this;
+            }
+
+            public Builder Resize(int length)
+            {
+                if (length < 0)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(length));
+                }
+
+                ValueBuffer.Resize(BitUtility.ByteCount(length));
+                Length = length;
+                return this;
+            }
+
+            public Builder Toggle(int index)
+            {
+                CheckIndex(index);
+                BitUtility.ToggleBit(ValueBuffer.Span, index);
+                return this;
+            }
+
+            public Builder Set(int index)
+            {
+                CheckIndex(index);
+                BitUtility.SetBit(ValueBuffer.Span, index);
+                return this;
+            }
+
+            public Builder Set(int index, bool value)
+            {
+                CheckIndex(index);
+                BitUtility.SetBit(ValueBuffer.Span, index, value);
+                return this;
+            }
+
+            public Builder Swap(int i, int j)
+            {
+                CheckIndex(i);
+                CheckIndex(j);
+                var bi = BitUtility.GetBit(ValueBuffer.Span, i);
+                var bj = BitUtility.GetBit(ValueBuffer.Span, j);
+                BitUtility.SetBit(ValueBuffer.Span, i, bj);
+                BitUtility.SetBit(ValueBuffer.Span, j, bi);
+                return this;
+            }
+
+            private void CheckIndex(int index)
+            {
+                if (index < 0 || index >= Length)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(index));
+                }
+            }
+        }
+
+        public ArrowBuffer ValueBuffer => Data.Buffers[1];
+        public ReadOnlySpan<byte> Values => ValueBuffer.Span.Slice(0, (int) Math.Ceiling(Length / 8.0));
+
         public BooleanArray(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
@@ -34,11 +161,8 @@ namespace Apache.Arrow
 
         public override void Accept(IArrowArrayVisitor visitor) => Accept(this, visitor);
 
-        public bool? GetBoolean(int index)
+        public bool GetBoolean(int index)
         {
-            if (IsNull(index))
-                return null;
-
             return BitUtility.GetBit(Values, index);
         }
     }
diff --git a/csharp/src/Apache.Arrow/Arrays/Date32Array.cs b/csharp/src/Apache.Arrow/Arrays/Date32Array.cs
index a0ea7e2..bc9c351 100644
--- a/csharp/src/Apache.Arrow/Arrays/Date32Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/Date32Array.cs
@@ -20,6 +20,26 @@ namespace Apache.Arrow
 {
     public class Date32Array: PrimitiveArray<int>
     {
+        private const int MillisecondsPerDay = 86400000;
+
+        public class Builder : PrimitiveArrayBuilder<DateTimeOffset, int, Date32Array, Builder>
+        {
+            internal class DateBuilder : PrimitiveArrayBuilder<int, Date32Array, DateBuilder>
+            {
+                protected override Date32Array Build(
+                    ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                    int length, int nullCount, int offset) =>
+                    new Date32Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+            }
+
+            public Builder() : base(new DateBuilder()) { }
+
+            protected override int ConvertTo(DateTimeOffset value)
+            {
+                return (int) (value.ToUnixTimeMilliseconds() / MillisecondsPerDay);
+            }
+        }
+
         public Date32Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
@@ -37,13 +57,10 @@ namespace Apache.Arrow
 
         public DateTimeOffset? GetDate(int index)
         {
-            var value = GetValue(index);
-
-            const long millisecondsPerDay = 86_400_000;
+            var values = ValueBuffer.Span.CastTo<int>().Slice(0, Length);
+            var value = values[index];
 
-            return value.HasValue
-                ? DateTimeOffset.FromUnixTimeMilliseconds(value.Value * millisecondsPerDay)
-                : default;
+            return DateTimeOffset.FromUnixTimeMilliseconds(value * MillisecondsPerDay);
         }
     }
 }
diff --git a/csharp/src/Apache.Arrow/Arrays/Date64Array.cs b/csharp/src/Apache.Arrow/Arrays/Date64Array.cs
index 307cda0..bf1e15c 100644
--- a/csharp/src/Apache.Arrow/Arrays/Date64Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/Date64Array.cs
@@ -15,6 +15,8 @@
 
 using Apache.Arrow.Types;
 using System;
+using System.Collections.Generic;
+using Apache.Arrow.Memory;
 
 namespace Apache.Arrow
 {
@@ -27,6 +29,24 @@ namespace Apache.Arrow
                 new[] { nullBitmapBuffer, valueBuffer }))
         { }
 
+        public class Builder : PrimitiveArrayBuilder<DateTimeOffset, long, Date64Array, Builder>
+        {
+            internal class DateBuilder: PrimitiveArrayBuilder<long, Date64Array, DateBuilder>
+            {
+                protected override Date64Array Build(
+                    ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                    int length, int nullCount, int offset) =>
+                    new Date64Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+            } 
+
+            public Builder() : base(new DateBuilder()) { }
+
+            protected override long ConvertTo(DateTimeOffset value)
+            {
+                return value.ToUnixTimeMilliseconds();
+            }
+        }
+
         public Date64Array(ArrayData data) 
             : base(data)
         {
@@ -37,11 +57,15 @@ namespace Apache.Arrow
 
         public DateTimeOffset? GetDate(int index)
         {
-            var value = GetValue(index);
+            if (!IsValid(index))
+            {
+                return null;
+            }
+
+            var values = ValueBuffer.Span.CastTo<long>().Slice(0, Length);
+            var value = values[index];
 
-            return value.HasValue
-                ? DateTimeOffset.FromUnixTimeMilliseconds(value.Value)
-                : default;
+            return DateTimeOffset.FromUnixTimeMilliseconds(value);
         }
     }
 }
diff --git a/csharp/src/Apache.Arrow/Arrays/DoubleArray.cs b/csharp/src/Apache.Arrow/Arrays/DoubleArray.cs
index 0c50e5a..6450aa1 100644
--- a/csharp/src/Apache.Arrow/Arrays/DoubleArray.cs
+++ b/csharp/src/Apache.Arrow/Arrays/DoubleArray.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class DoubleArray : PrimitiveArray<double>
     {
+        public class Builder : PrimitiveArrayBuilder<double, DoubleArray, Builder>
+        {
+            protected override DoubleArray Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new DoubleArray(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public DoubleArray(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/FloatArray.cs b/csharp/src/Apache.Arrow/Arrays/FloatArray.cs
index b4c9e60..8feca32 100644
--- a/csharp/src/Apache.Arrow/Arrays/FloatArray.cs
+++ b/csharp/src/Apache.Arrow/Arrays/FloatArray.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class FloatArray : PrimitiveArray<float>
     {
+        public class Builder : PrimitiveArrayBuilder<float, FloatArray, Builder>
+        {
+            protected override FloatArray Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new FloatArray(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public FloatArray(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/Int16Array.cs b/csharp/src/Apache.Arrow/Arrays/Int16Array.cs
index 639c6d5..0401865 100644
--- a/csharp/src/Apache.Arrow/Arrays/Int16Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/Int16Array.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class Int16Array : PrimitiveArray<short>
     {
+        public class Builder : PrimitiveArrayBuilder<short, Int16Array, Builder>
+        {
+            protected override Int16Array Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer, 
+                int length, int nullCount, int offset) =>
+                new Int16Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public Int16Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/Int32Array.cs b/csharp/src/Apache.Arrow/Arrays/Int32Array.cs
index 8bd0048..ef356c7 100644
--- a/csharp/src/Apache.Arrow/Arrays/Int32Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/Int32Array.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class Int32Array : PrimitiveArray<int>
     {
+        public class Builder : PrimitiveArrayBuilder<int, Int32Array, Builder>
+        {
+            protected override Int32Array Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new Int32Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public Int32Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/Int64Array.cs b/csharp/src/Apache.Arrow/Arrays/Int64Array.cs
index d010a8d..fe8fbc6 100644
--- a/csharp/src/Apache.Arrow/Arrays/Int64Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/Int64Array.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class Int64Array : PrimitiveArray<long>
     {
+        public class Builder : PrimitiveArrayBuilder<long, Int64Array, Builder>
+        {
+            protected override Int64Array Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new Int64Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public Int64Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/Int8Array.cs b/csharp/src/Apache.Arrow/Arrays/Int8Array.cs
index 67e68bd..58d543a 100644
--- a/csharp/src/Apache.Arrow/Arrays/Int8Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/Int8Array.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class Int8Array : PrimitiveArray<sbyte>
     {
+        public class Builder : PrimitiveArrayBuilder<sbyte, Int8Array, Builder>
+        {
+            protected override Int8Array Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new Int8Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public Int8Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/PrimitiveArray.cs b/csharp/src/Apache.Arrow/Arrays/PrimitiveArray.cs
index 617bddc..6098266 100644
--- a/csharp/src/Apache.Arrow/Arrays/PrimitiveArray.cs
+++ b/csharp/src/Apache.Arrow/Arrays/PrimitiveArray.cs
@@ -22,46 +22,45 @@ namespace Apache.Arrow
     public abstract class PrimitiveArray<T> : Array
         where T : struct
     {
-        
-    protected PrimitiveArray(ArrayData data)
-        : base(data)
-    {
-        data.EnsureBufferCount(2);
-    }
+        protected PrimitiveArray(ArrayData data)
+            : base(data)
+        {
+            data.EnsureBufferCount(2);
+        }
 
-    public ArrowBuffer ValueBuffer => Data.Buffers[1];
+        public ArrowBuffer ValueBuffer => Data.Buffers[1];
 
-    public ReadOnlySpan<T> Values => ValueBuffer.Span.CastTo<T>().Slice(0, Length);
+        public ReadOnlySpan<T> Values => ValueBuffer.Span.CastTo<T>().Slice(0, Length);
 
-    [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    public T? GetValue(int index)
-    {
-        return IsValid(index) ? Values[index] : (T?) null;
-    }
-
-    public IList<T?> ToList(bool includeNulls = false)
-    {
-        var span = Values;
-        var list = new List<T?>(span.Length);
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public T? GetValue(int index)
+        {
+            return IsValid(index) ? Values[index] : (T?) null;
+        }
 
-        for (var i = 0; i < span.Length; i++)
+        public IList<T?> ToList(bool includeNulls = false)
         {
-            var value = GetValue(i);
+            var span = Values;
+            var list = new List<T?>(span.Length);
 
-            if (value.HasValue)
-            {
-                list.Add(value.Value);
-            }
-            else
+            for (var i = 0; i < span.Length; i++)
             {
-                if (includeNulls)
+                var value = GetValue(i);
+
+                if (value.HasValue)
                 {
-                    list.Add(null);
+                    list.Add(value.Value);
+                }
+                else
+                {
+                    if (includeNulls)
+                    {
+                        list.Add(null);
+                    }
                 }
             }
-        }
 
-        return list;
-    }
+            return list;
+        }
     }
 }
diff --git a/csharp/src/Apache.Arrow/Arrays/PrimitiveArrayBuilder.cs b/csharp/src/Apache.Arrow/Arrays/PrimitiveArrayBuilder.cs
new file mode 100644
index 0000000..41997a4
--- /dev/null
+++ b/csharp/src/Apache.Arrow/Arrays/PrimitiveArrayBuilder.cs
@@ -0,0 +1,169 @@
+// 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
+//
+//     http://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 Apache.Arrow.Memory;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Apache.Arrow
+{
+    public abstract class PrimitiveArrayBuilder<TFrom, TTo, TArray, TBuilder> : IArrowArrayBuilder<TFrom, TArray>
+        where TTo: struct
+        where TArray: IArrowArray
+        where TBuilder: class, IArrowArrayBuilder<TArray>
+    {
+        protected TBuilder Instance => this as TBuilder;
+        protected IArrowArrayBuilder<TTo, TArray, IArrowArrayBuilder<TTo, TArray>> ArrayBuilder { get; }
+
+        internal PrimitiveArrayBuilder(IArrowArrayBuilder<TTo, TArray, IArrowArrayBuilder<TTo, TArray>> builder)
+        {
+            ArrayBuilder = builder ?? throw new ArgumentNullException(nameof(builder));
+        }
+
+        public TArray Build(MemoryAllocator allocator = default) => ArrayBuilder.Build(allocator);
+
+        public TBuilder Append(TFrom value)
+        {
+            ArrayBuilder.Append(ConvertTo(value));
+            return Instance;
+        }
+
+        public TBuilder Append(ReadOnlySpan<TFrom> span)
+        {
+            ArrayBuilder.Reserve(span.Length);
+            foreach (var value in span)
+            {
+                Append(value);
+            }
+            return Instance;
+        }
+
+        public TBuilder AppendRange(IEnumerable<TFrom> values)
+        {
+            ArrayBuilder.AppendRange(values.Select(ConvertTo));
+            return Instance;
+        }
+
+        public TBuilder Reserve(int capacity)
+        {
+            ArrayBuilder.Reserve(capacity);
+            return Instance;
+        }
+
+        public TBuilder Resize(int length)
+        {
+            ArrayBuilder.Resize(length);
+            return Instance;
+        }
+
+        public TBuilder Swap(int i, int j)
+        {
+            ArrayBuilder.Swap(i, j);
+            return Instance;
+        }
+
+        public TBuilder Set(int index, TFrom value)
+        {
+            ArrayBuilder.Set(index, ConvertTo(value));
+            return Instance;
+        }
+
+        public TBuilder Clear()
+        {
+            ArrayBuilder.Clear();
+            return Instance;
+        }
+
+        protected abstract TTo ConvertTo(TFrom value);
+    }
+
+    public abstract class PrimitiveArrayBuilder<T, TArray, TBuilder> : IArrowArrayBuilder<T, TArray, TBuilder>
+        where T: struct
+        where TArray : IArrowArray
+        where TBuilder : class, IArrowArrayBuilder<T, TArray>
+    {
+        protected TBuilder Instance => this as TBuilder;
+        protected ArrowBuffer.Builder<T> ValueBuffer { get; }
+
+        // TODO: Implement support for null values (null bitmaps)
+
+        internal PrimitiveArrayBuilder()
+        {
+            ValueBuffer = new ArrowBuffer.Builder<T>();
+        }
+
+        public TBuilder Resize(int length)
+        {
+            ValueBuffer.Resize(length);
+            return Instance;
+        }
+
+        public TBuilder Reserve(int capacity)
+        {
+            ValueBuffer.Reserve(capacity);
+            return Instance;
+        }
+
+        public TBuilder Append(T value)
+        {
+            ValueBuffer.Append(value);
+            return Instance;
+        }
+
+        public TBuilder Append(ReadOnlySpan<T> span)
+        {
+            ValueBuffer.Append(span);
+            return Instance;
+        }
+
+        public TBuilder AppendRange(IEnumerable<T> values)
+        {
+            ValueBuffer.AppendRange(values);
+            return Instance;
+        }
+
+        public TBuilder Clear()
+        {
+            ValueBuffer.Clear();
+            return Instance;
+        }
+
+        public TBuilder Set(int index, T value)
+        {
+            ValueBuffer.Span[index] = value;
+            return Instance;
+        }
+
+        public TBuilder Swap(int i, int j)
+        {
+            var x = ValueBuffer.Span[i];
+            ValueBuffer.Span[i] = ValueBuffer.Span[j];
+            ValueBuffer.Span[j] = x;
+            return Instance;
+        }
+
+        public TArray Build(MemoryAllocator allocator = default)
+        {
+            return Build(
+                ValueBuffer.Build(allocator), ArrowBuffer.Empty,
+                ValueBuffer.Length, 0, 0);
+        }
+
+        protected abstract TArray Build(
+            ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+            int length, int nullCount, int offset);
+    }
+}
diff --git a/csharp/src/Apache.Arrow/Arrays/StringArray.cs b/csharp/src/Apache.Arrow/Arrays/StringArray.cs
index 9ea9522..f3aca19 100644
--- a/csharp/src/Apache.Arrow/Arrays/StringArray.cs
+++ b/csharp/src/Apache.Arrow/Arrays/StringArray.cs
@@ -13,14 +13,44 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+using Apache.Arrow.Types;
+using System.Collections.Generic;
 using System.Runtime.InteropServices;
 using System.Text;
-using Apache.Arrow.Types;
 
 namespace Apache.Arrow
 {
     public class StringArray: BinaryArray
     {
+        public static readonly Encoding DefaultEncoding = Encoding.UTF8;
+
+        public new class Builder : BuilderBase<StringArray, Builder>
+        {
+            public Builder() : base(StringType.Default) { }
+
+            protected override StringArray Build(ArrayData data)
+            {
+                return new StringArray(data);
+            }
+
+            public Builder Append(string value, Encoding encoding = null)
+            {
+                encoding = encoding ?? DefaultEncoding;
+                var span = encoding.GetBytes(value);
+                return Append(span);
+            }
+
+            public Builder AppendRange(IEnumerable<string> values, Encoding encoding = null)
+            {
+                foreach (var value in values)
+                {
+                    Append(value);
+                }
+
+                return this;
+            }
+        }
+
         public StringArray(ArrayData data) 
             : base(ArrowTypeId.String, data) { }
 
@@ -37,7 +67,7 @@ namespace Apache.Arrow
 
         public string GetString(int index, Encoding encoding = default)
         {
-            encoding = encoding ?? Encoding.UTF8;
+            encoding = encoding ?? DefaultEncoding;
 
             var bytes = GetBytes(index);
 
diff --git a/csharp/src/Apache.Arrow/Arrays/TimestampArray.cs b/csharp/src/Apache.Arrow/Arrays/TimestampArray.cs
index f9fd0ae..482c0d0 100644
--- a/csharp/src/Apache.Arrow/Arrays/TimestampArray.cs
+++ b/csharp/src/Apache.Arrow/Arrays/TimestampArray.cs
@@ -15,18 +15,85 @@
 
 using Apache.Arrow.Types;
 using System;
+using System.Diagnostics;
 using System.IO;
 
 namespace Apache.Arrow
 {
     public class TimestampArray: PrimitiveArray<long>
     {
+        private static readonly DateTimeOffset Epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);
+
+        public class Builder: PrimitiveArrayBuilder<DateTimeOffset, long, TimestampArray, Builder>
+        {
+            internal class TimestampBuilder : PrimitiveArrayBuilder<long, TimestampArray, TimestampBuilder>
+            {
+                internal TimestampBuilder(TimestampType type)
+                {
+                    DataType = type ?? throw new ArgumentNullException(nameof(type));
+                }
+
+                public TimestampType DataType { get; }
+
+                protected override TimestampArray Build(
+                    ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                    int length, int nullCount, int offset) =>
+                    new TimestampArray(DataType, valueBuffer, nullBitmapBuffer,
+                        length, nullCount, offset);
+            }
+
+            protected TimeZoneInfo TimeZone { get; }
+            protected TimeUnit Unit { get; }
+
+            public Builder(TimeUnit unit = TimeUnit.Millisecond, string timezone = null)
+                : base(new TimestampBuilder(new TimestampType(unit, timezone)))
+            {
+                Unit = unit;
+                TimeZone = string.IsNullOrEmpty(timezone) 
+                               ? TimeZoneInfo.Utc
+                               : TimeZoneInfo.FindSystemTimeZoneById(timezone) ?? TimeZoneInfo.Utc;
+            }
+
+            protected override long ConvertTo(DateTimeOffset value)
+            {
+                // We must return the absolute time since the UNIX epoch while
+                // respecting the timezone offset; the calculation is as follows:
+                //
+                // - Compute span between epoch and specified time (using correct offset)
+                // - Compute number of units per tick
+
+                var span = value.ToOffset(TimeZone.BaseUtcOffset) - Epoch;
+                var ticks = span.Ticks;
+
+                switch (Unit)
+                {
+                    case TimeUnit.Nanosecond:
+                        return ticks / 100;
+                    case TimeUnit.Microsecond:
+                        return ticks / TimeSpan.TicksPerMillisecond / 1000;
+                    case TimeUnit.Millisecond:
+                        return ticks / TimeSpan.TicksPerMillisecond;
+                    case TimeUnit.Second:
+                        return ticks / TimeSpan.TicksPerSecond;
+                    default:
+                        throw new InvalidOperationException($"unsupported time unit <{Unit}>");
+                }
+            }
+        }
+
+        protected TimeZoneInfo TimeZone { get; }
+
         public TimestampArray(
+            TimestampType type,
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
-            : this(new ArrayData(TimestampType.Default, length, nullCount, offset,
-                new[] { nullBitmapBuffer, valueBuffer }))
-        { }
+            : this(new ArrayData(type, length, nullCount, offset,
+                new[] {nullBitmapBuffer, valueBuffer}))
+        {
+            TimeZone = type.Timezone != null 
+                ? TimeZoneInfo.FindSystemTimeZoneById(type.Timezone) ?? TimeZoneInfo.Utc
+                : TimeZoneInfo.Utc;
+        }
 
         public TimestampArray(ArrayData data)
             : base(data)
@@ -43,23 +110,35 @@ namespace Apache.Arrow
                 return null;
             }
 
+            Debug.Assert((Data.DataType as TimestampType) != null);
+
             var value = Values[index];
-            var type = Data.DataType as TimestampType;
+            var type = (TimestampType) Data.DataType;
+
+            long ticks;
 
             switch (type.Unit)
             {
                 case TimeUnit.Nanosecond:
-                    return DateTimeOffset.FromUnixTimeMilliseconds(value / 1000000);
+                    ticks = value * 100;
+                    break;
                 case TimeUnit.Microsecond:
-                    return DateTimeOffset.FromUnixTimeMilliseconds(value / 1000);
+                    ticks = value * TimeSpan.TicksPerMillisecond * 1000;
+                    break;
                 case TimeUnit.Millisecond:
-                    return DateTimeOffset.FromUnixTimeMilliseconds(value);
+                    ticks = value * TimeSpan.TicksPerMillisecond;
+                    break;
                 case TimeUnit.Second:
-                    return DateTimeOffset.FromUnixTimeSeconds(value);
+                    ticks = value * TimeSpan.TicksPerSecond;
+                    break;
                 default:
                     throw new InvalidDataException(
                         $"Unsupported timestamp unit <{type.Unit}>");
             }
+
+            return new DateTimeOffset(
+                Epoch.Ticks + TimeZone.BaseUtcOffset.Ticks + ticks,
+                TimeZone.BaseUtcOffset);
         }
     }
 }
diff --git a/csharp/src/Apache.Arrow/Arrays/UInt16Array.cs b/csharp/src/Apache.Arrow/Arrays/UInt16Array.cs
index 8ffe3c1..bba244f 100644
--- a/csharp/src/Apache.Arrow/Arrays/UInt16Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/UInt16Array.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class UInt16Array : PrimitiveArray<ushort>
     {
+        public class Builder : PrimitiveArrayBuilder<ushort, UInt16Array, Builder>
+        {
+            protected override UInt16Array Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new UInt16Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public UInt16Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/UInt32Array.cs b/csharp/src/Apache.Arrow/Arrays/UInt32Array.cs
index 0321ea5..65320be 100644
--- a/csharp/src/Apache.Arrow/Arrays/UInt32Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/UInt32Array.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class UInt32Array : PrimitiveArray<uint>
     {
+        public class Builder : PrimitiveArrayBuilder<uint, UInt32Array, Builder>
+        {
+            protected override UInt32Array Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new UInt32Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public UInt32Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/UInt64Array.cs b/csharp/src/Apache.Arrow/Arrays/UInt64Array.cs
index 09b1e6d..617949f 100644
--- a/csharp/src/Apache.Arrow/Arrays/UInt64Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/UInt64Array.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class UInt64Array : PrimitiveArray<ulong>
     {
+        public class Builder : PrimitiveArrayBuilder<ulong, UInt64Array, Builder>
+        {
+            protected override UInt64Array Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new UInt64Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public UInt64Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/Arrays/UInt8Array.cs b/csharp/src/Apache.Arrow/Arrays/UInt8Array.cs
index e4506b5..5cde791 100644
--- a/csharp/src/Apache.Arrow/Arrays/UInt8Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/UInt8Array.cs
@@ -19,6 +19,14 @@ namespace Apache.Arrow
 {
     public class UInt8Array : PrimitiveArray<byte>
     {
+        public class Builder : PrimitiveArrayBuilder<byte, UInt8Array, Builder>
+        {
+            protected override UInt8Array Build(
+                ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
+                int length, int nullCount, int offset) =>
+                new UInt8Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
+        }
+
         public UInt8Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
diff --git a/csharp/src/Apache.Arrow/ArrowBuffer.Builder.cs b/csharp/src/Apache.Arrow/ArrowBuffer.Builder.cs
index c64b7d5..699f72a 100644
--- a/csharp/src/Apache.Arrow/ArrowBuffer.Builder.cs
+++ b/csharp/src/Apache.Arrow/ArrowBuffer.Builder.cs
@@ -25,17 +25,24 @@ namespace Apache.Arrow
         public class Builder<T>
             where T : struct
         {
+            private const int DefaultCapacity = 8;
+
             private readonly int _size;
-            private byte[] _buffer;
 
-            public int Capacity => _buffer.Length / _size;
+            public int Capacity => Memory.Length / _size;
             public int Length { get; private set; }
+            public Memory<byte> Memory { get; private set; }
+            public Span<T> Span
+            {
+                [MethodImpl(MethodImplOptions.AggressiveInlining)]
+                get => Memory.Span.CastTo<T>();
+            }
 
-            public Builder(int capacity = 8)
+            public Builder(int capacity = DefaultCapacity)
             {
                 _size = Unsafe.SizeOf<T>();
-                _buffer = new byte[capacity * _size];
 
+                Memory = new byte[capacity * _size];
                 Length = 0;
             }
 
@@ -47,29 +54,19 @@ namespace Apache.Arrow
 
             public Builder<T> Append(T value)
             {
-                var span = EnsureCapacity(1);
-                span[Length++] = value;
+                EnsureCapacity(1);
+                Span[Length++] = value;
                 return this;
             }
 
             public Builder<T> Append(ReadOnlySpan<T> source)
             {
-                var span = EnsureCapacity(source.Length);
-                source.CopyTo(span.Slice(Length, source.Length));
+                EnsureCapacity(source.Length);
+                source.CopyTo(Span.Slice(Length, source.Length));
                 Length += source.Length;
                 return this;
             }
 
-            public Builder<T> Append(Func<IEnumerable<T>> fn)
-            {
-                if (fn != null)
-                {
-                    AppendRange(fn());
-                }
-
-                return this;
-            }
-
             public Builder<T> AppendRange(IEnumerable<T> values)
             {
                 if (values != null)
@@ -91,13 +88,8 @@ namespace Apache.Arrow
 
             public Builder<T> Resize(int capacity)
             {
-                if (capacity < 0)
-                {
-                    throw new ArgumentOutOfRangeException(nameof(capacity));
-                }
-
-                Reallocate(capacity);
-                Length = Math.Min(Length, capacity);
+                EnsureCapacity(capacity);
+                Length = Math.Max(0, capacity);
 
                 return this;
             }
@@ -125,21 +117,17 @@ namespace Apache.Arrow
                 return new ArrowBuffer(memoryOwner);
             }
 
-            private Span<T> EnsureCapacity(int len)
+            private void EnsureCapacity(int n)
             {
-                var targetCapacity = Length + len;
+                var length = checked(Length + n);
 
-                if (targetCapacity > Capacity)
+                if (length > Capacity)
                 {
                     // TODO: specifiable growth strategy
 
-                    var capacity = Math.Max(
-                        targetCapacity * _size, _buffer.Length * 2);
-
+                    var capacity = Math.Max(length * _size, Memory.Length * 2);
                     Reallocate(capacity);
                 }
-
-                return Span;
             }
 
             private void Reallocate(int length)
@@ -151,17 +139,13 @@ namespace Apache.Arrow
 
                 if (length != 0)
                 {
-                    System.Array.Resize(ref _buffer, length);
+                    var memory = new Memory<byte>(new byte[length]);
+                    Memory.CopyTo(memory);
+
+                    Memory = memory;
                 }
             }
 
-            private Memory<byte> Memory => _buffer;
-
-            private Span<T> Span
-            {
-                [MethodImpl(MethodImplOptions.AggressiveInlining)]
-                get => Memory.Span.CastTo<T>();
-            }
         }
 
     }
diff --git a/csharp/src/Apache.Arrow/BitUtility.cs b/csharp/src/Apache.Arrow/BitUtility.cs
index efb426c..a5da46b 100644
--- a/csharp/src/Apache.Arrow/BitUtility.cs
+++ b/csharp/src/Apache.Arrow/BitUtility.cs
@@ -53,6 +53,20 @@ namespace Apache.Arrow
             data[index / 8] |= BitMask[index % 8];
         }
 
+        public static void SetBit(Span<byte> data, int index, bool value)
+        {
+            var idx = index / 8;
+            var mod = index % 8;
+            data[idx] = value
+                ? (byte)(data[idx] | BitMask[mod])
+                : (byte)(data[idx] & ~BitMask[mod]);
+        }
+
+        public static void ToggleBit(Span<byte> data, int index)
+        {
+            data[index / 8] ^= BitMask[index % 8];
+        }
+
         /// <summary>
         /// Counts the number of set bits in a span of bytes starting
         /// at a specific bit offset.
@@ -117,6 +131,17 @@ namespace Apache.Arrow
             Debug.Assert(factor > 0 && (factor & (factor - 1)) == 0);
             return (n + (factor - 1)) & ~(factor - 1);
         }
+
+        /// <summary>
+        /// Calculates the number of bytes required to store n bits.
+        /// </summary>
+        /// <param name="n">number of bits</param>
+        /// <returns>number of bytes</returns>
+        public static int ByteCount(int n)
+        {
+            Debug.Assert(n >= 0);
+            return n / 8 + (n % 8 != 0 ? 1 : 0); // ceil(n / 8)
+        }
             
         internal static int ReadInt32(ReadOnlyMemory<byte> value)
         {
diff --git a/csharp/src/Apache.Arrow/Interfaces/IArrowArrayBuilder.cs b/csharp/src/Apache.Arrow/Interfaces/IArrowArrayBuilder.cs
new file mode 100644
index 0000000..bd928b1
--- /dev/null
+++ b/csharp/src/Apache.Arrow/Interfaces/IArrowArrayBuilder.cs
@@ -0,0 +1,46 @@
+// 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
+//
+//     http://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 Apache.Arrow.Memory;
+using System;
+using System.Collections.Generic;
+
+namespace Apache.Arrow
+{
+    public interface IArrowArrayBuilder { }
+
+    public interface IArrowArrayBuilder<out TArray> : IArrowArrayBuilder
+        where TArray: IArrowArray
+    {
+        TArray Build(MemoryAllocator allocator);
+    }
+
+    public interface IArrowArrayBuilder<T, out TArray> : IArrowArrayBuilder<TArray>
+        where TArray: IArrowArray { }
+
+    public interface IArrowArrayBuilder<T, out TArray, out TBuilder> : IArrowArrayBuilder<T, TArray>
+        where TArray : IArrowArray
+        where TBuilder : IArrowArrayBuilder<TArray>
+    {
+        TBuilder Append(T value);
+        TBuilder Append(ReadOnlySpan<T> span);
+        TBuilder AppendRange(IEnumerable<T> values);
+        TBuilder Reserve(int capacity);
+        TBuilder Resize(int length);
+        TBuilder Swap(int i, int j);
+        TBuilder Set(int index, T value);
+        TBuilder Clear();
+    }
+}
\ No newline at end of file
diff --git a/csharp/src/Apache.Arrow/Ipc/ArrowStreamWriter.cs b/csharp/src/Apache.Arrow/Ipc/ArrowStreamWriter.cs
index f3d0840..7696dfa 100644
--- a/csharp/src/Apache.Arrow/Ipc/ArrowStreamWriter.cs
+++ b/csharp/src/Apache.Arrow/Ipc/ArrowStreamWriter.cs
@@ -83,8 +83,12 @@ namespace Apache.Arrow.Ipc
             public void Visit(DoubleArray array) => CreateBuffers(array);
             public void Visit(TimestampArray array) => CreateBuffers(array);
             public void Visit(BooleanArray array) => CreateBuffers(array);
-            public void Visit(Date32Array array) => CreateBuffers(array);
-            public void Visit(Date64Array array) => CreateBuffers(array);
+            public void Visit(Date32Array array)
+            {
+                _buffers.Add(CreateBuffer(array.NullBitmapBuffer));
+                _buffers.Add(CreateBuffer(array.ValueBuffer));
+            }
+            public void Visit(Date64Array array) => throw new NotSupportedException();
 
             public void Visit(ListArray array)
             {
@@ -103,6 +107,12 @@ namespace Apache.Arrow.Ipc
                 _buffers.Add(CreateBuffer(array.ValueBuffer));
             }
 
+            private void CreateBuffers(BooleanArray array)
+            {
+                _buffers.Add(CreateBuffer(ArrowBuffer.Empty));
+                _buffers.Add(CreateBuffer(array.ValueBuffer));
+            }
+
             private void CreateBuffers<T>(PrimitiveArray<T> array)
                 where T: struct
             {
diff --git a/csharp/src/Apache.Arrow/RecordBatch.Builder.cs b/csharp/src/Apache.Arrow/RecordBatch.Builder.cs
new file mode 100644
index 0000000..3ede0b4
--- /dev/null
+++ b/csharp/src/Apache.Arrow/RecordBatch.Builder.cs
@@ -0,0 +1,155 @@
+// 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
+//
+//     http://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 Apache.Arrow.Memory;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Apache.Arrow
+{
+    public partial class RecordBatch
+    {
+        public class ArrayBuilder
+        {
+            private readonly MemoryAllocator _allocator;
+
+            internal ArrayBuilder(MemoryAllocator allocator)
+            {
+                _allocator = allocator;
+            }
+
+            public BooleanArray Boolean(Action<BooleanArray.Builder> action) => Build<BooleanArray, BooleanArray.Builder>(action);
+            public Int8Array Int8(Action<Int8Array.Builder> action) => Build<Int8Array, Int8Array.Builder>(action);
+            public Int16Array Int16(Action<Int16Array.Builder> action) => Build<Int16Array, Int16Array.Builder>(action);
+            public Int32Array Int32(Action<Int32Array.Builder> action) => Build<Int32Array, Int32Array.Builder>(action);
+            public Int64Array Int64(Action<Int64Array.Builder> action) => Build<Int64Array, Int64Array.Builder>(action);
+            public UInt8Array UInt8(Action<UInt8Array.Builder> action) => Build<UInt8Array, UInt8Array.Builder>(action);
+            public UInt16Array UInt16(Action<UInt16Array.Builder> action) => Build<UInt16Array, UInt16Array.Builder>(action);
+            public UInt32Array UInt32(Action<UInt32Array.Builder> action) => Build<UInt32Array, UInt32Array.Builder>(action);
+            public UInt64Array UInt64(Action<UInt64Array.Builder> action) => Build<UInt64Array, UInt64Array.Builder>(action);
+            public FloatArray Float(Action<FloatArray.Builder> action) => Build<FloatArray, FloatArray.Builder>(action);
+            public DoubleArray Double(Action<DoubleArray.Builder> action) => Build<DoubleArray, DoubleArray.Builder>(action);
+            public Date32Array Date32(Action<Date32Array.Builder> action) => Build<Date32Array, Date32Array.Builder>(action);
+            public Date64Array Date64(Action<Date64Array.Builder> action) => Build<Date64Array, Date64Array.Builder>(action);
+            public BinaryArray Binary(Action<BinaryArray.Builder> action) => Build<BinaryArray, BinaryArray.Builder>(action);
+            public StringArray String(Action<StringArray.Builder> action) => Build<StringArray, StringArray.Builder>(action);
+
+            private TArray Build<TArray, TArrayBuilder>(Action<TArrayBuilder> action)
+                where TArray: IArrowArray
+                where TArrayBuilder: IArrowArrayBuilder<TArray>, new()
+            {
+                if (action == null)
+                {
+                    return default;
+                }
+
+                var builder = new TArrayBuilder();
+                action(builder);
+
+                return builder.Build(_allocator);
+            }
+        }
+
+        public class Builder
+        {
+            private readonly MemoryAllocator _allocator;
+            private readonly ArrayBuilder _arrayBuilder;
+            private readonly Schema.Builder _schemaBuilder;
+            private readonly List<IArrowArray> _arrays;
+
+            public Builder(MemoryAllocator allocator = default)
+            {
+                _allocator = allocator ?? MemoryAllocator.Default.Value;
+                _arrayBuilder = new ArrayBuilder(_allocator);
+                _schemaBuilder = new Schema.Builder();
+                _arrays = new List<IArrowArray>();
+            }
+
+            public RecordBatch Build()
+            {
+                var schema = _schemaBuilder.Build();
+                var length = _arrays.Max(x => x.Length);
+
+                // each array has its own memoryOwner, so the RecordBatch itself doesn't
+                // have a memoryOwner
+                IMemoryOwner<byte> memoryOwner = null;
+                var batch = new RecordBatch(schema, memoryOwner, _arrays, length);
+
+                return batch;
+            }
+
+            public Builder Clear()
+            {
+                _schemaBuilder.Clear();
+                _arrays.Clear();
+                return this;
+            }
+
+            public Builder Append(RecordBatch batch)
+            {
+                foreach (var field in batch.Schema.Fields)
+                {
+                    _schemaBuilder.Field(field.Value);
+                }
+
+                foreach (var array in batch.Arrays)
+                {
+                    _arrays.Add(array);
+                }
+
+                return this;
+            }
+
+            public Builder Append<TArray>(string name, bool nullable, IArrowArrayBuilder<TArray> builder)
+                where TArray: IArrowArray
+            {
+                return builder == null 
+                    ? this 
+                    : Append(name, nullable, builder.Build(_allocator));
+            }
+
+            public Builder Append<TArray>(string name, bool nullable, TArray array)
+                where TArray: IArrowArray
+            {
+                if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
+                if (array == null) return this;
+
+                _arrays.Add(array);
+
+                _schemaBuilder.Field(f => f
+                    .Name(name)
+                    .Nullable(nullable)
+                    .DataType(array.Data.DataType));
+
+                return this;
+            }
+
+            public Builder Append<TArray>(string name, bool nullable, Func<ArrayBuilder, TArray> action)
+                where TArray: IArrowArray
+            {
+                if (action == null) return this;
+
+                var array = action(_arrayBuilder);
+
+                Append(name, nullable, array);
+
+                return this;
+            }
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/csharp/src/Apache.Arrow/RecordBatch.cs b/csharp/src/Apache.Arrow/RecordBatch.cs
index 822cbac..9fe4080 100644
--- a/csharp/src/Apache.Arrow/RecordBatch.cs
+++ b/csharp/src/Apache.Arrow/RecordBatch.cs
@@ -21,7 +21,7 @@ using System.Linq;
 
 namespace Apache.Arrow
 {
-    public class RecordBatch : IDisposable
+    public partial class RecordBatch : IDisposable
     {
         public Schema Schema { get; }
         public int ColumnCount => _arrays.Count;
diff --git a/csharp/src/Apache.Arrow/Schema.Builder.cs b/csharp/src/Apache.Arrow/Schema.Builder.cs
index 560958c..fd14d7a 100644
--- a/csharp/src/Apache.Arrow/Schema.Builder.cs
+++ b/csharp/src/Apache.Arrow/Schema.Builder.cs
@@ -32,6 +32,13 @@ namespace Apache.Arrow
                 _metadata = new Dictionary<string, string>();
             }
 
+            public Builder Clear()
+            {
+                _fields.Clear();
+                _metadata.Clear();
+                return this;
+            }
+
             public Builder Field(Field field)
             {
                 if (field == null) return this;
diff --git a/csharp/test/Apache.Arrow.Tests/ArrayBuilderTests.cs b/csharp/test/Apache.Arrow.Tests/ArrayBuilderTests.cs
new file mode 100644
index 0000000..3d5cc7f
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Tests/ArrayBuilderTests.cs
@@ -0,0 +1,72 @@
+// 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
+//
+//     http://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 Apache.Arrow.Types;
+using System;
+using Xunit;
+
+namespace Apache.Arrow.Tests
+{
+    public class ArrayBuilderTests
+    {
+        // TODO: Test various builder invariants (Append, AppendRange, Clear, Resize, Reserve, etc)
+
+        [Fact]
+        public void PrimitiveArrayBuildersProduceExpectedArray()
+        {
+            TestArrayBuilder<Int8Array, Int8Array.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<Int16Array, Int16Array.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<Int32Array, Int32Array.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<Int64Array, Int64Array.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<UInt8Array, UInt8Array.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<UInt16Array, UInt16Array.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<UInt32Array, UInt32Array.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<UInt64Array, UInt64Array.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<FloatArray, FloatArray.Builder>(x => x.Append(10).Append(20).Append(30));
+            TestArrayBuilder<DoubleArray, DoubleArray.Builder>(x => x.Append(10).Append(20).Append(30));
+        }
+
+        public class TimestampArrayBuilder
+        {
+            [Fact]
+            public void ProducesExpectedArray()
+            {
+                var now = DateTimeOffset.UtcNow.ToLocalTime();
+                var array = new TimestampArray.Builder(TimeUnit.Nanosecond, TimeZoneInfo.Local.Id)
+                    .Append(now)
+                    .Build();
+
+                Assert.Equal(1, array.Length);
+                Assert.NotNull(array.GetTimestamp(0));
+                Assert.Equal(now.Truncate(TimeSpan.FromTicks(100)), array.GetTimestamp(0).Value);
+            }
+        }
+
+        private static void TestArrayBuilder<TArray, TArrayBuilder>(Action<TArrayBuilder> action)
+            where TArray: IArrowArray
+            where TArrayBuilder: IArrowArrayBuilder<TArray>, new()
+        {
+            var builder = new TArrayBuilder();
+            action(builder);
+            var array = builder.Build(default);
+
+            Assert.IsAssignableFrom<TArray>(array);
+            Assert.NotNull(array);
+            Assert.Equal(3, array.Length);
+            Assert.Equal(0, array.NullCount);
+        }
+        
+    }
+}
diff --git a/csharp/test/Apache.Arrow.Tests/ArrowBufferBuilderTests.cs b/csharp/test/Apache.Arrow.Tests/ArrowBufferBuilderTests.cs
index 144baa9..bd5550b 100644
--- a/csharp/test/Apache.Arrow.Tests/ArrowBufferBuilderTests.cs
+++ b/csharp/test/Apache.Arrow.Tests/ArrowBufferBuilderTests.cs
@@ -14,7 +14,6 @@
 // limitations under the License.
 
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using Xunit;
 
@@ -22,16 +21,24 @@ namespace Apache.Arrow.Tests
 {
     public class ArrowBufferBuilderTests
     {
-        public class Append
+        [Fact]
+        public void ThrowsWhenIndexOutOfBounds()
         {
+            Assert.Throws<IndexOutOfRangeException>(() =>
+            {
+                var builder = new ArrowBuffer.Builder<int>();
+                builder.Span[100] = 100;
+            });
+        }
 
+        public class Append
+        {
             [Fact]
             public void DoesNotThrowWithNullParameters()
             {
                 var builder = new ArrowBuffer.Builder<int>();
 
                 builder.AppendRange(null);
-                builder.Append((Func<IEnumerable<int>>) null);
             }
 
             [Fact]
@@ -179,5 +186,30 @@ namespace Apache.Arrow.Tests
                 Assert.True(zeros.SequenceEqual(values));
             }
         }
+
+        public class Resize
+        {
+            [Fact]
+            public void LengthHasExpectedValueAfterResize()
+            {
+                var builder = new ArrowBuffer.Builder<int>();
+                builder.Resize(8);
+
+                Assert.True(builder.Capacity >= 8);
+                Assert.Equal(8, builder.Length);
+            }
+
+            [Fact]
+            public void NegativeLengthResizesToZero()
+            {
+                var builder = new ArrowBuffer.Builder<int>();
+                builder.Append(10);
+                builder.Append(20);
+                builder.Resize(-1);
+
+                Assert.Equal(0, builder.Length);
+            }
+        }
+
     }
 }
diff --git a/csharp/test/Apache.Arrow.Tests/ArrowReaderVerifier.cs b/csharp/test/Apache.Arrow.Tests/ArrowReaderVerifier.cs
index d7f0c26..87da093 100644
--- a/csharp/test/Apache.Arrow.Tests/ArrowReaderVerifier.cs
+++ b/csharp/test/Apache.Arrow.Tests/ArrowReaderVerifier.cs
@@ -116,6 +116,20 @@ namespace Apache.Arrow.Tests
                 Assert.True(expectedArray.NullBitmapBuffer.Span.SequenceEqual(actualArray.NullBitmapBuffer.Span));
                 Assert.True(expectedArray.Values.Slice(0, expectedArray.Length).SequenceEqual(actualArray.Values.Slice(0, actualArray.Length)));
             }
+
+            private void CompareArrays(BooleanArray actualArray)
+            {
+                Assert.IsAssignableFrom<BooleanArray>(_expectedArray);
+                BooleanArray expectedArray = (BooleanArray)_expectedArray;
+
+                Assert.Equal(expectedArray.Length, actualArray.Length);
+                Assert.Equal(expectedArray.NullCount, actualArray.NullCount);
+                Assert.Equal(expectedArray.Offset, actualArray.Offset);
+
+                Assert.True(expectedArray.NullBitmapBuffer.Span.SequenceEqual(actualArray.NullBitmapBuffer.Span));
+                int booleanByteCount = BitUtility.ByteCount(expectedArray.Length);
+                Assert.True(expectedArray.Values.Slice(0, booleanByteCount).SequenceEqual(actualArray.Values.Slice(0, booleanByteCount)));
+            }
         }
     }
 }
diff --git a/csharp/test/Apache.Arrow.Tests/BitUtilityTests.cs b/csharp/test/Apache.Arrow.Tests/BitUtilityTests.cs
index b59bc4d..e4933fa 100644
--- a/csharp/test/Apache.Arrow.Tests/BitUtilityTests.cs
+++ b/csharp/test/Apache.Arrow.Tests/BitUtilityTests.cs
@@ -20,6 +20,21 @@ namespace Apache.Arrow.Tests
 {
     public class BitUtilityTests
     {
+        public class ByteCount
+        {
+            [Theory]
+            [InlineData(0, 0)]
+            [InlineData(1, 1)]
+            [InlineData(8, 1)]
+            [InlineData(9, 2)]
+            [InlineData(32, 4)]
+            public void HasExpectedResult(int n, int expected)
+            {
+                var count = BitUtility.ByteCount(n);
+                Assert.Equal(expected, count);
+            }
+        }
+
         public class CountBits
         {
             [Theory]
diff --git a/csharp/test/Apache.Arrow.Tests/BooleanArrayTests.cs b/csharp/test/Apache.Arrow.Tests/BooleanArrayTests.cs
new file mode 100644
index 0000000..df6eff1
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Tests/BooleanArrayTests.cs
@@ -0,0 +1,211 @@
+// 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
+//
+//     http://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 Xunit;
+
+namespace Apache.Arrow.Tests
+{
+    public class BooleanArrayTests
+    {
+        public class Builder
+        {
+            public class Append
+            {
+                [Theory]
+                [InlineData(1)]
+                [InlineData(3)]
+                public void IncrementsLength(int count)
+                {
+                    var builder = new BooleanArray.Builder();
+
+                    for (var i = 0; i < count; i++)
+                    {
+                        builder.Append(true);
+                    }
+
+                    var array = builder.Build();
+
+                    Assert.Equal(count, array.Length);
+                }
+
+                [Fact]
+                public void AppendsExpectedBit()
+                {
+                    var array1 = new BooleanArray.Builder()
+                        .Append(false)
+                        .Build();
+
+                    Assert.False(array1.GetBoolean(0));
+
+                    var array2 = new BooleanArray.Builder()
+                        .Append(true)
+                        .Build();
+
+                    Assert.True(array2.GetBoolean(0));
+                }
+            }
+
+            public class Clear
+            {
+                [Fact]
+                public void SetsAllBitsToDefault()
+                {
+                    var array = new BooleanArray.Builder()
+                        .Resize(8)
+                        .Set(0, true)
+                        .Set(7, true)
+                        .Clear()
+                        .Build();
+
+                    for (var i = 0; i < array.Length; i++)
+                    {
+                        Assert.False(array.GetBoolean(i));
+                    }
+                }
+            }
+
+            public class Toggle
+            {
+                [Theory]
+                [InlineData(8, 1)]
+                [InlineData(16, 13)]
+                public void TogglesExpectedBitToFalse(int length, int index)
+                {
+                    var array = new BooleanArray.Builder()
+                        .Resize(length)
+                        .Set(index, true)
+                        .Toggle(index)
+                        .Build();
+
+                    Assert.False(array.GetBoolean(index));
+                }
+
+                [Theory]
+                [InlineData(8, 1)]
+                [InlineData(16, 13)]
+                public void TogglesExpectedBitToTreu(int length, int index)
+                {
+                    var array = new BooleanArray.Builder()
+                        .Resize(length)
+                        .Set(index, false)
+                        .Toggle(index)
+                        .Build();
+
+                    Assert.True(array.GetBoolean(index));
+                }
+
+                [Fact]
+                public void ThrowsWhenIndexOutOfRange()
+                {
+                    Assert.Throws<ArgumentOutOfRangeException>(() =>
+                    {
+                        var builder = new BooleanArray.Builder();
+                        builder.Toggle(8);
+                    });
+                }
+            }
+
+            public class Swap
+            {
+                [Fact]
+                public void SwapsExpectedBits()
+                {
+                    var array = new BooleanArray.Builder()
+                        .Resize(8)
+                        .Set(0, true)
+                        .Swap(0, 7)
+                        .Build();
+
+                    Assert.False(array.GetBoolean(0));
+                    Assert.True(array.GetBoolean(7));
+                }
+
+                [Fact]
+                public void ThrowsWhenIndexOutOfRange()
+                {
+                    Assert.Throws<ArgumentOutOfRangeException>(() =>
+                    {
+                        var builder = new BooleanArray.Builder();
+                        builder.Swap(0, 1);
+                    });
+                }
+            }
+
+            public class Set
+            {
+                [Theory]
+                [InlineData(8, 0)]
+                [InlineData(8, 4)]
+                [InlineData(8, 7)]
+                [InlineData(16, 8)]
+                [InlineData(16, 15)]
+                public void SetsExpectedBitToTrue(int length, int index)
+                {
+                    var array = new BooleanArray.Builder()
+                        .Resize(length)
+                        .Set(index, true)
+                        .Build();
+
+                    Assert.True(array.GetBoolean(index));
+                }
+
+                [Theory]
+                [InlineData(8, 0)]
+                [InlineData(8, 4)]
+                [InlineData(8, 7)]
+                [InlineData(16, 8)]
+                [InlineData(16, 15)]
+                public void SetsExpectedBitsToFalse(int length, int index)
+                {
+                    var array = new BooleanArray.Builder()
+                        .Resize(length)
+                        .Set(index, false)
+                        .Build();
+
+                    Assert.False(array.GetBoolean(index));
+                }
+
+                [Theory]
+                [InlineData(4)]
+                public void UnsetBitsAreUnchanged(int index)
+                {
+                    var array = new BooleanArray.Builder()
+                        .Resize(8)
+                        .Set(index, true)
+                        .Build();
+
+                    for (var i = 0; i < 8; i++)
+                    {
+                        if (i != index)
+                        {
+                            Assert.False(array.GetBoolean(i));
+                        }
+                    }
+                }
+
+                [Fact]
+                public void ThrowsWhenIndexOutOfRange()
+                {
+                    Assert.Throws<ArgumentOutOfRangeException>(() =>
+                    {
+                        var builder = new BooleanArray.Builder();
+                        builder.Set(builder.Length, false);
+                    });
+                }
+            }
+        }
+    }
+}
diff --git a/csharp/src/Apache.Arrow/Arrays/DoubleArray.cs b/csharp/test/Apache.Arrow.Tests/Extensions/DateTimeOffsetExtensions.cs
similarity index 57%
copy from csharp/src/Apache.Arrow/Arrays/DoubleArray.cs
copy to csharp/test/Apache.Arrow.Tests/Extensions/DateTimeOffsetExtensions.cs
index 0c50e5a..4375c39 100644
--- a/csharp/src/Apache.Arrow/Arrays/DoubleArray.cs
+++ b/csharp/test/Apache.Arrow.Tests/Extensions/DateTimeOffsetExtensions.cs
@@ -13,25 +13,28 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-using Apache.Arrow.Types;
+using System;
+using System.Collections.Generic;
+using System.Text;
 
-namespace Apache.Arrow
+namespace Apache.Arrow.Tests
 {
-    public class DoubleArray : PrimitiveArray<double>
+    public static class DateTimeOffsetExtensions
     {
-        public DoubleArray(
-            ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
-            int length, int nullCount, int offset)
-            : this(new ArrayData(DoubleType.Default, length, nullCount, offset,
-                new[] { nullBitmapBuffer, valueBuffer }))
-        { }
-
-        public DoubleArray(ArrayData data)
-            : base(data)
+        public static DateTimeOffset Truncate(this DateTimeOffset dateTimeOffset, TimeSpan offset)
         {
-            data.EnsureDataType(ArrowTypeId.Double);
-        }
+            if (offset == TimeSpan.Zero)
+            {
+                return dateTimeOffset;
+            }
 
-        public override void Accept(IArrowArrayVisitor visitor) => Accept(this, visitor);
+            if (dateTimeOffset == DateTimeOffset.MinValue || dateTimeOffset == DateTimeOffset.MaxValue)
+            {
+                return dateTimeOffset;
+            }
+
+            return dateTimeOffset.AddTicks(-(dateTimeOffset.Ticks % offset.Ticks));
+        }
+            
     }
 }
diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt
index c304a39..25b1ecd 100644
--- a/dev/release/rat_exclude_files.txt
+++ b/dev/release/rat_exclude_files.txt
@@ -185,6 +185,8 @@ csharp/.gitattributes
 csharp/src/Apache.Arrow/Flatbuf/*
 csharp/Apache.Arrow.sln
 csharp/Directory.Build.props
+csharp/examples/FluentBuilderExample/FluentBuilderExample.csproj
+csharp/examples/Examples.sln
 csharp/src/Apache.Arrow/Apache.Arrow.csproj
 csharp/src/Apache.Arrow/Properties/Resources.Designer.cs
 csharp/src/Apache.Arrow/Properties/Resources.resx