FixedWidthParser.NET 1.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package FixedWidthParser.NET --version 1.1.0
                    
NuGet\Install-Package FixedWidthParser.NET -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FixedWidthParser.NET" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FixedWidthParser.NET" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="FixedWidthParser.NET" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FixedWidthParser.NET --version 1.1.0
                    
#r "nuget: FixedWidthParser.NET, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FixedWidthParser.NET@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FixedWidthParser.NET&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=FixedWidthParser.NET&version=1.1.0
                    
Install as a Cake Tool

FixedWidthParser

CI CodeQL codecov NuGet NuGet downloads License: MIT Benchmarks

A high-performance, low-allocation library for parsing and writing fixed-width (flat) files in .NET 10. Columns are declared with attributes, layouts are validated up front, and the hot paths work over spans so fixed-width records can be parsed, streamed and written without the usual per-line churn.

The package includes both:

  • a runtime parser/writer API for regular attribute-mapped models; and
  • a bundled Roslyn source generator for reflection-free TryParse implementations, including a UTF-8 byte path.

Features

  • Attribute-driven column mapping ([FixedColumn(start, length)]) on properties and public fields.
  • Runtime parsing for single lines and lazy batch reading from a TextReader, Stream or file.
  • Source-generated parsing for partial models implementing IFixedWidthModel<TSelf>.
  • UTF-8 byte parsing via Utf8FixedWidthParser<T>, FixedWidthByteReader<T> and generated IUtf8FixedWidthModel<TSelf> models, avoiding StreamReader and UTF-16 transcoding for ASCII-style flat files.
  • Synchronous and asynchronous readers (IEnumerable<T> / IAsyncEnumerable<T>) with struct enumerators on the synchronous path.
  • System.IO.Pipelines support on the byte path: read records straight from a PipeReader (FixedWidthByteReader<T>.ReadAsync(PipeReader) / FixedWidthUtf8.ReadAsync<T>(PipeReader)).
  • Writing for single records and batches, synchronous and asynchronous, with StreamWriter reuse and ReadOnlySpan<T> overloads for zero-allocation output.
  • Configurable formatting per column: alignment, padding character, format string and explicit overflow policy.
  • Culture-aware numeric parsing/formatting, including double/float via csFastFloat and generic ISpanParsable / ISpanFormattable support.
  • Layout validation at construction or generation time: negative Start, non-positive Length and overlapping columns fail clearly.
  • ref struct model support on parser/source-generated single-line parsing.

Requirements

  • .NET 10 (net10.0)

Dependencies: CommunityToolkit.HighPerformance (StringPool) and csFastFloat (fast double/float parsing).

Installation

dotnet add package FixedWidthParser.NET

Or as a <PackageReference>:

<PackageReference Include="FixedWidthParser.NET" Version="1.0.0" />

The package ships the Roslyn source generator bundled as an analyzer. Models that implement IFixedWidthModel<TSelf> or IUtf8FixedWidthModel<TSelf> get generated parsers automatically; no extra package or setup is required.

Defining a Model

Runtime/reflection models only need a public parameterless constructor and mapped fields or properties:

using FixedWidthParser.Attributes;

public readonly record struct Person
{
    public Person()
    {
        Name = string.Empty;
        Age = 0;
        Salary = 0.0;
    }

    [FixedColumn(0, 10)] public string Name { get; init; }
    [FixedColumn(10, 5)] public int Age { get; init; }
    [FixedColumn(15, 10)] public double Salary { get; init; }
}

start is the 0-based offset and length is the column width.

For source generation, make the model partial and implement one or both marker interfaces:

using FixedWidthParser;
using FixedWidthParser.Attributes;

public readonly partial record struct GeneratedPerson :
    IFixedWidthModel<GeneratedPerson>,
    IUtf8FixedWidthModel<GeneratedPerson>
{
    [FixedColumn(0, 10)] public string Name { get; init; }
    [FixedColumn(10, 5)] public int Age { get; init; }
    [FixedColumn(15, 10)] public double Salary { get; init; }
}

The generator emits distinct TryParse overloads for ReadOnlySpan<char> and ReadOnlySpan<byte> when both interfaces are present.

Parsing

Runtime Single-Line Parsing

using System.Globalization;
using FixedWidthParser.Parsers;

var parser = new FixedWidthParser<Person>();

if (parser.TryParse("John Doe  30   60000.00  ", CultureInfo.InvariantCulture, stringPool: null, out var person))
{
    // person.Name == "John Doe"
    // person.Age == 30
    // person.Salary == 60000.0
}

Source-Generated Single-Line Parsing

using System.Globalization;
using FixedWidthParser;

if (FixedWidth.TryParse<GeneratedPerson>(
        "John Doe  30   60000.00  ",
        CultureInfo.InvariantCulture,
        stringPool: null,
        out var person))
{
    // reflection-free generated parser
}

Reading Text Files

using System.Globalization;
using FixedWidthParser.Readers;

var reader = new FixedWidthReader<Person>(CultureInfo.InvariantCulture);

foreach (var person in reader.ReadFile("people.txt"))
{
    // ...
}

Read(TextReader) and Read(Stream, encoding, leaveOpen) are also available. Reading is lazy and reuses a pooled buffer; lines are sliced directly from the buffer, so the reader does not allocate a string per line. A malformed line throws a FormatException carrying the line number.

Empty lines (including a trailing newline at end of file) are skipped: they are counted toward the line number but not yielded as records. A line that is non-empty but shorter than the declared layout is treated as malformed and throws.

The source-generated facade has matching overloads:

foreach (var person in FixedWidth.ReadFile<GeneratedPerson>("people.txt", formatProvider: CultureInfo.InvariantCulture))
{
    // generated TryParse for each line
}

Async Reading

await foreach (var person in reader.ReadFileAsync("people.txt"))
{
    // ...
}

ReadAsync(TextReader) and ReadAsync(Stream, encoding, leaveOpen) mirror the synchronous overloads; ReadFileAsync uses true async file I/O. Cancellation is honored via WithCancellation.

UTF-8 Byte Parsing

For ASCII/single-byte fixed-width files, the UTF-8 APIs parse directly from bytes. This avoids StreamReader, avoids UTF-8 to UTF-16 transcoding, and keeps offsets measured in bytes.

using System.Globalization;
using FixedWidthParser.Readers;

var reader = new FixedWidthByteReader<Person>(CultureInfo.InvariantCulture);

foreach (var person in reader.ReadFile("people.txt"))
{
    // parsed from raw UTF-8 bytes
}

Generated UTF-8 models use FixedWidthUtf8:

using System.Globalization;
using FixedWidthParser;

if (FixedWidthUtf8.TryParse<GeneratedPerson>(
        "John Doe  30   60000.00  "u8,
        CultureInfo.InvariantCulture,
        stringPool: null,
        out var person))
{
    // generated byte parser
}

await using var stream = File.OpenRead("people.txt");
await foreach (var person in FixedWidthUtf8.ReadAsync<GeneratedPerson>(stream, formatProvider: CultureInfo.InvariantCulture))
{
    // async raw-byte streaming
}

Column offsets on the UTF-8 path are byte offsets. That is ideal for the ASCII-style payloads common in flat files; with multi-byte UTF-8 characters, byte offsets and character offsets are not the same.

The byte path supports the same StringPool interning as the char path: pass a pool to FixedWidthByteReader<T> / the FixedWidthUtf8 methods (the stringPool argument above) and string columns are interned through it; pass null to decode a fresh string per value.

Reading from a PipeReader

When the source is already a System.IO.Pipelines.PipeReader — a Kestrel request body, a socket, a named pipe, or an upstream pipeline stage — you can parse straight off it, letting the pipe own buffering and read-ahead. Both the reflection reader and the generated facade expose a PipeReader overload of ReadAsync:

using System.IO.Pipelines;
using System.Globalization;
using FixedWidthParser;
using FixedWidthParser.Readers;

// Reflection reader:
var reader = new FixedWidthByteReader<Person>(CultureInfo.InvariantCulture);
await foreach (var person in reader.ReadAsync(pipeReader))
{
    // parsed from the pipe
}

// Source-generated facade:
await foreach (var person in FixedWidthUtf8.ReadAsync<GeneratedPerson>(pipeReader, formatProvider: CultureInfo.InvariantCulture))
{
    // ...
}

Lines are sliced from the pipe's ReadOnlySequence<byte> and parsed in place when contiguous, copying into a pooled buffer only when a line spans segment boundaries. The same line semantics apply (BOM skipped once, \n/\r\n, empty lines skipped, trailing line without a newline yielded). By default the reader is completed when iteration ends; pass leaveOpen: true to leave it open.

When to use it. Prefer the PipeReader overload when you already hold a pipe. For plain files and streams the Stream/file overloads remain the faster default — a PipeReader adds per-read overhead that only pays off when there is real I/O to overlap (network, slow disk), not for in-memory or local-file sources.

Writing

using System.Globalization;
using FixedWidthParser.Writers;

var writer = new FixedWidthWriter<Person>();
var people = new[]
{
    new Person { Name = "John Doe", Age = 30, Salary = 60000 },
    new Person { Name = "Jane",     Age = 28, Salary = 55000 },
};

using var stream = File.Create("out.txt");
writer.WriteMany(stream, people.AsSpan(), CultureInfo.InvariantCulture);

Overloads cover Stream/StreamWriter with IEnumerable<T>/ReadOnlySpan<T>, plus WriteAsync and WriteManyAsync. Reusing a StreamWriter, or passing a span, keeps writing allocation-free per line.

Formatting Options

Each column can be tuned through named attribute arguments:

[FixedColumn(0, 8, Alignment = Alignment.Right, Padding = '0')] public int Id { get; init; }       // "00000042"
[FixedColumn(8, 10, Format = "F2")]                            public double Amount { get; init; } // "1234.50   "
[FixedColumn(18, 5, Overflow = OverflowBehavior.Truncate)]     public string Code { get; init; }
  • Alignment: Left (default) or Right.
  • Padding: fill character (default space; for example '0' for zero-padding).
  • Format: format string passed to ISpanFormattable (for example "F2" or "N0"); ignored for string.
  • Overflow: Default, Truncate or Throw. Default resolves per type: strings truncate, numeric types throw.

Culture Handling

Pass an IFormatProvider to TryParse, the reader constructor, the source-generated facade methods or the write methods. The generic path (ISpanParsable/ISpanFormattable) and the double/float processors honor it. When the provider is null, '.' is used as the decimal separator.

StringPool

Pass a CommunityToolkit.HighPerformance.Buffers.StringPool to intern repeated string-column values:

using System.Globalization;
using CommunityToolkit.HighPerformance.Buffers;
using FixedWidthParser.Readers;

var pool = new StringPool();
var reader = new FixedWidthReader<Person>(CultureInfo.InvariantCulture, stringPool: pool);

This is a time-vs-memory trade-off: pooling removes repeated string allocations but costs extra CPU for hashing and lookup. Prefer it for GC-sensitive or high-concurrency workloads; skip it for raw throughput.

Pooling applies to both the char and UTF-8 byte paths (FixedWidthReader<T>/FixedWidthByteReader<T> and the FixedWidth/FixedWidthUtf8 facades), and to ref struct models. When no pool is supplied, each string column is decoded into a fresh string.

Validation

Invalid layouts fail fast with an InvalidOperationException on the runtime parser/writer paths, or generator diagnostics on generated models. Negative Start, Length < 1, and overlapping columns are rejected. Adjacent columns are valid.

ref struct Models

The parser accepts ref struct models (where TModel : new(), allows ref struct), useful for stack-only row processing:

using FixedWidthParser.Attributes;
using FixedWidthParser.Parsers;

public ref struct Row
{
    public Row()
    {
        Name = string.Empty;
        Age = 0;
    }

    [FixedColumn(0, 10)] public string Name { get; set; }
    [FixedColumn(10, 5)] public int Age { get; set; }
}

var parser = new FixedWidthParser<Row>();
parser.TryParse(line, CultureInfo.InvariantCulture, null, out var row);

Batch readers and the writer use regular generic constraints because IEnumerable<T> cannot carry a ref struct.

Performance

Measured with BenchmarkDotNet (MemoryDiagnoser) on .NET 10. Highlights:

  • Parsing a line is allocation-light on the runtime path and reflection-free on the generated path.
  • Text readers avoid allocating a string per line by slicing a reusable buffer.
  • UTF-8 byte readers avoid StreamReader and transcoding for ASCII-style flat files.
  • Writing with StreamWriter reuse or ReadOnlySpan<T> is zero-alloc per line.

Run benchmarks:

dotnet run -c Release --project tests/Benchmarks/Benchmarks.csproj -- --filter "*ReaderBenchmarks*"

Benchmark reports are written to tests/Benchmarks/BenchmarkDotNet.Artifacts/results.

Project Layout

src/FixedWidthParser/                  The library
src/FixedWidthParser.Generator/        Roslyn source generator
tests/FixedWidthParser.Tests/          Runtime, reader, writer and parity tests
tests/FixedWidthParser.Generator.Tests/Source generator tests
tests/Benchmarks/                      BenchmarkDotNet benchmarks

Building and Testing

dotnet build FixedWidthParser.slnx -c Release
dotnet test tests/FixedWidthParser.Tests/FixedWidthParser.Tests.csproj
dotnet test tests/FixedWidthParser.Generator.Tests/FixedWidthParser.Generator.Tests.csproj
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 135 7/15/2026
1.1.0 125 6/15/2026
1.0.0 119 6/11/2026
1.0.0-preview1 111 6/9/2026