message extensions + refactor
diff --git a/csharp/src/Google.Protobuf/CodedOutputStream.cs b/csharp/src/Google.Protobuf/CodedOutputStream.cs
index b4c9045..1c4e40b 100644
--- a/csharp/src/Google.Protobuf/CodedOutputStream.cs
+++ b/csharp/src/Google.Protobuf/CodedOutputStream.cs
@@ -636,7 +636,7 @@
         public void Flush()

         {

             var span = new Span<byte>(buffer);

-            state.writeBufferHelper.Flush(ref span, ref state);

+            WriteBufferHelper.Flush(ref span, ref state);

             

             /*if (output != null)

             {

@@ -648,36 +648,18 @@
         /// Verifies that SpaceLeft returns zero. It's common to create a byte array

         /// that is exactly big enough to hold a message, then write to it with

         /// a CodedOutputStream. Calling CheckNoSpaceLeft after writing verifies that

-        /// the message was actually as big as expected, which can help bugs.

+        /// the message was actually as big as expected, which can help finding bugs.

         /// </summary>

         public void CheckNoSpaceLeft()

         {

-            if (SpaceLeft != 0)

-            {

-                throw new InvalidOperationException("Did not write as much data as expected.");

-            }

+            WriteBufferHelper.CheckNoSpaceLeft(ref state);

         }

 

         /// <summary>

         /// If writing to a flat array, returns the space left in the array. Otherwise,

         /// throws an InvalidOperationException.

         /// </summary>

-        public int SpaceLeft

-        {

-            get

-            {

-                if (output == null)

-                {

-                    return state.limit - state.position;

-                }

-                else

-                {

-                    throw new InvalidOperationException(

-                        "SpaceLeft can only be called on CodedOutputStreams that are " +

-                        "writing to a flat array.");

-                }

-            }

-        }

+        public int SpaceLeft => WriteBufferHelper.GetSpaceLeft(ref state);

 

         internal byte[] InternalBuffer => buffer;

 

diff --git a/csharp/src/Google.Protobuf/MessageExtensions.cs b/csharp/src/Google.Protobuf/MessageExtensions.cs
index e9a408c..d0db44b 100644
--- a/csharp/src/Google.Protobuf/MessageExtensions.cs
+++ b/csharp/src/Google.Protobuf/MessageExtensions.cs
@@ -33,6 +33,7 @@
 using Google.Protobuf.Reflection;
 using System.Buffers;
 using System.Collections;
+using System;
 using System.IO;
 using System.Linq;
 using System.Security;
@@ -146,6 +147,40 @@
         }
 
         /// <summary>
+        /// Writes the given message data to the given buffer writer in protobuf encoding.
+        /// </summary>
+        /// <param name="message">The message to write to the stream.</param>
+        /// <param name="output">The stream to write to.</param>
+        public static void WriteTo(this IMessage message, IBufferWriter<byte> output)
+        {
+            ProtoPreconditions.CheckNotNull(message, nameof(message));
+            ProtoPreconditions.CheckNotNull(output, nameof(output));
+
+            WriteContext.Initialize(output, out WriteContext ctx);
+            WritingPrimitivesMessages.WriteRawMessage(ref ctx, message);
+            ctx.Flush();
+
+            // TODO: handling errors when IBufferWriter is used?
+        }
+
+        /// <summary>
+        /// Writes the given message data to the given span in protobuf encoding.
+        /// The size of the destination span needs to fit the serialized size
+        /// of the message exactly, otherwise an exception is thrown.
+        /// </summary>
+        /// <param name="message">The message to write to the stream.</param>
+        /// <param name="output">The span to write to. Size must match size of the message exactly.</param>
+        public static void WriteTo(this IMessage message, Span<byte> output)
+        {
+            ProtoPreconditions.CheckNotNull(message, nameof(message));
+
+            WriteContext.Initialize(ref output, out WriteContext ctx);
+            WritingPrimitivesMessages.WriteRawMessage(ref ctx, message);
+            ctx.Flush();
+            ctx.CheckNoSpaceLeft();
+        }
+
+        /// <summary>
         /// Checks if all required fields in a message have values set. For proto3 messages, this returns true
         /// </summary>
         public static bool IsInitialized(this IMessage message)
diff --git a/csharp/src/Google.Protobuf/WriteBufferHelper.cs b/csharp/src/Google.Protobuf/WriteBufferHelper.cs
index bf29b22..9bd5061 100644
--- a/csharp/src/Google.Protobuf/WriteBufferHelper.cs
+++ b/csharp/src/Google.Protobuf/WriteBufferHelper.cs
@@ -62,7 +62,7 @@
         }
 
         /// <summary>
-        /// Initialize an instance with a coded output stream.
+        /// Initialize an instance with a buffer writer.
         /// This approach is faster than using a constructor because the instance to initialize is passed by reference
         /// and we can write directly into it without copying.
         /// </summary>
@@ -74,21 +74,65 @@
             buffer = default;  // TODO: initialize the initial buffer so that the first write is not via slowpath.
         }
 
-        public void RefreshBuffer(ref Span<byte> buffer, ref WriterInternalState state)
+        /// <summary>
+        /// Initialize an instance with a buffer represented by a single span (i.e. buffer cannot be refreshed)
+        /// This approach is faster than using a constructor because the instance to initialize is passed by reference
+        /// and we can write directly into it without copying.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void InitializeNonRefreshable(out WriteBufferHelper instance)
         {
-            if (codedOutputStream?.InternalOutputStream != null)
+            instance.bufferWriter = null;
+            instance.codedOutputStream = null;
+        }
+
+        /// <summary>
+        /// Verifies that SpaceLeft returns zero.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void CheckNoSpaceLeft(ref WriterInternalState state)
+        {
+            if (GetSpaceLeft(ref state) != 0)
+            {
+                throw new InvalidOperationException("Did not write as much data as expected.");
+            }
+        }
+
+        /// <summary>
+        /// If writing to a flat array, returns the space left in the array. Otherwise,
+        /// throws an InvalidOperationException.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static int GetSpaceLeft(ref WriterInternalState state)
+        {
+            if (state.writeBufferHelper.codedOutputStream?.InternalOutputStream == null && state.writeBufferHelper.bufferWriter == null)
+            {
+                return state.limit - state.position;
+            }
+            else
+            {
+                throw new InvalidOperationException(
+                    "SpaceLeft can only be called on CodedOutputStreams that are " +
+                        "writing to a flat array or when writing to a single span.");
+            }
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void RefreshBuffer(ref Span<byte> buffer, ref WriterInternalState state)
+        {
+            if (state.writeBufferHelper.codedOutputStream?.InternalOutputStream != null)
             {
                 // because we're using coded output stream, we know that "buffer" and codedOutputStream.InternalBuffer are identical.
-                codedOutputStream.InternalOutputStream.Write(codedOutputStream.InternalBuffer, 0, state.position);
+                state.writeBufferHelper.codedOutputStream.InternalOutputStream.Write(state.writeBufferHelper.codedOutputStream.InternalBuffer, 0, state.position);
                 // reset position, limit stays the same because we are reusing the codedOutputStream's internal buffer.
                 state.position = 0;
             }
-            else if (bufferWriter != null)
+            else if (state.writeBufferHelper.bufferWriter != null)
             {
                 // commit the bytes and get a new buffer to write to.
-                bufferWriter.Advance(state.position);
+                state.writeBufferHelper.bufferWriter.Advance(state.position);
                 state.position = 0;
-                buffer = bufferWriter.GetSpan();
+                buffer = state.writeBufferHelper.bufferWriter.GetSpan();
                 state.limit = buffer.Length;
             }
             else
@@ -98,17 +142,18 @@
             }
         }
 
-        public void Flush(ref Span<byte> buffer, ref WriterInternalState state)
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void Flush(ref Span<byte> buffer, ref WriterInternalState state)
         {
-            if (codedOutputStream?.InternalOutputStream != null)
+            if (state.writeBufferHelper.codedOutputStream?.InternalOutputStream != null)
             {
                 // because we're using coded output stream, we know that "buffer" and codedOutputStream.InternalBuffer are identical.
-                codedOutputStream.InternalOutputStream.Write(codedOutputStream.InternalBuffer, 0, state.position);
+                state.writeBufferHelper.codedOutputStream.InternalOutputStream.Write(state.writeBufferHelper.codedOutputStream.InternalBuffer, 0, state.position);
                 state.position = 0;
             }
-            else if (bufferWriter != null)
+            else if (state.writeBufferHelper.bufferWriter != null)
             {
-                bufferWriter.Advance(state.position);
+                state.writeBufferHelper.bufferWriter.Advance(state.position);
                 state.position = 0;
                 state.limit = 0;
                 buffer = default;  // invalidate the current buffer
diff --git a/csharp/src/Google.Protobuf/WriteContext.cs b/csharp/src/Google.Protobuf/WriteContext.cs
index c4d0343..e822e1d 100644
--- a/csharp/src/Google.Protobuf/WriteContext.cs
+++ b/csharp/src/Google.Protobuf/WriteContext.cs
@@ -87,6 +87,16 @@
             ctx.state.position = 0;
         }
 
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static void Initialize(ref Span<byte> buffer, out WriteContext ctx)
+        {
+            ctx.buffer = buffer;
+            ctx.state = default;
+            ctx.state.limit = ctx.buffer.Length;
+            ctx.state.position = 0;
+            WriteBufferHelper.InitializeNonRefreshable(out ctx.state.writeBufferHelper);
+        }
+
         /// <summary>
         /// Writes a double field value, without a tag.
         /// </summary>
@@ -340,8 +350,12 @@
 
         internal void Flush()
         {
-            // TODO: should the method be static or not?
-            state.writeBufferHelper.Flush(ref buffer, ref state);
+            WriteBufferHelper.Flush(ref buffer, ref state);
+        }
+
+        internal void CheckNoSpaceLeft()
+        {
+            WriteBufferHelper.CheckNoSpaceLeft(ref state);
         }
 
         internal void CopyStateTo(CodedOutputStream output)
diff --git a/csharp/src/Google.Protobuf/WritingPrimitives.cs b/csharp/src/Google.Protobuf/WritingPrimitives.cs
index 76df2df..f618789 100644
--- a/csharp/src/Google.Protobuf/WritingPrimitives.cs
+++ b/csharp/src/Google.Protobuf/WritingPrimitives.cs
@@ -376,7 +376,7 @@
         {
             if (state.position == state.limit)
             {
-                state.writeBufferHelper.RefreshBuffer(ref buffer, ref state);
+                WriteBufferHelper.RefreshBuffer(ref buffer, ref state);
             }
 
             buffer[state.position++] = value;
@@ -429,7 +429,7 @@
                     value.Slice(bytesWritten, length).CopyTo(buffer.Slice(state.position, length));
                     bytesWritten += length;
                     state.position += length;
-                    state.writeBufferHelper.RefreshBuffer(ref buffer, ref state);
+                    WriteBufferHelper.RefreshBuffer(ref buffer, ref state);
                 }
 
                 // copy the remaining data