Wolfgang.Etl.FixedWidth 0.13.0

Prefix Reserved
dotnet add package Wolfgang.Etl.FixedWidth --version 0.13.0
                    
NuGet\Install-Package Wolfgang.Etl.FixedWidth -Version 0.13.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="Wolfgang.Etl.FixedWidth" Version="0.13.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Wolfgang.Etl.FixedWidth" Version="0.13.0" />
                    
Directory.Packages.props
<PackageReference Include="Wolfgang.Etl.FixedWidth" />
                    
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 Wolfgang.Etl.FixedWidth --version 0.13.0
                    
#r "nuget: Wolfgang.Etl.FixedWidth, 0.13.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 Wolfgang.Etl.FixedWidth@0.13.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=Wolfgang.Etl.FixedWidth&version=0.13.0
                    
Install as a Cake Addin
#tool nuget:?package=Wolfgang.Etl.FixedWidth&version=0.13.0
                    
Install as a Cake Tool

Wolfgang.Etl.FixedWidth

Extractor and Loader for reading and writing fixed width files and text streams

NuGet Downloads PR build release License: MIT .NET GitHub OpenSSF Scorecard


📦 Installation

dotnet add package Wolfgang.Etl.FixedWidth

NuGet Package: Wolfgang.Etl.FixedWidth on NuGet.org


📄 License

This project is licensed under the MIT License. See the LICENSE file for details.


📚 Documentation


🚀 Quick Start

Extraction — Reading a Fixed-Width File

Define a POCO with [FixedWidthField] attributes, then read records with await foreach:

using Wolfgang.Etl.FixedWidth;
using Wolfgang.Etl.FixedWidth.Attributes;
using Wolfgang.Etl.FixedWidth.Enums;

// 1. Define the record class.
public class PersonRecord
{
    [FixedWidthField(0, 10)]
    public string FirstName { get; set; } = string.Empty;



    [FixedWidthField(1, 10)]
    public string LastName { get; set; } = string.Empty;



    [FixedWidthField(2, 3, Alignment = FieldAlignment.Right, Pad = '0')]
    public int Age { get; set; }
}

// 2. Create the extractor (accepts any TextReader or Stream).
var reader = new StringReader
(
    "Alice     Anderson  025\n" +
    "Bob       Baker     042\n" +
    "Charlie   Clark     033"
);

var extractor = new FixedWidthExtractor<PersonRecord>(reader);

// 3. Iterate records asynchronously.
await foreach (var person in extractor.ExtractAsync(CancellationToken.None))
{
    Console.WriteLine($"{person.FirstName} {person.LastName}, Age {person.Age}");
}

// Output:
//   Alice Anderson, Age 25
//   Bob Baker, Age 42
//   Charlie Clark, Age 33

Loading — Writing a Fixed-Width File

var writer = new StringWriter();
var loader = new FixedWidthLoader<PersonRecord>(writer);

// LoadAsync accepts an IAsyncEnumerable<PersonRecord>; `sourceItems` is any
// async sequence of records (for example, the output of a FixedWidthExtractor).
await loader.LoadAsync(sourceItems, CancellationToken.None);

Console.WriteLine(writer.ToString());
// Output:
//   Alice     Anderson  025
//   Bob       Baker     042
//   Charlie   Clark     033

For file-based I/O, use the Stream constructor which creates a 64 KB buffered reader/writer for improved throughput:

// Extraction from a file
await using var readStream = File.OpenRead("people.dat");
using var extractor = new FixedWidthExtractor<PersonRecord>(readStream);

// Loading to a file
await using var writeStream = File.OpenWrite("output.dat");
using var loader = new FixedWidthLoader<PersonRecord>(writeStream);

Because the Stream constructors accept any Stream, compression works out of the box — wrap the file stream in a GZipStream or BrotliStream to read or write compressed fixed-width data (common for mainframe .gz exports) without a decompressed copy on disk:

// Extraction from a GZip-compressed file
await using var readStream = File.OpenRead("people.dat.gz");
await using var readGzip = new GZipStream(readStream, CompressionMode.Decompress);
using var extractor = new FixedWidthExtractor<PersonRecord>(readGzip);

// Loading to a GZip-compressed file
await using var writeStream = File.Create("output.dat.gz");
await using var writeGzip = new GZipStream(writeStream, CompressionLevel.Optimal);
using var loader = new FixedWidthLoader<PersonRecord>(writeGzip);

See the CompressedStreams example for a complete GZip and Brotli round trip.

Controlling line endings

FixedWidthExtractor reads any line ending automatically — \n, \r, or \r\n — so no configuration is needed for input.

For output, the loader writes each record with its TextWriter's newline. To force a specific ending regardless of the platform you run on — for example, a downstream mainframe or FTP consumer that requires Unix \n — pass a TextWriter with the NewLine you want:

// Force Unix (LF) line endings, even on Windows
await using var stream = File.Create("output.dat");
await using var writer = new StreamWriter(stream) { NewLine = "\n" };
using var loader = new FixedWidthLoader<PersonRecord>(writer);

await loader.LoadAsync(records, CancellationToken.None);

NewLine accepts any string ("\n", "\r\n", or a custom terminator). The default is Environment.NewLine.

Checkpoint and resume (large files)

Processing a multi-GB fixed-width export can take a while; if the pipeline crashes at record 5,000,000 you don't want to re-read from the top. Opt in to byte-offset tracking, persist CurrentByteOffset after each record, and resume from the saved position with StartByteOffset:

// First run — checkpoint after each record
await using var stream = File.OpenRead("huge.dat");
using var extractor = new FixedWidthExtractor<Record>(stream) { TrackByteOffset = true };
await foreach (var record in extractor.ExtractAsync(token))
{
    Process(record);
    SaveCheckpoint(extractor.CurrentByteOffset);   // byte position of the next unread line
}

// After a crash — seek straight to the next unread line, skipping everything already done
await using var stream = File.OpenRead("huge.dat");
using var extractor = new FixedWidthExtractor<Record>(stream) { StartByteOffset = LoadCheckpoint() };
await foreach (var record in extractor.ExtractAsync(token)) { /* only the remainder */ }

Terminators (\n, \r, \r\n), multi-byte UTF-8, and a leading byte-order mark are all counted exactly, so a saved offset is a precise byte position. Tracking is opt-in — it wraps the reader in a byte-counting decoder, so the default read path keeps its throughput — and requires the Stream constructor (seekable for resume). On resume, header lines are not re-skipped and SkipItemCount applies from the resumed position. CurrentLineNumber is available for diagnostics independently of checkpointing.

Inspecting the layout

FixedWidthSchema.For<T>() exposes the resolved field layout as a read-only view — useful for generating documentation, building validation tooling, or debugging a mapping. It applies the same validation as extraction, so an invalid layout (duplicate column index, a mapped field with no public setter) throws here too.

var schema = FixedWidthSchema.For<PersonRecord>();

foreach (var field in schema.Fields)   // includes skip columns (field.IsSkip)
{
    Console.WriteLine($"{field.StartPosition}-{field.EndPosition}  {field.Name}  ({field.Length})");
}

schema.ExpectedLineWidth;   // total line width, including skipped columns
schema.TotalColumnCount;    // columns including skips
schema.FieldCount;          // mapped fields only
schema.SkipCount;           // skipped columns

Each FixedWidthFieldInfo carries Name, StartPosition/EndPosition, Length, ColumnIndex, PropertyType, Alignment, Pad, Format, Header, and NumberStyles. Skipped columns have IsSkip == true and expose a SkipMessage instead of a name.

ToDiagram() renders the layout as a text table — drop it into a log line at startup or paste it into a ticket:

Console.WriteLine(FixedWidthSchema.For<EmployeeRecord>().ToDiagram());
Position  Field           Type    Length  Align  Pad  Format
--------  --------------  ------  ------  -----  ---  ------
0-9       FirstName       String  10      Left   ' '
10-17     [skip]                  8
18-23     EmployeeNumber  String  6       Left   ' '

Total width: 24  |  Columns: 3 (2 fields + 1 skip)  |  Delimiter: none

Defining the layout in code

When you can't decorate the record type — a third-party POCO, or a layout chosen at runtime — build the schema with FixedWidthSchemaBuilder<T> instead of attributes, then hand it to the extractor or loader via its Schema property:

var schema = new FixedWidthSchemaBuilder<CustomerRecord>()
    .Field(r => r.CustomerId, index: 0, length: 8)
    .Field(r => r.Name, index: 1, length: 30)
    .Skip(index: 2, length: 5)
    .Field(r => r.Balance, index: 3, length: 9, alignment: FieldAlignment.Right, format: "0000000.00")
    .Build();

using var extractor = new FixedWidthExtractor<CustomerRecord>(reader) { Schema = schema };

The builder uses lambda expressions for type-safe, refactor-proof property references — no magic strings. index is the zero-based column ordinal (the same value as [FixedWidthField(index, length)]); start positions are computed from the column lengths. A schema built this way is equivalent to one resolved from attributes: it validates the same way (duplicate index, no public setter) and is fully introspectable via Fields / ToDiagram(). Setting Schema overrides any attributes on the type.

See the SchemaBuilder example for a runnable walk-through.

Reading and writing binary / mainframe records

Mainframe (COBOL) files mix text with binary-encoded numeric fields — COMP big-endian integers and COMP-3 packed decimals — and are not newline-delimited: every record is a fixed number of bytes, written back-to-back. Because packed-decimal and binary bytes routinely contain 0x0A/0x0D, they must never be split on newlines. FixedWidthBinaryExtractor<T> / FixedWidthBinaryLoader<T> read and write these records by byte count, decoding/encoding each field per its declared type.

Declare the layout with [FixedWidthBinaryField] — widths are in bytes, and BinaryFieldType selects the decoding (Text, Binary, or PackedDecimal):

using Wolfgang.Etl.FixedWidth;
using Wolfgang.Etl.FixedWidth.Attributes;
using Wolfgang.Etl.FixedWidth.Enums;

public class AccountRecord
{
    [FixedWidthBinaryField(0, 8, BinaryFieldType.Text)]
    public string AccountId { get; set; } = string.Empty;

    [FixedWidthBinaryField(1, 4, BinaryFieldType.Binary)]              // COMP
    public int TransactionCount { get; set; }

    [FixedWidthBinaryField(2, 5, BinaryFieldType.PackedDecimal, Scale = 2)]   // PIC S9(7)V99 COMP-3
    public decimal Balance { get; set; }
}

await using var stream = File.OpenRead("accounts.dat");
using var extractor = new FixedWidthBinaryExtractor<AccountRecord>(stream);
await foreach (var account in extractor.ExtractAsync(CancellationToken.None))
{
    // account.Balance decoded from COMP-3, account.TransactionCount from COMP
}

Text fields decode with the extractor/loader's encoding — ASCII by default. For EBCDIC data, register the code-page provider and pass the encoding (the System.Text.Encoding.CodePages package is only needed by consumers who use it, so it is not bundled):

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
using var extractor = new FixedWidthBinaryExtractor<AccountRecord>(stream, Encoding.GetEncoding("IBM037"));

Writing is symmetric — FixedWidthBinaryLoader<T> encodes each field back to its COMP/COMP-3/text bytes. See the BinaryRecords example for a runnable write → read round trip.

Transforming between layouts

To reformat a fixed-width file from one layout to another — reordering, adding/removing, or format-converting fields (a common mainframe-migration task) — FixedWidthTransformer<TSource, TDestination> is the projection stage between an extractor and a loader:

using var extractor   = new FixedWidthExtractor<LegacyRecord>(sourceReader);
using var transformer = new FixedWidthTransformer<LegacyRecord, ModernRecord>(
    legacy => new ModernRecord
    {
        Id   = legacy.OldId,
        Name = legacy.FullName.Trim(),
    });
using var loader      = new FixedWidthLoader<ModernRecord>(destinationWriter);

// Extract → transform → load in a single streaming pass.
var modern = transformer.TransformAsync(extractor.ExtractAsync(token), token);
await loader.LoadAsync(modern, token);

The projection delegate handles every reformatting case. When source and destination differ only in layout — the same property names and compatible types — use the auto-mapping factory instead of writing the copy by hand:

using var transformer = FixedWidthTransformer<LegacyRecord, ModernRecord>.ByMatchingProperties();

ByMatchingProperties() copies every source property to the destination property of the same name and an assignable type, and requires a public parameterless constructor on the destination.

Reading files with multiple record types

Mainframe and EDI batch files often interleave several record layouts on different lines — a header, many detail rows, and a trailer — distinguished by a discriminator character. FixedWidthMultiRecordExtractor routes each line to the right POCO: register one rule per type, and the first matching predicate wins.

using var extractor = new FixedWidthMultiRecordExtractor(reader)
    .When(line => line[0] == 'H', typeof(HeaderRecord))
    .When(line => line[0] == 'D', typeof(DetailRecord))
    .When(line => line[0] == 'T', typeof(TrailerRecord));

await foreach (var record in extractor.ExtractAsync(token))
{
    switch (record)
    {
        case HeaderRecord h: /* ... */ break;
        case DetailRecord d: /* ... */ break;
        case TrailerRecord t: /* ... */ break;
    }
}

From a file or other Stream, the encoding travels on the options record (the TextReader form above already knows its encoding, so it takes no record):

using var extractor = new FixedWidthMultiRecordExtractor
(
    File.OpenRead("batch.txt"),
    new FixedWidthMultiRecordExtractorOptions { Encoding = Encoding.Latin1 }
)
    .When(line => line[0] == 'H', typeof(HeaderRecord))
    .When(line => line[0] == 'D', typeof(DetailRecord))
    .When(line => line[0] == 'T', typeof(TrailerRecord));

Each record type keeps its own independent [FixedWidthField] layout. A line that matches no rule throws by default; set UnmatchedLineHandling = UnmatchedLineHandling.Skip to drop it, or register a catch-all type with .Otherwise(typeof(UnknownRecord)). Blank lines are skipped before predicates run (so a discriminator can index the line safely), and the extractor shares the family's HeaderLineCount, FieldDelimiter, ValueParser, SkipItemCount/MaximumItemCount, dead-letter OnError, and progress reporting.

Trailer record-count validation falls out of this naturally: capture the trailer as it streams past, count the details, and compare — no extra API needed.

if (trailer.RecordCount != detailCount)
    throw new InvalidDataException($"Trailer says {trailer.RecordCount}, file has {detailCount}.");

See the MultiRecordTrailer example for a runnable header/detail/trailer walk-through that checks both the record count and a control total.

Composing an ETL pipeline

Rather than wiring an extractor, transformer, and loader together by hand, the whole extract → transform → load flow can be expressed as one fluent chain on the generic EtlPipeline (introduced in Wolfgang.Etl.Abstractions 0.16.0; this package builds against the version in its NuGet dependency). FixedWidthExtractor<T> source factories hang off EtlPipeline.Create() and FixedWidthLoader<T> sink terminators hang off the pipeline, with the extractor/loader configuration exposed as inline setters:

using Wolfgang.Etl.Abstractions;
using Wolfgang.Etl.FixedWidth;

// Fixed-width in, human-readable table out — path factories own the files they open.
await EtlPipeline
    .Create()
    .FixedWidthExtractor<PersonRecord>("people.dat")
    .FixedWidthLoader<PersonRecord>("people.txt")
    .WriteHeader(true)
    .FieldSeparator('-')
    .FieldDelimiter(" | ")
    .RunAsync();

Insert transform stages with Through — an inline Func<IAsyncEnumerable<T>, IAsyncEnumerable<TOut>> stage needs no reference to the operators package:

await EtlPipeline
    .Create()
    .FixedWidthExtractor<PersonRecord>(sourceReader)
    .Through(KeepAdults)                 // a stream-to-stream transform delegate
    .FixedWidthLoader<PersonRecord>(destinationWriter)
    .RunAsync();

Every source and sink has path, Stream, and TextReader/TextWriter overloads (plus an existing-FixedWidthExtractor<T> overload). Path factories own the file stream they open and dispose it when the run finishes, on success or failure; caller-supplied streams, readers, and writers are always left open. The builder methods (HeaderLineCount, MalformedLineHandling, FieldDelimiter, Encoding, WriteHeader, ValueConverter, IsDryRun, …) map 1:1 to the FixedWidthExtractor<T> / FixedWidthLoader<T> properties.

See the PipelineExtensions example for a complete, runnable walk-through.

Metrics and observability

The extractor and loader can emit standard System.Diagnostics.Metrics instruments from the meter Wolfgang.Etl.FixedWidth, so throughput and error rates flow to OpenTelemetry, Prometheus, Grafana, Application Insights, or any MeterListener. Metrics are zero-config: they activate automatically when a listener subscribes to the Wolfgang.Etl.FixedWidth meter — there's no flag to set. When nothing is listening, the extract/load loop (sampling once per operation) runs no metric code at all, so telemetry adds zero overhead for callers that don't use it.

Instrument Type Description
wolfgang.etl.fixedwidth.items.extracted Counter Items successfully extracted
wolfgang.etl.fixedwidth.items.loaded Counter Items successfully loaded
wolfgang.etl.fixedwidth.items.skipped Counter Items skipped via the skip budget
wolfgang.etl.fixedwidth.lines.read Counter Physical lines read (including blank/skipped)
wolfgang.etl.fixedwidth.operation.duration Histogram (ms) Duration of an extract/load operation

Every measurement is tagged etl.operation (extract or load) and etl.record_type (typeof(TRecord).Name).

// Subscribe once at startup — OpenTelemetry, zero per-call code; metrics turn on automatically:
builder.Services.AddOpenTelemetry()
    .WithMetrics(m => m.AddMeter("Wolfgang.Etl.FixedWidth"));

See the Metrics example for a runnable MeterListener walk-through.


✨ Features

Feature Description
Attribute-based field mapping [FixedWidthField(index, length)] maps properties to columns by index and width
Skip columns [FixedWidthSkip(index, length)] declares columns in the file that are not mapped to any property
Alignment and padding Alignment = FieldAlignment.Left\|Right with configurable Pad character (default space)
Custom parsing ValueParser delegate on the extractor for custom extraction logic per field
Custom conversion ValueConverter delegate on the loader for custom write formatting per field
Header rows HasHeader / HeaderLineCount (extractor) and WriteHeader (loader)
Separator lines FieldSeparator character for visual separator lines between headers and data
Field delimiters FieldDelimiter string (e.g. " \| ") inserted between fields for human-readable output
Pagination SkipItemCount and MaximumItemCount for skipping and limiting records
Blank line handling BlankLineHandling — ThrowException, Skip, or ReturnDefault
Malformed line handling MalformedLineHandling — ThrowException, Skip, or ReturnDefault
Line filtering LineFilter delegate for custom line-level control (Process, Skip, Stop)
Progress reporting Timer-based IProgress<T> reporting via FixedWidthReport (includes CurrentLineNumber)
Checkpoint / resume TrackByteOffset + CurrentByteOffset / StartByteOffset — persist a byte-offset checkpoint per record and resume a crashed run without re-reading the file
Zero-copy parsing ReadOnlyMemory<char> slicing avoids string allocations during field extraction
Span-based numerics Span<char>-based numeric parsing on net8.0+ for reduced allocation
Source-generated accessors A bundled Roslyn generator emits direct-access factory/getter/setter delegates for [FixedWidthField] types at compile time — no reflection, no Expression.Compile, Native AOT & trimming compatible; falls back to compiled delegates on net462/netstandard2.0
Schema introspection FixedWidthSchema.For<T>() exposes the resolved layout (positions, widths, types, skips); ToDiagram() renders it as a text table
Code-defined layout FixedWidthSchemaBuilder<T> defines a layout in fluent, type-safe code (no attributes required); assign it to the extractor/loader Schema property
Binary / mainframe FixedWidthBinaryExtractor<T> / FixedWidthBinaryLoader<T> read/write fixed-length binary records with COBOL COMP / COMP-3 fields via [FixedWidthBinaryField]
Format transformation FixedWidthTransformer<TSource, TDestination> projects one layout to another in a single streaming pass, with optional ByMatchingProperties() auto-mapping
Multi-record-type files FixedWidthMultiRecordExtractor routes each line to a different POCO by a discriminator predicate (.When(…) / .Otherwise(…)), for header/detail/trailer batch files
Pipeline composition EtlPipeline.Create().FixedWidthExtractor<T>(…).FixedWidthLoader<T>(…).RunAsync() — fluent source factories and sink terminators over the generic EtlPipeline (introduced in Wolfgang.Etl.Abstractions 0.16.0)
Metrics Zero-config System.Diagnostics.Metrics instruments (throughput, skips, duration) from the Wolfgang.Etl.FixedWidth meter — OpenTelemetry / Prometheus / any MeterListener
Multi-TFM support net462, net481, netstandard2.0, net5.0, net6.0, net7.0, net8.0, net10.0

Examples:

The examples/ folder contains 16 runnable console projects demonstrating each feature:

Example Description
BasicExtraction Read fixed-width data into strongly typed records
BasicLoading Write records to fixed-width output
CompressedStreams Read and write GZip / Brotli compressed fixed-width data
RoundTrip Extract, transform, and reload records end-to-end
CustomParsersConverters Custom ValueParser and ValueConverter delegates
ProgressReporting Timer-based IProgress<FixedWidthReport> callbacks
ErrorHandling BlankLineHandling, MalformedLineHandling, and LineFilter
FieldDelimiter Delimited output (e.g. " \| ") for human-readable tables
SkipAndMax SkipItemCount and MaximumItemCount for pagination
HeadersAndSeparators WriteHeader, HasHeader, and FieldSeparator
PipelineExtensions Compose extract → transform → load as one EtlPipeline fluent chain
Metrics Subscribe to the Wolfgang.Etl.FixedWidth meter and read throughput/duration metrics
SchemaBuilder Define a layout in code with FixedWidthSchemaBuilder<T> instead of attributes
DataReader Expose a fixed-width source as an IDataReader for SqlBulkCopy / DataTable (no POCO per row)
BinaryRecords Read/write fixed-length binary (mainframe) records with COMP-3 / COMP fields
MultiRecordTrailer Route header/detail/trailer records with FixedWidthMultiRecordExtractor and validate the trailer's record count and control total

Compile-time diagnostics:

The package ships a Roslyn analyzer that catches [FixedWidthField] layout mistakes in the IDE and the build — no configuration required:

ID Severity Flags
FW003 Error Two columns declare the same Index (field mapping throws at runtime)
FW004 Warning A DateTime / DateTimeOffset / TimeSpan field with no Format (parsing and writing throw)
FW005 Warning A Format pattern wider than the field length (the value overflows on write)
FW007 Warning A mapped property with no public setter (extraction throws)
FW008 Info A mapped property with no public getter (loading throws)

🎯 Supported Frameworks

This library targets:

  • .NET Framework: 4.6.2, 4.8.1
  • .NET Standard: 2.0
  • .NET: 5.0, 6.0, 7.0, 8.0, 10.0

The CI test matrix additionally exercises the library on .NET Framework 4.7.x/4.8, .NET Core 3.1 and .NET 9.0 via the nearest package asset; those are tested-against runtimes, not package target frameworks.

See the NuGet package page for the authoritative per-TFM compatibility matrix.

🔍 Code Quality & Static Analysis

This project enforces strict code quality standards through 8 specialized analyzers and custom async-first rules:

Analyzers in Use

  1. Microsoft.CodeAnalysis.NetAnalyzers - Built-in .NET analyzers for correctness and performance
  2. Roslynator.Analyzers - Advanced refactoring and code quality rules
  3. AsyncFixer - Async/await best practices and anti-pattern detection
  4. Microsoft.VisualStudio.Threading.Analyzers - Thread safety and async patterns
  5. Microsoft.CodeAnalysis.BannedApiAnalyzers - Prevents usage of banned synchronous APIs
  6. Meziantou.Analyzer - Comprehensive code quality rules
  7. SonarAnalyzer.CSharp - Industry-standard code analysis
  8. Microsoft.CodeAnalysis.PublicApiAnalyzers - Tracks the shipped public surface (RS0016/RS0017)

Async-First Enforcement

This library uses BannedSymbols.txt to prohibit synchronous APIs and enforce async-first patterns:

Blocked APIs Include:

  • ❌ Task.Wait(), Task.Result - Use await instead
  • ❌ Thread.Sleep() - Use await Task.Delay() instead
  • ❌ Synchronous file I/O (File.ReadAllText) - Use async versions
  • ❌ Synchronous stream operations - Use ReadAsync(), WriteAsync()
  • ❌ Parallel.For/ForEach - Use Task.WhenAll() or Parallel.ForEachAsync()
  • ❌ Obsolete APIs (WebClient, BinaryFormatter)

Why? To ensure all code is truly async and non-blocking for optimal performance in async contexts.


🛠️ Building from Source

Prerequisites

Build Steps

# Clone the repository
git clone https://github.com/Chris-Wolfgang/ETL-FixedWidth.git
cd ETL-FixedWidth

# Restore dependencies
dotnet restore

# Build the solution
dotnet build --configuration Release

# Run tests
dotnet test --configuration Release

# Run code formatting (PowerShell Core)
pwsh ./scripts/format.ps1

Code Formatting

This project uses .editorconfig and dotnet format:

# Format code
dotnet format

# Verify formatting (as CI does)
dotnet format --verify-no-changes

See README-FORMATTING.md for detailed formatting guidelines.

Building Documentation

This project uses DocFX to generate API documentation:

# Install DocFX (one-time setup)
dotnet tool install -g docfx

# Generate API metadata and build documentation
cd docfx_project
docfx metadata  # Extract API metadata from source code
docfx build     # Build HTML documentation

# Documentation is generated in the docs/ folder at the repository root

The documentation is automatically built and deployed to GitHub Pages when changes are pushed to the main branch.

Local Preview:

# Serve documentation locally (with live reload)
cd docfx_project
docfx build --serve

# Open http://localhost:8080 in your browser

Documentation Structure:

  • docfx_project/ - DocFX configuration and source files
  • docs/ - Generated HTML documentation (published to GitHub Pages)
  • docfx_project/index.md - Main landing page content
  • docfx_project/docs/ - Additional documentation articles
  • docfx_project/api/ - Auto-generated API reference YAML files

🔐 Verify the build

The library is built deterministically, so you can rebuild the exact assemblies from the tagged source and confirm a NuGet release was built from that source and nothing else. Every GitHub release attaches a reproducible-build-manifest.json with the expected per-framework assembly hashes and the toolchain that produced them.

# Download the manifest for a release, rebuild at the tag, and compare hashes.
gh release download v0.8.0 --repo Chris-Wolfgang/ETL-FixedWidth --pattern reproducible-build-manifest.json
git clone --depth 1 --branch v0.8.0 https://github.com/Chris-Wolfgang/ETL-FixedWidth
dotnet build ETL-FixedWidth/src/Wolfgang.Etl.FixedWidth/Wolfgang.Etl.FixedWidth.csproj -c Release -p:ContinuousIntegrationBuild=true
find ETL-FixedWidth/src/Wolfgang.Etl.FixedWidth/bin/Release -name 'Wolfgang.Etl.FixedWidth.dll' -exec sha256sum {} \;

See docs/REPRODUCIBLE-BUILD.md for the full procedure — which SDK version to use, how to file a discrepancy, and how to publish a third-party verification attestation.


🤝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md for:

  • Code quality standards
  • Build and test instructions
  • Pull request guidelines
  • Analyzer configuration details

🙏 Acknowledgments

  • Wolfgang.Etl.Abstractions — provides the ExtractorBase, LoaderBase, and TransformerBase base classes, progress reporting infrastructure, and the IProgressTimer contract that this library builds on.
  • Microsoft.Extensions.Logging.Abstractions — provides the ILogger interface used for optional structured diagnostic logging throughout the extractor and loader.
Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
0.13.0 444 9/23/2026
0.12.0 1,945 9/17/2026
0.11.0 687 8/27/2026
0.10.1 441 8/19/2026
0.10.0 344 8/14/2026
0.9.0 202 8/13/2026
0.8.0 564 8/9/2026
0.7.0 218 7/24/2026
0.6.0 700 7/20/2026
0.5.1 201 7/18/2026
0.5.0 247 7/16/2026
0.4.0 125 7/15/2026
0.3.0 124 7/14/2026
0.2.3 124 7/12/2026
0.2.2 129 6/27/2026
0.2.1 134 5/9/2026
0.2.0 130 4/28/2026
0.1.0 142 3/24/2026
0.0.0 155 3/17/2026