Ported FieldMaskUtil from Java to C# (#5045)

* Ported FieldMaskUtil from Java to C#

* Merged FieldMaskUtil into FieldMaskPartial

- Removed FieldMaskUtil
- Moved FieldMaskTree to root
- Updated tests

* Improved tests

- Removed internal method FieldMaskTree.GetFieldPaths
- Proof FieldMask.Paths only contains expected values

* Added FieldMaskTreeTest to Makefile

* Added FieldMaskTree to Makefile
diff --git a/Makefile.am b/Makefile.am
index 6bafd10..0c86b93 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -93,6 +93,7 @@
   csharp/src/Google.Protobuf.Test/DeprecatedMemberTest.cs                    \
   csharp/src/Google.Protobuf.Test/EqualityTester.cs                          \
   csharp/src/Google.Protobuf.Test/FieldCodecTest.cs                          \
+  csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs                       \
   csharp/src/Google.Protobuf.Test/GeneratedMessageTest.cs                    \
   csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj                \
   csharp/src/Google.Protobuf.Test/IssuesTest.cs                              \
@@ -140,6 +141,7 @@
   csharp/src/Google.Protobuf/Compatibility/StreamExtensions.cs               \
   csharp/src/Google.Protobuf/Compatibility/TypeExtensions.cs                 \
   csharp/src/Google.Protobuf/FieldCodec.cs                                   \
+  csharp/src/Google.Protobuf/FieldMaskTree.cs                                \
   csharp/src/Google.Protobuf/FrameworkPortability.cs                         \
   csharp/src/Google.Protobuf/Google.Protobuf.csproj                          \
   csharp/src/Google.Protobuf/ICustomDiagnosticMessage.cs                     \
diff --git a/csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs b/csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs
new file mode 100644
index 0000000..b0caab9
--- /dev/null
+++ b/csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs
@@ -0,0 +1,436 @@
+#region Copyright notice and license
+// Protocol Buffers - Google's data interchange format
+// Copyright 2015 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#endregion
+
+using System.Collections.Generic;
+using Google.Protobuf.Collections;
+using Google.Protobuf.TestProtos;
+using NUnit.Framework;
+using Google.Protobuf.WellKnownTypes;
+
+namespace Google.Protobuf
+{
+    public class FieldMaskTreeTest
+    {
+        [Test]
+        public void AddFieldPath()
+        {
+            FieldMaskTree tree = new FieldMaskTree();
+            RepeatedField<string> paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(0, paths.Count);
+
+            tree.AddFieldPath("");
+            paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(1, paths.Count);
+            Assert.Contains("", paths);
+
+            // New branch.
+            tree.AddFieldPath("foo");
+            paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(2, paths.Count);
+            Assert.Contains("foo", paths);
+
+            // Redundant path.
+            tree.AddFieldPath("foo");
+            paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(2, paths.Count);
+
+            // New branch.
+            tree.AddFieldPath("bar.baz");
+            paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(3, paths.Count);
+            Assert.Contains("bar.baz", paths);
+            
+            // Redundant sub-path.
+            tree.AddFieldPath("foo.bar");
+            paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(3, paths.Count);
+
+            // New branch from a non-root node.
+            tree.AddFieldPath("bar.quz");
+            paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(4, paths.Count);
+            Assert.Contains("bar.quz", paths);
+            
+            // A path that matches several existing sub-paths.
+            tree.AddFieldPath("bar");
+            paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(3, paths.Count);
+            Assert.Contains("foo", paths);
+            Assert.Contains("bar", paths);
+        }
+
+        [Test]
+        public void MergeFromFieldMask()
+        {
+            FieldMaskTree tree = new FieldMaskTree();
+            tree.MergeFromFieldMask(new FieldMask
+            {
+                Paths = {"foo", "bar.baz", "bar.quz"}
+            });
+            RepeatedField<string> paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(3, paths.Count);
+            Assert.Contains("foo", paths);
+            Assert.Contains("bar.baz", paths);
+            Assert.Contains("bar.quz", paths);
+
+            tree.MergeFromFieldMask(new FieldMask
+            {
+                Paths = {"foo.bar", "bar"}
+            });
+            paths = tree.ToFieldMask().Paths;
+            Assert.AreEqual(2, paths.Count);
+            Assert.Contains("foo", paths);
+            Assert.Contains("bar", paths);
+        }
+
+        [Test]
+        public void IntersectFieldPath()
+        {
+            FieldMaskTree tree = new FieldMaskTree();
+            FieldMaskTree result = new FieldMaskTree();
+            tree.MergeFromFieldMask(new FieldMask
+            {
+                Paths = {"foo", "bar.baz", "bar.quz"}
+            });
+
+            // Empty path.
+            tree.IntersectFieldPath("", result);
+            RepeatedField<string> paths = result.ToFieldMask().Paths;
+            Assert.AreEqual(0, paths.Count);
+
+            // Non-exist path.
+            tree.IntersectFieldPath("quz", result);
+            paths = result.ToFieldMask().Paths;
+            Assert.AreEqual(0, paths.Count);
+
+            // Sub-path of an existing leaf.
+            tree.IntersectFieldPath("foo.bar", result);
+            paths = result.ToFieldMask().Paths;
+            Assert.AreEqual(1, paths.Count);
+            Assert.Contains("foo.bar", paths);
+
+            // Match an existing leaf node.
+            tree.IntersectFieldPath("foo", result);
+            paths = result.ToFieldMask().Paths;
+            Assert.AreEqual(1, paths.Count);
+            Assert.Contains("foo", paths);
+
+            // Non-exist path.
+            tree.IntersectFieldPath("bar.foo", result);
+            paths = result.ToFieldMask().Paths;
+            Assert.AreEqual(1, paths.Count);
+            Assert.Contains("foo", paths);
+
+            // Match a non-leaf node.
+            tree.IntersectFieldPath("bar", result);
+            paths = result.ToFieldMask().Paths;
+            Assert.AreEqual(3, paths.Count);
+            Assert.Contains("foo", paths);
+            Assert.Contains("bar.baz", paths);
+            Assert.Contains("bar.quz", paths);
+        }
+
+        private void Merge(FieldMaskTree tree, IMessage source, IMessage destination, FieldMask.MergeOptions options, bool useDynamicMessage)
+        {
+            if (useDynamicMessage)
+            {
+                var newSource = source.Descriptor.Parser.CreateTemplate();
+                newSource.MergeFrom(source.ToByteString());
+
+                var newDestination = source.Descriptor.Parser.CreateTemplate();
+                newDestination.MergeFrom(destination.ToByteString());
+
+                tree.Merge(newSource, newDestination, options);
+
+                // Clear before merging:
+                foreach (var fieldDescriptor in destination.Descriptor.Fields.InFieldNumberOrder())
+                {
+                    fieldDescriptor.Accessor.Clear(destination);
+                }
+                destination.MergeFrom(newDestination.ToByteString());
+            }
+            else
+            {
+                tree.Merge(source, destination, options);
+            }
+        }
+
+        [Test]
+        [TestCase(true)]
+        [TestCase(false)]
+        public void Merge(bool useDynamicMessage)
+        {
+            TestAllTypes value = new TestAllTypes
+            {
+                SingleInt32 = 1234,
+                SingleNestedMessage = new TestAllTypes.Types.NestedMessage {Bb = 5678},
+                RepeatedInt32 = {4321},
+                RepeatedNestedMessage = {new TestAllTypes.Types.NestedMessage {Bb = 8765}}
+            };
+
+            NestedTestAllTypes source = new NestedTestAllTypes
+            {
+                Payload = value,
+                Child = new NestedTestAllTypes {Payload = value}
+            };
+            // Now we have a message source with the following structure:
+            //   [root] -+- payload -+- single_int32
+            //           |           +- single_nested_message
+            //           |           +- repeated_int32
+            //           |           +- repeated_nested_message
+            //           |
+            //           +- child --- payload -+- single_int32
+            //                                 +- single_nested_message
+            //                                 +- repeated_int32
+            //                                 +- repeated_nested_message
+
+            FieldMask.MergeOptions options = new FieldMask.MergeOptions();
+
+            // Test merging each individual field.
+            NestedTestAllTypes destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("payload.single_int32"),
+                source, destination, options, useDynamicMessage);
+            NestedTestAllTypes expected = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    SingleInt32 = 1234
+                }
+            };
+            Assert.AreEqual(expected, destination);
+
+            destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("payload.single_nested_message"),
+                source, destination, options, useDynamicMessage);
+            expected = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    SingleNestedMessage = new TestAllTypes.Types.NestedMessage {Bb = 5678}
+                }
+            };
+            Assert.AreEqual(expected, destination);
+
+            destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("payload.repeated_int32"),
+                source, destination, options, useDynamicMessage);
+            expected = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    RepeatedInt32 = {4321}
+                }
+            };
+            Assert.AreEqual(expected, destination);
+
+            destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("payload.repeated_nested_message"),
+                source, destination, options, useDynamicMessage);
+            expected = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    RepeatedNestedMessage = {new TestAllTypes.Types.NestedMessage {Bb = 8765}}
+                }
+            };
+            Assert.AreEqual(expected, destination);
+
+            destination = new NestedTestAllTypes();
+            Merge(
+                new FieldMaskTree().AddFieldPath("child.payload.single_int32"),
+                source,
+                destination,
+                options,
+                useDynamicMessage);
+            expected = new NestedTestAllTypes
+            {
+                Child = new NestedTestAllTypes
+                {
+                    Payload = new TestAllTypes
+                    {
+                        SingleInt32 = 1234
+                    }
+                }
+            };
+            Assert.AreEqual(expected, destination);
+
+            destination = new NestedTestAllTypes();
+            Merge(
+                new FieldMaskTree().AddFieldPath("child.payload.single_nested_message"),
+                source,
+                destination,
+                options,
+                useDynamicMessage);
+            expected = new NestedTestAllTypes
+            {
+                Child = new NestedTestAllTypes
+                {
+                    Payload = new TestAllTypes
+                    {
+                        SingleNestedMessage = new TestAllTypes.Types.NestedMessage {Bb = 5678}
+                    }
+                }
+            };
+            Assert.AreEqual(expected, destination);
+
+            destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("child.payload.repeated_int32"),
+                source, destination, options, useDynamicMessage);
+            expected = new NestedTestAllTypes
+            {
+                Child = new NestedTestAllTypes
+                {
+                    Payload = new TestAllTypes
+                    {
+                        RepeatedInt32 = {4321}
+                    }
+                }
+            };
+            Assert.AreEqual(expected, destination);
+
+            destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("child.payload.repeated_nested_message"),
+                source, destination, options, useDynamicMessage);
+            expected = new NestedTestAllTypes
+            {
+                Child = new NestedTestAllTypes
+                {
+                    Payload = new TestAllTypes
+                    {
+                        RepeatedNestedMessage = {new TestAllTypes.Types.NestedMessage {Bb = 8765}}
+                    }
+                }
+            };
+            Assert.AreEqual(expected, destination);
+
+            destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("child").AddFieldPath("payload"),
+                source, destination, options, useDynamicMessage);
+            Assert.AreEqual(source, destination);
+
+            // Test repeated options.
+            destination = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    RepeatedInt32 = { 1000 }
+                }
+            };
+            Merge(new FieldMaskTree().AddFieldPath("payload.repeated_int32"),
+                    source, destination, options, useDynamicMessage);
+            // Default behavior is to append repeated fields.
+            Assert.AreEqual(2, destination.Payload.RepeatedInt32.Count);
+            Assert.AreEqual(1000, destination.Payload.RepeatedInt32[0]);
+            Assert.AreEqual(4321, destination.Payload.RepeatedInt32[1]);
+            // Change to replace repeated fields.
+            options.ReplaceRepeatedFields = true;
+            Merge(new FieldMaskTree().AddFieldPath("payload.repeated_int32"),
+                source, destination, options, useDynamicMessage);
+            Assert.AreEqual(1, destination.Payload.RepeatedInt32.Count);
+            Assert.AreEqual(4321, destination.Payload.RepeatedInt32[0]);
+
+            // Test message options.
+            destination = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    SingleInt32 = 1000,
+                    SingleUint32 = 2000
+                }
+            };
+            Merge(new FieldMaskTree().AddFieldPath("payload"),
+                    source, destination, options, useDynamicMessage);
+            // Default behavior is to merge message fields.
+            Assert.AreEqual(1234, destination.Payload.SingleInt32);
+            Assert.AreEqual(2000, destination.Payload.SingleUint32);
+
+            // Test merging unset message fields.
+            NestedTestAllTypes clearedSource = source.Clone();
+            clearedSource.Payload = null;
+            destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("payload"),
+                clearedSource, destination, options, useDynamicMessage);
+            Assert.IsNull(destination.Payload);
+
+            // Skip a message field if they are unset in both source and target.
+            destination = new NestedTestAllTypes();
+            Merge(new FieldMaskTree().AddFieldPath("payload.single_int32"),
+                clearedSource, destination, options, useDynamicMessage);
+            Assert.IsNull(destination.Payload);
+
+            // Change to replace message fields.
+            options.ReplaceMessageFields = true;
+            destination = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    SingleInt32 = 1000,
+                    SingleUint32 = 2000
+                }
+            };
+            Merge(new FieldMaskTree().AddFieldPath("payload"),
+                    source, destination, options, useDynamicMessage);
+            Assert.AreEqual(1234, destination.Payload.SingleInt32);
+            Assert.AreEqual(0, destination.Payload.SingleUint32);
+
+            // Test merging unset message fields.
+            destination = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    SingleInt32 = 1000,
+                    SingleUint32 = 2000
+                }
+            };
+            Merge(new FieldMaskTree().AddFieldPath("payload"),
+                    clearedSource, destination, options, useDynamicMessage);
+            Assert.IsNull(destination.Payload);
+
+            // Test merging unset primitive fields.
+            destination = source.Clone();
+            destination.Payload.SingleInt32 = 0;
+            NestedTestAllTypes sourceWithPayloadInt32Unset = destination;
+            destination = source.Clone();
+            Merge(new FieldMaskTree().AddFieldPath("payload.single_int32"),
+                sourceWithPayloadInt32Unset, destination, options, useDynamicMessage);
+            Assert.AreEqual(0, destination.Payload.SingleInt32);
+
+            // Change to clear unset primitive fields.
+            options.ReplacePrimitiveFields = true;
+            destination = source.Clone();
+            Merge(new FieldMaskTree().AddFieldPath("payload.single_int32"),
+                sourceWithPayloadInt32Unset, destination, options, useDynamicMessage);
+            Assert.IsNotNull(destination.Payload);
+        }
+
+    }
+}
diff --git a/csharp/src/Google.Protobuf.Test/WellKnownTypes/FieldMaskTest.cs b/csharp/src/Google.Protobuf.Test/WellKnownTypes/FieldMaskTest.cs
index 1d9908b..5dc5035 100644
--- a/csharp/src/Google.Protobuf.Test/WellKnownTypes/FieldMaskTest.cs
+++ b/csharp/src/Google.Protobuf.Test/WellKnownTypes/FieldMaskTest.cs
@@ -30,7 +30,8 @@
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #endregion
 
-
+using System;
+using Google.Protobuf.TestProtos;
 using NUnit.Framework;
 
 namespace Google.Protobuf.WellKnownTypes
@@ -58,5 +59,187 @@
                 "{ \"@warning\": \"Invalid FieldMask\", \"paths\": [ \"x\", \"foo__bar\", \"x\\\\y\" ] }",
                 mask.ToString());
         }
+
+        [Test]
+        public void IsValid()
+        {
+            Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload"));
+            Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>("nonexist"));
+            Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.single_int32"));
+            Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.repeated_int32"));
+            Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.single_nested_message"));
+            Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.repeated_nested_message"));
+            Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>("payload.nonexist"));
+
+            Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>(FieldMask.FromString("payload")));
+            Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>(FieldMask.FromString("nonexist")));
+            Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>(FieldMask.FromString("payload,nonexist")));
+
+            Assert.IsTrue(FieldMask.IsValid(NestedTestAllTypes.Descriptor, "payload"));
+            Assert.IsFalse(FieldMask.IsValid(NestedTestAllTypes.Descriptor, "nonexist"));
+
+            Assert.IsTrue(FieldMask.IsValid(NestedTestAllTypes.Descriptor, FieldMask.FromString("payload")));
+            Assert.IsFalse(FieldMask.IsValid(NestedTestAllTypes.Descriptor, FieldMask.FromString("nonexist")));
+
+            Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.single_nested_message.bb"));
+
+            // Repeated fields cannot have sub-paths.
+            Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>("payload.repeated_nested_message.bb"));
+
+            // Non-message fields cannot have sub-paths.
+            Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>("payload.single_int32.bb"));
+        }
+
+        [Test]
+        [TestCase(new string[] { }, "\"\"")]
+        [TestCase(new string[] { "foo" }, "\"foo\"")]
+        [TestCase(new string[] { "foo", "bar" }, "\"foo,bar\"")]
+        [TestCase(new string[] { "", "foo", "", "bar", "" }, "\",foo,,bar,\"")]
+        public void ToString(string[] input, string expectedOutput)
+        {
+            FieldMask mask = new FieldMask();
+            mask.Paths.AddRange(input);
+            Assert.AreEqual(expectedOutput, mask.ToString());
+        }
+
+        [Test]
+        [TestCase("", new string[] { })]
+        [TestCase("foo", new string[] { "foo" })]
+        [TestCase("foo,bar.baz", new string[] { "foo", "bar.baz" })]
+        [TestCase(",foo,,bar,", new string[] { "foo", "bar" })]
+        public void FromString(string input, string[] expectedOutput)
+        {
+            FieldMask mask = FieldMask.FromString(input);
+            Assert.AreEqual(expectedOutput.Length, mask.Paths.Count);
+            for (int i = 0; i < expectedOutput.Length; i++)
+            {
+                Assert.AreEqual(expectedOutput[i], mask.Paths[i]);
+            }
+        }
+
+        [Test]
+        public void FromString_Validated()
+        {
+            // Check whether the field paths are valid if a class parameter is provided.
+            Assert.DoesNotThrow(() => FieldMask.FromString<NestedTestAllTypes>(",payload"));
+            Assert.Throws<InvalidProtocolBufferException>(() => FieldMask.FromString<NestedTestAllTypes>("payload,nonexist"));
+        }
+
+        [Test]
+        [TestCase(new int[] { }, new string[] { })]
+        [TestCase(new int[] { TestAllTypes.SingleInt32FieldNumber }, new string[] { "single_int32" })]
+        [TestCase(new int[] { TestAllTypes.SingleInt32FieldNumber, TestAllTypes.SingleInt64FieldNumber }, new string[] { "single_int32", "single_int64" })]
+        public void FromFieldNumbers(int[] input, string[] expectedOutput)
+        {
+            FieldMask mask = FieldMask.FromFieldNumbers<TestAllTypes>(input);
+            Assert.AreEqual(expectedOutput.Length, mask.Paths.Count);
+            for (int i = 0; i < expectedOutput.Length; i++)
+            {
+                Assert.AreEqual(expectedOutput[i], mask.Paths[i]);
+            }
+        }
+
+        [Test]
+        public void FromFieldNumbers_Invalid()
+        {
+            Assert.Throws<ArgumentNullException>(() =>
+            {
+                int invalidFieldNumber = 1000;
+                FieldMask.FromFieldNumbers<TestAllTypes>(invalidFieldNumber);
+            });
+        }
+
+        [Test]
+        [TestCase(new string[] { }, "\"\"")]
+        [TestCase(new string[] { "foo" }, "\"foo\"")]
+        [TestCase(new string[] { "foo", "bar" }, "\"foo,bar\"")]
+        [TestCase(new string[] { "", "foo", "", "bar", "" }, "\",foo,bar\"")]
+        public void Normalize(string[] input, string expectedOutput)
+        {
+            FieldMask mask = new FieldMask();
+            mask.Paths.AddRange(input);
+            FieldMask result = mask.Normalize();
+            Assert.AreEqual(expectedOutput, result.ToString());
+        }
+
+        [Test]
+        public void Union()
+        {
+            // Only test a simple case here and expect
+            // {@link FieldMaskTreeTest#AddFieldPath} to cover all scenarios.
+            FieldMask mask1 = FieldMask.FromString("foo,bar.baz,bar.quz");
+            FieldMask mask2 = FieldMask.FromString("foo.bar,bar");
+            FieldMask result = mask1.Union(mask2);
+            Assert.AreEqual(2, result.Paths.Count);
+            Assert.Contains("bar", result.Paths);
+            Assert.Contains("foo", result.Paths);
+            Assert.That(result.Paths, Has.No.Member("bar.baz"));
+            Assert.That(result.Paths, Has.No.Member("bar.quz"));
+            Assert.That(result.Paths, Has.No.Member("foo.bar"));
+        }
+
+        [Test]
+        public void Union_UsingVarArgs()
+        {
+            FieldMask mask1 = FieldMask.FromString("foo");
+            FieldMask mask2 = FieldMask.FromString("foo.bar,bar.quz");
+            FieldMask mask3 = FieldMask.FromString("bar.quz");
+            FieldMask mask4 = FieldMask.FromString("bar");
+            FieldMask result = mask1.Union(mask2, mask3, mask4);
+            Assert.AreEqual(2, result.Paths.Count);
+            Assert.Contains("bar", result.Paths);
+            Assert.Contains("foo", result.Paths);
+            Assert.That(result.Paths, Has.No.Member("foo.bar"));
+            Assert.That(result.Paths, Has.No.Member("bar.quz"));
+        }
+
+        [Test]
+        public void Intersection()
+        {
+            // Only test a simple case here and expect
+            // {@link FieldMaskTreeTest#IntersectFieldPath} to cover all scenarios.
+            FieldMask mask1 = FieldMask.FromString("foo,bar.baz,bar.quz");
+            FieldMask mask2 = FieldMask.FromString("foo.bar,bar");
+            FieldMask result = mask1.Intersection(mask2);
+            Assert.AreEqual(3, result.Paths.Count);
+            Assert.Contains("foo.bar", result.Paths);
+            Assert.Contains("bar.baz", result.Paths);
+            Assert.Contains("bar.quz", result.Paths);
+            Assert.That(result.Paths, Has.No.Member("foo"));
+            Assert.That(result.Paths, Has.No.Member("bar"));
+        }
+
+        [Test]
+        public void Merge()
+        {
+            // Only test a simple case here and expect
+            // {@link FieldMaskTreeTest#Merge} to cover all scenarios.
+            FieldMask fieldMask = FieldMask.FromString("payload");
+            NestedTestAllTypes source = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    SingleInt32 = 1234,
+                    SingleFixed64 = 4321
+                }
+            };
+            NestedTestAllTypes destination = new NestedTestAllTypes();
+            fieldMask.Merge(source, destination);
+            Assert.AreEqual(1234, destination.Payload.SingleInt32);
+            Assert.AreEqual(4321, destination.Payload.SingleFixed64);
+
+            destination = new NestedTestAllTypes
+            {
+                Payload = new TestAllTypes
+                {
+                    SingleInt32 = 4321,
+                    SingleInt64 = 5678
+                }
+            };
+            fieldMask.Merge(source, destination);
+            Assert.AreEqual(1234, destination.Payload.SingleInt32);
+            Assert.AreEqual(5678, destination.Payload.SingleInt64);
+            Assert.AreEqual(4321, destination.Payload.SingleFixed64);
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/FieldMaskTree.cs b/csharp/src/Google.Protobuf/FieldMaskTree.cs
new file mode 100644
index 0000000..36c823b
--- /dev/null
+++ b/csharp/src/Google.Protobuf/FieldMaskTree.cs
@@ -0,0 +1,364 @@
+#region Copyright notice and license
+// Protocol Buffers - Google's data interchange format
+// Copyright 2015 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#endregion
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Google.Protobuf.Reflection;
+using Google.Protobuf.WellKnownTypes;
+
+namespace Google.Protobuf
+{
+    /// <summary>
+    /// <para>A tree representation of a FieldMask. Each leaf node in this tree represent
+    /// a field path in the FieldMask.</para>
+    /// 
+    /// <para>For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be:</para>
+    /// <code>
+    ///   [root] -+- foo -+- bar
+    ///           |       |
+    ///           |       +- baz
+    ///           |
+    ///           +- bar --- baz
+    /// </code>
+    /// 
+    /// <para>By representing FieldMasks with this tree structure we can easily convert
+    /// a FieldMask to a canonical form, merge two FieldMasks, calculate the
+    /// intersection to two FieldMasks and traverse all fields specified by the
+    /// FieldMask in a message tree.</para>
+    /// </summary>
+    internal sealed class FieldMaskTree
+    {
+        private const char FIELD_PATH_SEPARATOR = '.';
+
+        internal sealed class Node
+        {
+            public Dictionary<string, Node> Children { get; } = new Dictionary<string, Node>();
+        }
+
+        private readonly Node root = new Node();
+
+        /// <summary>
+        /// Creates an empty FieldMaskTree.
+        /// </summary>
+        public FieldMaskTree()
+        {
+        }
+
+        /// <summary>
+        /// Creates a FieldMaskTree for a given FieldMask.
+        /// </summary>
+        public FieldMaskTree(FieldMask mask)
+        {
+            MergeFromFieldMask(mask);
+        }
+
+        public override string ToString()
+        {
+            return ToFieldMask().ToString();
+        }
+
+        /// <summary>
+        /// Adds a field path to the tree. In a FieldMask, every field path matches the
+        /// specified field as well as all its sub-fields. For example, a field path
+        /// "foo.bar" matches field "foo.bar" and also "foo.bar.baz", etc. When adding
+        /// a field path to the tree, redundant sub-paths will be removed. That is,
+        /// after adding "foo.bar" to the tree, "foo.bar.baz" will be removed if it
+        /// exists, which will turn the tree node for "foo.bar" to a leaf node.
+        /// Likewise, if the field path to add is a sub-path of an existing leaf node,
+        /// nothing will be changed in the tree.
+        /// </summary>
+        public FieldMaskTree AddFieldPath(string path)
+        {
+            var parts = path.Split(FIELD_PATH_SEPARATOR);
+            if (parts.Length == 0)
+            {
+                return this;
+            }
+
+            var node = root;
+            var createNewBranch = false;
+
+            // Find the matching node in the tree.
+            foreach (var part in parts)
+            {
+                // Check whether the path matches an existing leaf node.
+                if (!createNewBranch
+                    && node != root
+                    && node.Children.Count == 0)
+                {
+                    // The path to add is a sub-path of an existing leaf node.
+                    return this;
+                }
+
+                if (!node.Children.TryGetValue(part, out var childNode))
+                {
+                    createNewBranch = true;
+                    childNode = new Node();
+                    node.Children.Add(part, childNode);
+                }
+                node = childNode;
+            }
+
+            // Turn the matching node into a leaf node (i.e., remove sub-paths).
+            node.Children.Clear();
+            return this;
+        }
+
+        /// <summary>
+        /// Merges all field paths in a FieldMask into this tree.
+        /// </summary>
+        public FieldMaskTree MergeFromFieldMask(FieldMask mask)
+        {
+            foreach (var path in mask.Paths)
+            {
+                AddFieldPath(path);
+            }
+
+            return this;
+        }
+
+        /// <summary>
+        /// Converts this tree to a FieldMask.
+        /// </summary>
+        public FieldMask ToFieldMask()
+        {
+            var mask = new FieldMask();
+            if (root.Children.Count != 0)
+            {
+                var paths = new List<string>();
+                GetFieldPaths(root, "", paths);
+                mask.Paths.AddRange(paths);
+            }
+
+            return mask;
+        }
+
+        /// <summary>
+        /// Gathers all field paths in a sub-tree.
+        /// </summary>
+        private void GetFieldPaths(Node node, string path, List<string> paths)
+        {
+            if (node.Children.Count == 0)
+            {
+                paths.Add(path);
+                return;
+            }
+
+            foreach (var entry in node.Children)
+            {
+                var childPath = path.Length == 0 ? entry.Key : path + "." + entry.Key;
+                GetFieldPaths(entry.Value, childPath, paths);
+            }
+        }
+
+        /// <summary>
+        /// Adds the intersection of this tree with the given <paramref name="path"/> to <paramref name="output"/>.
+        /// </summary>
+        public void IntersectFieldPath(string path, FieldMaskTree output)
+        {
+            if (root.Children.Count == 0)
+            {
+                return;
+            }
+
+            var parts = path.Split(FIELD_PATH_SEPARATOR);
+            if (parts.Length == 0)
+            {
+                return;
+            }
+
+            var node = root;
+            foreach (var part in parts)
+            {
+                if (node != root
+                    && node.Children.Count == 0)
+                {
+                    // The given path is a sub-path of an existing leaf node in the tree.
+                    output.AddFieldPath(path);
+                    return;
+                }
+
+                if (!node.Children.TryGetValue(part, out node))
+                {
+                    return;
+                }
+            }
+
+            // We found a matching node for the path. All leaf children of this matching
+            // node is in the intersection.
+            var paths = new List<string>();
+            GetFieldPaths(node, path, paths);
+            foreach (var value in paths)
+            {
+                output.AddFieldPath(value);
+            }
+        }
+
+        /// <summary>
+        /// Merges all fields specified by this FieldMaskTree from <paramref name="source"/> to <paramref name="destination"/>.
+        /// </summary>
+        public void Merge(IMessage source, IMessage destination, FieldMask.MergeOptions options)
+        {
+            if (source.Descriptor != destination.Descriptor)
+            {
+                throw new InvalidProtocolBufferException("Cannot merge messages of different types.");
+            }
+
+            if (root.Children.Count == 0)
+            {
+                return;
+            }
+
+            Merge(root, "", source, destination, options);
+        }
+        
+        /// <summary>
+        /// Merges all fields specified by a sub-tree from <paramref name="source"/> to <paramref name="destination"/>.
+        /// </summary>
+        private void Merge(
+            Node node,
+            string path,
+            IMessage source,
+            IMessage destination,
+            FieldMask.MergeOptions options)
+        {
+            if (source.Descriptor != destination.Descriptor)
+            {
+                throw new InvalidProtocolBufferException($"source ({source.Descriptor}) and destination ({destination.Descriptor}) descriptor must be equal");
+            }
+
+            var descriptor = source.Descriptor;
+            foreach (var entry in node.Children)
+            {
+                var field = descriptor.FindFieldByName(entry.Key);
+                if (field == null)
+                {
+                    Debug.WriteLine($"Cannot find field \"{entry.Key}\" in message type \"{descriptor.FullName}\"");
+                    continue;
+                }
+
+                if (entry.Value.Children.Count != 0)
+                {
+                    if (field.IsRepeated
+                        || field.FieldType != FieldType.Message)
+                    {
+                        Debug.WriteLine($"Field \"{field.FullName}\" is not a singular message field and cannot have sub-fields.");
+                        continue;
+                    }
+
+                    var sourceField = field.Accessor.GetValue(source);
+                    var destinationField = field.Accessor.GetValue(destination);
+                    if (sourceField == null
+                        && destinationField == null)
+                    {
+                        // If the message field is not present in both source and destination, skip recursing
+                        // so we don't create unnecessary empty messages.
+                        continue;
+                    }
+
+                    if (destinationField == null)
+                    {
+                        // If we have to merge but the destination does not contain the field, create it.
+                        destinationField = field.MessageType.Parser.CreateTemplate();
+                        field.Accessor.SetValue(destination, destinationField);
+                    }
+
+                    var childPath = path.Length == 0 ? entry.Key : path + "." + entry.Key;
+                    Merge(entry.Value, childPath, (IMessage)sourceField, (IMessage)destinationField, options);
+                    continue;
+                }
+
+                if (field.IsRepeated)
+                {
+                    if (options.ReplaceRepeatedFields)
+                    {
+                        field.Accessor.Clear(destination);
+                    }
+
+                    var sourceField = (IList)field.Accessor.GetValue(source);
+                    var destinationField = (IList)field.Accessor.GetValue(destination);
+                    foreach (var element in sourceField)
+                    {
+                        destinationField.Add(element);
+                    }
+                }
+                else
+                {
+                    var sourceField = field.Accessor.GetValue(source);
+                    if (field.FieldType == FieldType.Message)
+                    {
+                        if (options.ReplaceMessageFields)
+                        {
+                            if (sourceField == null)
+                            {
+                                field.Accessor.Clear(destination);
+                            }
+                            else
+                            {
+                                field.Accessor.SetValue(destination, sourceField);
+                            }
+                        }
+                        else
+                        {
+                            if (sourceField != null)
+                            {
+                                var sourceByteString = ((IMessage)sourceField).ToByteString();
+                                var destinationValue = (IMessage)field.Accessor.GetValue(destination);
+                                if (destinationValue != null)
+                                {
+                                    destinationValue.MergeFrom(sourceByteString);
+                                }
+                                else
+                                {
+                                    field.Accessor.SetValue(destination, field.MessageType.Parser.ParseFrom(sourceByteString));
+                                }
+                            }
+                        }
+                    }
+                    else
+                    {
+                        if (sourceField != null
+                            || !options.ReplacePrimitiveFields)
+                        {
+                            field.Accessor.SetValue(destination, sourceField);
+                        }
+                        else
+                        {
+                            field.Accessor.Clear(destination);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs
index 4ae10d8..31fd887 100644
--- a/csharp/src/Google.Protobuf/JsonFormatter.cs
+++ b/csharp/src/Google.Protobuf/JsonFormatter.cs
@@ -271,7 +271,25 @@
             }
             return result.ToString();
         }
-        
+
+        internal static string FromJsonName(string name)
+        {
+            StringBuilder result = new StringBuilder(name.Length);
+            foreach (char ch in name)
+            {
+                if (char.IsUpper(ch))
+                {
+                    result.Append('_');
+                    result.Append(char.ToLowerInvariant(ch));
+                }
+                else
+                {
+                    result.Append(ch);
+                }
+            }
+            return result.ToString();
+        }
+
         private static void WriteNull(TextWriter writer)
         {
             writer.Write("null");
diff --git a/csharp/src/Google.Protobuf/WellKnownTypes/FieldMaskPartial.cs b/csharp/src/Google.Protobuf/WellKnownTypes/FieldMaskPartial.cs
index 4b0670f..27e513c 100644
--- a/csharp/src/Google.Protobuf/WellKnownTypes/FieldMaskPartial.cs
+++ b/csharp/src/Google.Protobuf/WellKnownTypes/FieldMaskPartial.cs
@@ -35,15 +35,18 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Text;
+using Google.Protobuf.Reflection;
 
 namespace Google.Protobuf.WellKnownTypes
 {
     // Manually-written partial class for the FieldMask well-known type.
     public partial class FieldMask : ICustomDiagnosticMessage
     {
+        private const char FIELD_PATH_SEPARATOR = ',';
+        private const char FIELD_SEPARATOR_REGEX = '.';
+
         /// <summary>
-        /// Converts a timestamp  specified in seconds/nanoseconds to a string.
+        /// Converts a field mask specified by paths to a string.
         /// </summary>
         /// <remarks>
         /// If the value is a normalized duration in the range described in <c>field_mask.proto</c>,
@@ -55,7 +58,7 @@
         /// <exception cref="InvalidOperationException">The represented field mask is invalid, and <paramref name="diagnosticOnly"/> is <c>false</c>.</exception>
         internal static string ToJson(IList<string> paths, bool diagnosticOnly)
         {
-            var firstInvalid = paths.FirstOrDefault(p => !ValidatePath(p));
+            var firstInvalid = paths.FirstOrDefault(p => !IsPathValid(p));
             if (firstInvalid == null)
             {
                 var writer = new StringWriter();
@@ -85,10 +88,102 @@
         }
 
         /// <summary>
+        /// Returns a string representation of this <see cref="FieldMask"/> for diagnostic purposes.
+        /// </summary>
+        /// <remarks>
+        /// Normally the returned value will be a JSON string value (including leading and trailing quotes) but
+        /// when the value is non-normalized or out of range, a JSON object representation will be returned
+        /// instead, including a warning. This is to avoid exceptions being thrown when trying to
+        /// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized
+        /// values.
+        /// </remarks>
+        /// <returns>A string representation of this value.</returns>
+        public string ToDiagnosticString()
+        {
+            return ToJson(Paths, true);
+        }
+
+        /// <summary>
+        /// Parses from a string to a FieldMask.
+        /// </summary>
+        public static FieldMask FromString(string value)
+        {
+            return FromStringEnumerable<Empty>(new List<string>(value.Split(FIELD_PATH_SEPARATOR)));
+        }
+
+        /// <summary>
+        /// Parses from a string to a FieldMask and validates all field paths.
+        /// </summary>
+        /// <typeparam name="T">The type to validate the field paths against.</typeparam>
+        public static FieldMask FromString<T>(string value) where T : IMessage
+        {
+            return FromStringEnumerable<T>(new List<string>(value.Split(FIELD_PATH_SEPARATOR)));
+        }
+
+        /// <summary>
+        /// Constructs a FieldMask for a list of field paths in a certain type.
+        /// </summary>
+        /// <typeparam name="T">The type to validate the field paths against.</typeparam>
+        public static FieldMask FromStringEnumerable<T>(IEnumerable<string> paths) where T : IMessage
+        {
+            var mask = new FieldMask();
+            foreach (var path in paths)
+            {
+                if (path.Length == 0)
+                {
+                    // Ignore empty field paths.
+                    continue;
+                }
+
+                if (typeof(T) != typeof(Empty)
+                    && !IsValid<T>(path))
+                {
+                    throw new InvalidProtocolBufferException(path + " is not a valid path for " + typeof(T));
+                }
+
+                mask.Paths.Add(path);
+            }
+
+            return mask;
+        }
+
+        /// <summary>
+        /// Constructs a FieldMask from the passed field numbers.
+        /// </summary>
+        /// <typeparam name="T">The type to validate the field paths against.</typeparam>
+        public static FieldMask FromFieldNumbers<T>(params int[] fieldNumbers) where T : IMessage
+        {
+            return FromFieldNumbers<T>((IEnumerable<int>)fieldNumbers);
+        }
+
+        /// <summary>
+        /// Constructs a FieldMask from the passed field numbers.
+        /// </summary>
+        /// <typeparam name="T">The type to validate the field paths against.</typeparam>
+        public static FieldMask FromFieldNumbers<T>(IEnumerable<int> fieldNumbers) where T : IMessage
+        {
+            var descriptor = Activator.CreateInstance<T>().Descriptor;
+
+            var mask = new FieldMask();
+            foreach (var fieldNumber in fieldNumbers)
+            {
+                var field = descriptor.FindFieldByNumber(fieldNumber);
+                if (field == null)
+                {
+                    throw new ArgumentNullException($"{fieldNumber} is not a valid field number for {descriptor.Name}");
+                }
+
+                mask.Paths.Add(field.Name);
+            }
+
+            return mask;
+        }
+
+        /// <summary>
         /// Checks whether the given path is valid for a field mask.
         /// </summary>
         /// <returns>true if the path is valid; false otherwise</returns>
-        private static bool ValidatePath(string input)
+        private static bool IsPathValid(string input)
         {
             for (int i = 0; i < input.Length; i++)
             {
@@ -110,19 +205,166 @@
         }
 
         /// <summary>
-        /// Returns a string representation of this <see cref="FieldMask"/> for diagnostic purposes.
+        /// Checks whether paths in a given fields mask are valid.
         /// </summary>
-        /// <remarks>
-        /// Normally the returned value will be a JSON string value (including leading and trailing quotes) but
-        /// when the value is non-normalized or out of range, a JSON object representation will be returned
-        /// instead, including a warning. This is to avoid exceptions being thrown when trying to
-        /// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized
-        /// values.
-        /// </remarks>
-        /// <returns>A string representation of this value.</returns>
-        public string ToDiagnosticString()
+        /// <typeparam name="T">The type to validate the field paths against.</typeparam>
+        public static bool IsValid<T>(FieldMask fieldMask) where T : IMessage
         {
-            return ToJson(Paths, true);
+            var descriptor = Activator.CreateInstance<T>().Descriptor;
+
+            return IsValid(descriptor, fieldMask);
+        }
+
+        /// <summary>
+        /// Checks whether paths in a given fields mask are valid.
+        /// </summary>
+        public static bool IsValid(MessageDescriptor descriptor, FieldMask fieldMask)
+        {
+            foreach (var path in fieldMask.Paths)
+            {
+                if (!IsValid(descriptor, path))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Checks whether a given field path is valid.
+        /// </summary>
+        /// <typeparam name="T">The type to validate the field paths against.</typeparam>
+        public static bool IsValid<T>(string path) where T : IMessage
+        {
+            var descriptor = Activator.CreateInstance<T>().Descriptor;
+
+            return IsValid(descriptor, path);
+        }
+
+        /// <summary>
+        /// Checks whether paths in a given fields mask are valid.
+        /// </summary>
+        public static bool IsValid(MessageDescriptor descriptor, string path)
+        {
+            var parts = path.Split(FIELD_SEPARATOR_REGEX);
+            if (parts.Length == 0)
+            {
+                return false;
+            }
+
+            foreach (var name in parts)
+            {
+                var field = descriptor?.FindFieldByName(name);
+                if (field == null)
+                {
+                    return false;
+                }
+
+                if (!field.IsRepeated
+                    && field.FieldType == FieldType.Message)
+                {
+                    descriptor = field.MessageType;
+                }
+                else
+                {
+                    descriptor = null;
+                }
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Converts this FieldMask to its canonical form. In the canonical form of a
+        /// FieldMask, all field paths are sorted alphabetically and redundant field
+        /// paths are removed.
+        /// </summary>
+        public FieldMask Normalize()
+        {
+            return new FieldMaskTree(this).ToFieldMask();
+        }
+
+        /// <summary>
+        /// Creates a union of two or more FieldMasks.
+        /// </summary>
+        public FieldMask Union(params FieldMask[] otherMasks)
+        {
+            var maskTree = new FieldMaskTree(this);
+            foreach (var mask in otherMasks)
+            {
+                maskTree.MergeFromFieldMask(mask);
+            }
+
+            return maskTree.ToFieldMask();
+        }
+
+        /// <summary>
+        /// Calculates the intersection of two FieldMasks.
+        /// </summary>
+        public FieldMask Intersection(FieldMask additionalMask)
+        {
+            var tree = new FieldMaskTree(this);
+            var result = new FieldMaskTree();
+            foreach (var path in additionalMask.Paths)
+            {
+                tree.IntersectFieldPath(path, result);
+            }
+
+            return result.ToFieldMask();
+        }
+
+        /// <summary>
+        /// Merges fields specified by this FieldMask from one message to another with the
+        /// specified merge options.
+        /// </summary>
+        public void Merge(IMessage source, IMessage destination, MergeOptions options)
+        {
+            new FieldMaskTree(this).Merge(source, destination, options);
+        }
+
+        /// <summary>
+        /// Merges fields specified by this FieldMask from one message to another.
+        /// </summary>
+        public void Merge(IMessage source, IMessage destination)
+        {
+            Merge(source, destination, new MergeOptions());
+        }
+
+        /// <summary>
+        /// Options to customize merging behavior.
+        /// </summary>
+        public sealed class MergeOptions
+        {
+            /// <summary>
+            /// Whether to replace message fields(i.e., discard existing content in
+            /// destination message fields) when merging.
+            /// Default behavior is to merge the source message field into the
+            /// destination message field.
+            /// </summary>
+            public bool ReplaceMessageFields { get; set; } = false;
+
+            /// <summary>
+            /// Whether to replace repeated fields (i.e., discard existing content in
+            /// destination repeated fields) when merging.
+            /// Default behavior is to append elements from source repeated field to the
+            /// destination repeated field.
+            /// </summary>
+            public bool ReplaceRepeatedFields { get; set; } = false;
+
+            /// <summary>
+            /// Whether to replace primitive (non-repeated and non-message) fields in
+            /// destination message fields with the source primitive fields (i.e., if the
+            /// field is set in the source, the value is copied to the
+            /// destination; if the field is unset in the source, the field is cleared
+            /// from the destination) when merging.
+            /// 
+            /// Default behavior is to always set the value of the source primitive
+            /// field to the destination primitive field, and if the source field is
+            /// unset, the default value of the source field is copied to the
+            /// destination.
+            /// </summary>
+            public bool ReplacePrimitiveFields { get; set; } = false;
         }
     }
 }