ZaString is a high-performance, zero-allocation string toolbox for modern .NET applications. It provides stack-first, Span-based builders and helpers so you can assemble, format and encode strings with predictable, allocation-free performance.
ZaString is built for scenarios where every allocation matters. If you're writing high-frequency, low-latency services, middleware, or hotspots that produce a lot of transient strings (logging, serialization, templating), ZaString helps keep GC pressure low while staying familiar and idiomatic to C# developers.
Key highlights:
- Zero allocation: Stack-first, Span-backed APIs avoid ephemeral heap allocations
- High performance: Faster than standard
StringBuilderin many scenarios (see benchmarks) - Fluent ergonomic API: Chainable methods for simple building and complex formatting
- UTFβ8 support: Write bytes directly when you need to emit UTFβ8
- Escape helpers: Ready-to-use JSON, HTML, CSV, URL escaping utilities
- Interpolated string handlers: Integrates with C# interpolation for zero-cost formatting
Install from NuGet:
dotnet add package ZaStringusing ZaString.Core;
using ZaString.Extensions;
// Create a stack-allocated buffer and a builder
Span<char> buffer = stackalloc char[100];
var builder = ZaSpanStringBuilder.Create(buffer);
// Build strings with a fluent, zero-allocation API
builder.Append("Hello, ")
.Append("World!")
.Append(" Number: ")
.Append(42)
.Append(" Pi: ")
.Append(Math.PI, "F2");
var spanResult = builder.AsSpan(); // zero-allocation read-only span
Console.WriteLine(spanResult.ToString()); // prints: Hello, World! Number: 42 Pi: 3.14The main string builder that writes directly to a provided Span<char>:
Span<char> buffer = stackalloc char[64];
var builder = ZaSpanStringBuilder.Create(buffer);
builder.Append("User: ")
.Append("John Doe")
.Append(", Age: ")
.Append(25)
.Append(", Active: ")
.Append(true);
// Access as ReadOnlySpan<char> (zero allocation)
var userInfo = builder.AsSpan();For scenarios requiring heap allocation with automatic buffer management:
using var builder = ZaPooledStringBuilder.Rent(128);
builder.Append("Pooled string building")
.Append(" with automatic cleanup");
var result = builder.ToString();
// Buffer automatically returned to pool when disposedUTF-8 byte-level string writing:
Span<byte> buffer = stackalloc byte[256];
var writer = ZaUtf8SpanWriter.Create(buffer);
writer.Append("Hello, UTF-8 World!")
.Append(" Number: ")
.Append(123);
var utf8Bytes = writer.AsSpan();var name = "Alice";
var age = 30;
var pi = Math.PI;
builder.Append($"User: {name}, Age: {age}, Pi: {pi:F2}");builder.Append("Currency: ")
.Append(1234.56, "C") // "$1,234.56"
.Append(", Number: ")
.Append(12345, "N0") // "12,345"
.Append(", Percentage: ")
.Append(0.85, "P2"); // "85.00%"var fr = new CultureInfo("fr-FR");
builder.Append(1234.56, "C", fr); // "1 234,56 β¬"var isActive = true;
builder.Append("Status: ")
.AppendIf(isActive, "Active", "Inactive");// JSON escaping
builder.AppendJsonEscaped("Line1\nLine2\t\"Quote\"");
// HTML escaping
builder.AppendHtmlEscaped("<script>alert('xss')</script>");
// URL encoding
builder.AppendUrlEncoded("Hello World!");
// CSV escaping
builder.AppendCsvEscaped("Value,with,commas");builder.AppendPathSegment("api")
.AppendPathSegment("v1")
.AppendPathSegment("users")
.AppendQueryParam("q", "search term", isFirst: true)
.AppendQueryParam("page", "1");
// Result: "api/v1/users?q=search%20term&page=1"Non-throwing variants for buffer overflow handling:
Span<char> smallBuffer = stackalloc char[10];
var builder = ZaSpanStringBuilder.Create(smallBuffer);
if (builder.TryAppend("Hello, World!"))
{
Console.WriteLine("Successfully appended");
}
else
{
Console.WriteLine("Buffer too small");
}ZaString significantly outperforms traditional string-building approaches. See the benchmark results below and in
the tests/ZaString.Benchmarks project for details.
| Method | Mean Time | Memory Allocations | Performance Ratio |
|---|---|---|---|
StringBuilder (Baseline) |
146.1 ns | 480 B | 1.00x |
String concatenation |
116.3 ns | 248 B | 0.80x |
String interpolation |
116.9 ns | 136 B | 0.80x |
| ZaSpanStringBuilder | 115.2 ns | 0 B | 0.79x |
Basic String Building:
StringBuilder: 146.1 ns, 480 B allocatedStringConcatenation: 116.3 ns, 248 B allocatedStringInterpolation: 116.9 ns, 136 B allocated- ZaSpanStringBuilder: 115.2 ns, 0 B allocated β‘
Number Formatting:
StringBuilder: 295.3 ns, 584 B allocated- ZaSpanStringBuilder: 234.9 ns, 0 B allocated (20% faster)
Large String Operations:
StringBuilder: 1,565.9 ns, 27,312 B allocated- ZaSpanStringBuilder: 1,236.5 ns, 0 B allocated (21% faster)
DateTime Formatting:
StringBuilder: 189.0 ns, 384 B allocated- ZaSpanStringBuilder: 135.7 ns, 0 B allocated (28% faster)
Span vs String Processing:
StringBuilder: 24.7 ns, 256 B allocated- ZaSpanStringBuilder: 10.4 ns, 0 B allocated (58% faster)
These results use a builder baseline (StringBuilder.AppendFormat with InvariantCulture) for apples-to-apples comparison against ZaSpanStringBuilder (zero allocation).
| Case | Builder Mean | Builder Alloc | ZaSpan Mean | ZaSpan Alloc |
|---|---|---|---|---|
| Double | 128.70 ns | 176 B | 104.26 ns | 0 B |
| Double (Formatted F2) | 94.20 ns | 160 B | 73.33 ns | 0 B |
| Float | 105.12 ns | 168 B | 88.40 ns | 0 B |
| Long | 27.51 ns | 176 B | 12.58 ns | 0 B |
| Integer (Formatted N0) | 59.43 ns | 168 B | 38.28 ns | 0 B |
Environment: .NET 8.0.19, Ryzen 9 5950X, BenchmarkDotNet 0.15.2.
- Zero Memory Allocations: ZaSpanStringBuilder uses stack-allocated buffers
- 20-58% Faster: Significantly outperforms StringBuilder across most scenarios
- Predictable Performance: No GC pressure or memory fragmentation
- Scalable: Performance scales linearly with string size
- Memory Efficient: Up to 100% reduction in memory allocations
// Traditional approach - 146.1 ns, 480 B allocated
var sb = new StringBuilder();
sb.Append("Name: ").Append("John").Append(", Age: ").Append(25);
var result = sb.ToString();
// ZaString approach - 115.2 ns, 0 B allocated
Span<char> buffer = stackalloc char[50];
var builder = ZaSpanStringBuilder.Create(buffer);
builder.Append("Name: ").Append("John").Append(", Age: ").Append(25);
var result = builder.AsSpan(); // Zero allocation- High-frequency string operations in performance-critical applications
- Parsing and formatting without memory pressure
- HTTP response building in web servers
- Logging and diagnostics with minimal overhead
- Data serialization to avoid temporary allocations
- Real-time applications requiring predictable performance
Create(Span<char>)- Create a new builder with a bufferAppend()- Append various types with fluent APIAppendLine()- Append with line terminatorTryAppend()- Non-throwing append variantsAsSpan()- Get result asReadOnlySpan<char>ToString()- Get result as string (allocates)Clear()- Reset builder for reuseSetLength(int)- Set current lengthRemoveLast(int)- Remove characters from end
AppendIf()- Conditional appendingAppendJoin()- Join collections with separatorsAppendRepeat()- Repeat charactersAppendFormat()- Composite formattingAppendJsonEscaped()- JSON string escapingAppendHtmlEscaped()- HTML entity escapingAppendUrlEncoded()- URL percent encodingAppendCsvEscaped()- CSV field escapingAppendPathSegment()- URL path buildingAppendQueryParam()- URL query parameter building
ZaSpanStringBuilder and ZaUtf8SpanWriter are ref struct types. This means they:
- Cannot be boxed (cast to
objector interface). - Cannot be fields of a class (only other
ref structs). - Cannot be used in
asyncmethods acrossawaitpoints. - Are designed for short-lived, stack-based operations.
- ZaPooledStringBuilder: Always use
usingor callDispose()to return the internal buffer to theArrayPool. Failure to do so will lead to memory leaks in the pool. - ZaUtf8Handle: This struct wraps a pooled array. You MUST call
Dispose()when finished. Avoid copying this struct around, as multiple disposals of the same handle can corrupt the pool.
ZaUtf8Handle.Pointer provides raw access to the underlying buffer.
- Warning: The underlying array is NOT pinned by default.
- If you need a stable pointer for external calls (P/Invoke) or async operations, you must pin the array manually (e.g., using
fixedorGCHandle). - Accessing the pointer after disposal is undefined behavior.
Run the full unit tests:
dotnet testRun performance benchmarks (Release):
dotnet run --project tests/ZaString.Benchmarks -c ReleaseSee the samples directory for complete working examples demonstrating all features.
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with modern C# features and .NET 9.0
- Inspired by the performance benefits of
Span<T>andMemory<T> - Designed for zero-allocation scenarios in high-performance applications
Made with β€οΈ for high-performance .NET applications
