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
<PackageReference Include="Wolfgang.Etl.FixedWidth" Version="0.13.0" />
<PackageVersion Include="Wolfgang.Etl.FixedWidth" Version="0.13.0" />
<PackageReference Include="Wolfgang.Etl.FixedWidth" />
paket add Wolfgang.Etl.FixedWidth --version 0.13.0
#r "nuget: Wolfgang.Etl.FixedWidth, 0.13.0"
#:package Wolfgang.Etl.FixedWidth@0.13.0
#addin nuget:?package=Wolfgang.Etl.FixedWidth&version=0.13.0
#tool nuget:?package=Wolfgang.Etl.FixedWidth&version=0.13.0
Wolfgang.Etl.FixedWidth
Extractor and Loader for reading and writing fixed width files and text streams
📦 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
- GitHub Repository: https://github.com/Chris-Wolfgang/ETL-FixedWidth
- API Documentation: https://Chris-Wolfgang.github.io/ETL-FixedWidth/
- Formatting Guide: README-FORMATTING.md
- Contributing Guide: CONTRIBUTING.md
🚀 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
- Microsoft.CodeAnalysis.NetAnalyzers - Built-in .NET analyzers for correctness and performance
- Roslynator.Analyzers - Advanced refactoring and code quality rules
- AsyncFixer - Async/await best practices and anti-pattern detection
- Microsoft.VisualStudio.Threading.Analyzers - Thread safety and async patterns
- Microsoft.CodeAnalysis.BannedApiAnalyzers - Prevents usage of banned synchronous APIs
- Meziantou.Analyzer - Comprehensive code quality rules
- SonarAnalyzer.CSharp - Industry-standard code analysis
- 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- Useawaitinstead - ❌
Thread.Sleep()- Useawait Task.Delay()instead - ❌ Synchronous file I/O (
File.ReadAllText) - Use async versions - ❌ Synchronous stream operations - Use
ReadAsync(),WriteAsync() - ❌
Parallel.For/ForEach- UseTask.WhenAll()orParallel.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
- .NET 10.0 SDK or later (required for the
net10.0target framework) - Optional: PowerShell Core for formatting scripts
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 filesdocs/- Generated HTML documentation (published to GitHub Pages)docfx_project/index.md- Main landing page contentdocfx_project/docs/- Additional documentation articlesdocfx_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, andTransformerBasebase classes, progress reporting infrastructure, and theIProgressTimercontract that this library builds on. - Microsoft.Extensions.Logging.Abstractions — provides the
ILoggerinterface used for optional structured diagnostic logging throughout the extractor and loader.
| Product | Versions 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. |
-
.NETFramework 4.6.2
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- System.Diagnostics.DiagnosticSource (>= 10.0.12)
- System.Memory (>= 4.6.3)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
.NETFramework 4.8.1
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- System.Diagnostics.DiagnosticSource (>= 10.0.12)
- System.Memory (>= 4.6.3)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- System.Diagnostics.DiagnosticSource (>= 10.0.12)
- System.Memory (>= 4.6.3)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net10.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net5.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- System.Diagnostics.DiagnosticSource (>= 10.0.12)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net6.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- System.Diagnostics.DiagnosticSource (>= 10.0.12)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net7.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- System.Diagnostics.DiagnosticSource (>= 10.0.12)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net8.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- System.Diagnostics.DiagnosticSource (>= 10.0.12)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
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 |