String Mapping
Reinvented.
Spinner maps objects to fixed-width strings and back — with compiled delegates, zero-allocation Span APIs and zero reflection at runtime.
Blazing Fast
Compiled delegates and expression trees eliminate all reflection overhead at runtime.
Zero Allocation
Span-based APIs and ThreadStatic StringBuilder minimise GC pressure to near-zero.
Thread Safe
Designed for high-concurrency — one instance shared across hundreds of threads.
Extensible
Custom interceptors for complex conversions without sacrificing performance.
Fixed-Width
COBOL-style fixed-width layouts with automatic padding, trimming and truncation.
Bi-directional
Seamlessly convert between strongly-typed objects and raw string representations.
Getting Started
Spinner targets .NET 8 and above. Install it from NuGet in seconds.
Installation
Install-Package Spinner
dotnet add package Spinner
Quick Start
Define your model with attributes, then create a Spinner<T> instance:
using Spinner.Attributes;
public class PersonModel
{
[ObjectMapper(30)]
public string Name { get; set; }
[ObjectMapper(8)]
public string BirthDate { get; set; }
}
// Write: object → fixed-width string
var spinner = new Spinner<PersonModel>();
var person = new PersonModel { Name = "John Doe", BirthDate = "19900101" };
string result = spinner.WriteAsString(person);
// → "John Doe 19900101"
// Read: fixed-width string → object
PersonModel parsed = spinner.ReadFromString(result);
Write Object to String
Map strongly-typed .NET objects to fixed-width strings using declarative attributes.
Attributes
ObjectMapperlengthWritePropertypaddingTypeLeft or RightWritePropertyinterceptorTypeIInterceptor<T> for this fieldPadding Types
Use PaddingType.Right for right-aligned fields (e.g. numeric amounts).
[ObjectMapper(10)]
[WriteProperty(PaddingType.Left)]
public string Name { get; set; }
// "John "
[ObjectMapper(10)]
[WriteProperty(PaddingType.Right)]
public string Amount { get; set; }
// " 1000"
API
WriteAsString(T obj)
Concatenates all mapped fields into a single string.
→ stringWriteAsSpan(T obj)
Returns a ReadOnlySpan<char> backed by the ThreadStatic buffer — zero extra allocation.
Read String to Object
Parse fixed-width strings back into strongly-typed .NET objects automatically.
Configuration
public class OrderModel
{
[ObjectMapper(10)]
[ReadProperty(0)] // position 0 → chars 0..9
public string OrderId { get; set; }
[ObjectMapper(15)]
[ReadProperty(1)] // position 1 → chars 10..24
public decimal Amount { get; set; }
[ObjectMapper(8)]
[ReadProperty(2)] // position 2 → chars 25..32
public DateTime OrderDate { get; set; }
}
var spinner = new Spinner<OrderModel>();
OrderModel order = spinner.ReadFromString("ORD001 000000099.9920240101");
Supported Types
Spinner automatically converts these primitive types — no interceptor needed:
Zero-Allocation Reading
For hot throughput paths, use ReadFromSpan to skip string creation entirely:
// No string allocation — pass a Span directly from a buffer / memory-mapped file
ReadOnlySpan<char> raw = buffer.AsSpan(0, 33);
PersonModel person = spinner.ReadFromSpan(raw);
Interceptors
Plug custom conversion logic into the pipeline without sacrificing performance.
Implement IInterceptor<T> to override how a single field is read or written:
public class CurrencyInterceptor : IInterceptor<decimal>
{
// "0000001234" → 12.34
public decimal Read(ReadOnlySpan<char> value)
{
if (long.TryParse(value, out var raw))
return raw / 100m;
return 0m;
}
// 12.34 → "0000001234"
public string Write(decimal value)
=> ((long)(value * 100)).ToString("D10");
}
public class PaymentModel
{
[ObjectMapper(10)]
[ReadProperty(0, typeof(CurrencyInterceptor))]
[WriteProperty(PaddingType.Right, typeof(CurrencyInterceptor))]
public decimal Amount { get; set; }
}
Common Use Cases
Currency Formatting
Convert integer cents to decimal amounts and back.
Custom Date Formats
Parse non-standard date representations like YYYYMMDD.
Boolean Mapping
Map "S"/"N" or "1"/"0" to bool values.
Enum Conversion
Map status codes like "AP" or "RJ" to typed enums.
Advanced Features
Thread Safety
Spinner<T> instance
can be shared across hundreds of concurrent threads with no locks.
All write-side mutable state lives in [ThreadStatic] StringBuilder
fields — each thread gets its own buffer. Property metadata is cached in a
ConcurrentDictionary on first access and never mutated afterward.
// ASP.NET Core — register as Singleton
builder.Services.AddSingleton<Spinner<PersonModel>>();
// Or use a static readonly field
private static readonly Spinner<PersonModel> _spinner = new();
Inspecting the Mapper
Use GetObjectMapper to retrieve the mapping metadata at runtime:
var spinner = new Spinner<PersonModel>();
ObjectMapper[] mappers = spinner.GetObjectMapper;
foreach (var m in mappers)
Console.WriteLine($"Field length: {m.Lenght}");
Performance Benchmarks
Measured on Windows 11 · 12th Gen Intel Core i5-1235U · .NET 8 / 9 / 10
Read Benchmarks
Write Benchmarks
Interceptor Benchmarks
Best Practices
Reuse Instances
Create one Spinner<T> and reuse it everywhere — property metadata is compiled and cached after first use.
Prefer Span APIs
Use ReadFromSpan and WriteAsSpan on hot paths to achieve true zero-allocation throughput.
Avoid Per-Request Creation
Never instantiate Spinner<T> inside loops or per-request code — that discards cached delegates.
DI as Singleton
In ASP.NET Core, register Spinner<T> as a Singleton so the cache is built once and shared.
Migration Guide v2.0
Upgrading from v1.x brings massive performance gains with minimal breaking changes.
Breaking Changes
Spinner is now a sealed class
// Could NOT be stored in a field
var spinner = new Spinner<T>(value);
// Storable, injectable, singleton-safe
var spinner = new Spinner<T>();
Read method signatures simplified
T result = spinner.ReadFromString<T>(value);
T result = spinner.ReadFromString(value);
Removed properties
GetWriteProperties
→
Use GetObjectMapper
GetReadProperties
→
Use GetObjectMapper
New in v2.0
ReadFromSpan zero-allocation APIAggressiveOptimization on all hot paths