diff --git a/src/ZString/ZStringWriter.cs b/src/ZString/ZStringWriter.cs
new file mode 100644
index 0000000..862e56c
--- /dev/null
+++ b/src/ZString/ZStringWriter.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Cysharp.Text
+{
+ ///
+ /// A implementation that is backed with .
+ ///
+ ///
+ /// It's important to make sure the writer is always properly disposed.
+ ///
+ public sealed class ZStringWriter : TextWriter
+ {
+ private Utf16ValueStringBuilder sb;
+ private bool isOpen;
+ private UnicodeEncoding encoding;
+
+ ///
+ /// Creates a new instance using as format provider.
+ ///
+ public ZStringWriter() : this(CultureInfo.CurrentCulture)
+ {
+ }
+
+ ///
+ /// Creates a new instance with given format provider.
+ ///
+ public ZStringWriter(IFormatProvider formatProvider) : base(formatProvider)
+ {
+ sb = ZString.CreateStringBuilder();
+ isOpen = true;
+ }
+
+ ///
+ /// Disposes this instance, operations are no longer allowed.
+ ///
+ public override void Close()
+ {
+ Dispose(true);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ sb.Dispose();
+ isOpen = false;
+ base.Dispose(disposing);
+ }
+
+ public override Encoding Encoding => encoding = encoding ?? new UnicodeEncoding(false, false);
+
+ public override void Write(char value)
+ {
+ AssertNotDisposed();
+
+ sb.Append(value);
+ }
+
+ public override void Write(char[] buffer, int index, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (index < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+ if (buffer.Length - index < count)
+ {
+ throw new ArgumentException();
+ }
+ AssertNotDisposed();
+
+ sb.Append(buffer.AsSpan(index, count));
+ }
+
+ public override void Write(string value)
+ {
+ AssertNotDisposed();
+
+ if (value != null)
+ {
+ sb.Append(value);
+ }
+ }
+
+ public override Task WriteAsync(char value)
+ {
+ Write(value);
+ return Task.CompletedTask;
+ }
+
+ public override Task WriteAsync(string value)
+ {
+ Write(value);
+ return Task.CompletedTask;
+ }
+
+ public override Task WriteAsync(char[] buffer, int index, int count)
+ {
+ Write(buffer, index, count);
+ return Task.CompletedTask;
+ }
+
+ public override Task WriteLineAsync(char value)
+ {
+ WriteLine(value);
+ return Task.CompletedTask;
+ }
+
+ public override Task WriteLineAsync(string value)
+ {
+ WriteLine(value);
+ return Task.CompletedTask;
+ }
+
+ public override Task WriteLineAsync(char[] buffer, int index, int count)
+ {
+ WriteLine(buffer, index, count);
+ return Task.CompletedTask;
+ }
+
+ public override void Write(bool value)
+ {
+ AssertNotDisposed();
+ sb.Append(value);
+ }
+
+ public override void Write(decimal value)
+ {
+ AssertNotDisposed();
+ sb.Append(value);
+ }
+
+ ///
+ /// No-op.
+ ///
+ public override Task FlushAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Materializes the current state from underlying string builder.
+ ///
+ public override string ToString()
+ {
+ return sb.ToString();
+ }
+
+#if !NETSTANDARD2_0
+
+ public override void Write(ReadOnlySpan buffer)
+ {
+ AssertNotDisposed();
+
+ sb.Append(buffer);
+ }
+
+ public override void WriteLine(ReadOnlySpan buffer)
+ {
+ AssertNotDisposed();
+
+ sb.Append(buffer);
+ WriteLine();
+ }
+
+ public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Task.FromCanceled(cancellationToken);
+ }
+
+ Write(buffer.Span);
+ return Task.CompletedTask;
+ }
+
+ public override Task WriteLineAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Task.FromCanceled(cancellationToken);
+ }
+
+ WriteLine(buffer.Span);
+ return Task.CompletedTask;
+ }
+#endif
+
+ private void AssertNotDisposed()
+ {
+ if (!isOpen)
+ {
+ throw new ObjectDisposedException(nameof(sb));
+ }
+ }
+ }
+}
diff --git a/tests/ZString.Tests/ZStringWriterTest.cs b/tests/ZString.Tests/ZStringWriterTest.cs
new file mode 100644
index 0000000..3053934
--- /dev/null
+++ b/tests/ZString.Tests/ZStringWriterTest.cs
@@ -0,0 +1,37 @@
+using System;
+using Cysharp.Text;
+using Xunit;
+
+namespace ZStringTests
+{
+ public class ZStringWriterTest
+ {
+ [Fact]
+ public void DoubleDisposeTest()
+ {
+ var sb = new ZStringWriter();
+ sb.Dispose();
+ sb.Dispose(); // call more than once
+ }
+
+ [Fact]
+ public void BasicWrites()
+ {
+ using (var writer = new ZStringWriter())
+ {
+ writer.Write("text1".AsSpan());
+ writer.Write("text2");
+ writer.Write('c');
+ writer.Write(true);
+ writer.Write(123);
+ writer.Write(456f);
+ writer.Write(789d);
+ writer.Write("end".AsMemory());
+ writer.WriteLine();
+
+ var expected = "text1text2cTrue123456789end" + Environment.NewLine;
+ Assert.Equal(expected, writer.ToString());
+ }
+ }
+ }
+}
\ No newline at end of file