Wolfgang.Etl.Xml 0.8.1

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

Wolfgang.Etl.Xml

Extractors and loaders for working with XML files using the Wolfgang.Etl design pattern

NuGet Downloads PR build release License: MIT .NET GitHub


πŸ“¦ Installation

dotnet add package Wolfgang.Etl.Xml

πŸ“„ License

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


πŸ“š Documentation


πŸš€ Quick Start

Extract items from an XML stream through a full ETL pipeline:

using Wolfgang.Etl.TestKit;
using Wolfgang.Etl.Xml;

// Extract from XML β†’ Transform β†’ Load into memory
var extractor = new XmlSingleStreamExtractor<Person>(xmlStream);

var transformer = new TestTransformer<Person>();
var loader = new TestLoader<Person>(collectItems: true);

await loader.LoadAsync(transformer.TransformAsync(extractor.ExtractAsync()));

var items = loader.GetCollectedItems();

Load items to an XML stream from an in-memory source:

// Extract from memory β†’ Transform β†’ Load to XML
var extractor = new TestExtractor<Person>(people);
var transformer = new TestTransformer<Person>();

var loader = new XmlSingleStreamLoader<Person>(outputStream);

await loader.LoadAsync(transformer.TransformAsync(extractor.ExtractAsync()));

Fluent pipeline (EtlPipeline)

For end-to-end wiring, the XML source and sink factories plug straight into the EtlPipeline chain β€” no explicit extractor, transformer, or loader variables. The factories read the same as the CSV/JSON siblings:

using Wolfgang.Etl.Abstractions;
using Wolfgang.Etl.Xml;

// Read a single-root XML file β†’ write a single-root XML file.
await EtlPipeline
    .Create()
    .XmlSingleStreamExtractor<Person>("people.xml")
    .XmlSingleStreamLoader<Person>("people-copy.xml")
    .RunAsync();

Insert a Through stage to transform or filter records mid-stream:

await EtlPipeline
    .Create()
    .XmlSingleStreamExtractor<Person>("people.xml")
    .Through<Person>(people => people.Where(p => p.Age >= 18))
    .XmlSingleStreamLoader<Person>("adults.xml")
    .RunAsync(progress, cancellationToken);

Mix and match the single- and multi-stream shapes β€” e.g. fan a single XML document out to one file per record:

await EtlPipeline
    .Create()
    .XmlSingleStreamExtractor<Person>(sourceStream)
    .XmlMultiStreamLoader<Person>(person => File.Create($"{person.LastName}.xml"))
    .RunAsync();

…or fan the mirror direction β€” merge many single-document XML files back into one root document:

await EtlPipeline
    .Create()
    .XmlMultiStreamExtractor<Person>(Directory.EnumerateFiles("inbox", "*.xml").Select(File.OpenRead))
    .XmlSingleStreamLoader<Person>("people.xml")
    .RunAsync();

Stream ownership: path-based factories own the file stream they open and close it when the run finishes β€” on success and failure. Stream-based factories leave the caller's stream alone (honouring XmlSingleStream…Options.LeaveOpen), so the caller controls its lifetime.

Compressed streams (.xml.gz)

Every extractor and loader works against a plain Stream, so compression is transparent β€” wrap the underlying stream in a GZipStream (or any System.IO.Compression codec):

using System.IO.Compression;

// Write gzip-compressed XML. LeaveOpen = false lets the loader dispose the
// GZipStream when the load completes, flushing the gzip footer.
using (var file = File.Create("people.xml.gz"))
using (var gzip = new GZipStream(file, CompressionMode.Compress))
{
    var loader = new XmlSingleStreamLoader<Person>(gzip);
    await loader.LoadAsync(people);
}

// Read it back β€” decompress on the way in.
using (var file = File.OpenRead("people.xml.gz"))
using (var gunzip = new GZipStream(file, CompressionMode.Decompress))
{
    var extractor = new XmlSingleStreamExtractor<Person>(gunzip);
    await foreach (var person in extractor.ExtractAsync())
    {
        // ...
    }
}

See the runnable CompressedStreamRoundTripAsync example in examples/Wolfgang.Etl.Xml.Examples.

XSD validation during extraction

XmlSingleStreamExtractor<T> accepts a custom XmlReaderSettings (and clones it before use), so an XSD is validated as the document is read β€” no extra pass:

using System.Xml;
using System.Xml.Schema;

var schemas = new XmlSchemaSet();
schemas.Add(targetNamespace: null, "person.xsd");

var settings = new XmlReaderSettings
{
    ValidationType = ValidationType.Schema,
    Schemas = schemas,
};

var extractor = new XmlSingleStreamExtractor<Person>(stream, settings, logger);
await foreach (var person in extractor.ExtractAsync())
{
    // only schema-valid records reach here
}

A schema violation surfaces from ExtractAsync as an InvalidOperationException whose InnerException is the XmlSchemaValidationException (with the offending line and reason) β€” XmlSerializer wraps the reader's validation error. See the runnable XsdValidationAsync example in examples/Wolfgang.Etl.Xml.Examples.

Customizing the serialized XML

Both the extractors and loaders build an XmlSerializer from your record type (new XmlSerializer(typeof(T))) β€” loaders to serialize, extractors to deserialize β€” so they honour the standard System.Xml.Serialization attributes on your record type β€” element/attribute names, ignored members, the root element, collection shaping, and namespaces:

[XmlRoot("Employee")]
public sealed class Person
{
    [XmlElement("FullName")]          // <FullName> instead of <FirstName>
    public string FirstName { get; set; } = "";

    [XmlAttribute("years")]           // age as an attribute: <Employee years="30">
    public int Age { get; set; }

    [XmlIgnore]                        // never serialized
    public string InternalId { get; set; } = "";
}

Attribute-free customization (XmlAttributeOverrides) is not currently supported. Because the serializer is constructed from the type alone, there is no hook to inject an XmlAttributeOverrides instance. If you don't own the type or want to keep it attribute-free, project it onto a small DTO you do control (and decorate that), or open an issue for first-class XmlAttributeOverrides support.

Per-item error handling & dead-lettering

By default a stream/record that fails to deserialize or serialize aborts the run. On the multi-stream classes β€” where each stream is an independent record β€” you can instead skip or dead-letter the failed record and keep going by assigning an ErrorPolicy (inherited from the Abstractions base stages). Ready-made policies live in the Wolfgang.Etl.ErrorPolicies package:

using Wolfgang.Etl.ErrorPolicies;

var deadLetters = new List<ItemErrorContext>();

var extractor = new XmlMultiStreamExtractor<Person>(streams)
{
    // Skip / SkipAndLog(logger) / SkipAndDeadLetter(...) / SkipDeadLetterAndLog(...)
    ErrorPolicy = ItemErrorPolicy.SkipAndDeadLetter(deadLetters),
};

await foreach (var person in extractor.ExtractAsync())
{
    // only successfully-deserialized records reach here
}

// extractor.CurrentErrorItemCount == deadLetters.Count;
// each ItemErrorContext carries the 1-based item number and the exception.

The dead-letter policies are also overloaded for a System.Threading.Channels.ChannelWriter<ItemErrorContext>. See the runnable ErrorPolicyDeadLetterAsync example in examples/Wolfgang.Etl.Xml.Examples.

Single-stream classes keep fail-fast semantics. XmlSingleStreamExtractor<T> and XmlSingleStreamLoader<T> read/write one shared streaming document, which cannot resume mid-record after a partial failure, so a bad record aborts the run. Use the multi-stream variants for per-record error capture.


Metrics & observability

Every extractor and loader emits System.Diagnostics.Metrics measurements to the Wolfgang.Etl.Xml meter β€” no configuration required, and zero measurable overhead when nothing is listening. Point OpenTelemetry (or any MeterListener) at the meter to get throughput, skip/error rates, and operation latency:

using var meterProvider = Sdk.CreateMeterProviderBuilder()
    .AddMeter("Wolfgang.Etl.Xml")
    .AddPrometheusExporter()
    .Build();
Instrument Type Description
wolfgang.etl.xml.items.extracted Counter Items successfully extracted
wolfgang.etl.xml.items.loaded Counter Items successfully loaded
wolfgang.etl.xml.items.skipped Counter Items skipped by the skip budget
wolfgang.etl.xml.items.errored Counter Items skipped / dead-lettered by the error policy
wolfgang.etl.xml.operation.duration Histogram (ms) Duration of an extract / load operation

Every measurement is tagged with etl.operation (extract / load), etl.component (XmlSingleStream / XmlMultiStream), and etl.record_type.


✨ Features

Feature Description
Single-stream XML Read/write multiple items from/to a single XML document with a root element wrapper
Multi-stream XML Read/write one item per XML stream (one file per record)
Streaming deserialization Uses XmlReader for memory-efficient forward-only parsing
Progress reporting Built-in IProgress<XmlReport> support with configurable reporting intervals
Skip and maximum SkipItemCount and MaximumItemCount for paging through large XML sources
Custom XML settings Accept XmlReaderSettings and XmlWriterSettings for full control over XML behavior
Compressed streams Works over any Stream, so gzip/deflate/Brotli is transparent β€” wrap in GZipStream for .xml.gz
Structured logging High-performance LoggerMessage-based logging with categorized event IDs
Multi-TFM Targets .NET Framework 4.6.2+, .NET Standard 2.0, .NET 8.0, and .NET 10.0

Extractors

  • XmlSingleStreamExtractor<T> β€” Extracts items from a single XML stream containing a root element with child elements (e.g. <ArrayOfPerson><Person/>...</ArrayOfPerson>).
  • XmlMultiStreamExtractor<T> β€” Extracts items from multiple XML streams, one document per stream.

Loaders

  • XmlSingleStreamLoader<T> β€” Loads items into a single XML stream wrapped in a root element.
  • XmlMultiStreamLoader<T> β€” Loads items into multiple XML streams via a factory function, one document per stream.
  • XmlReport β€” Progress report returned via IProgress<XmlReport>. Properties: CurrentItemCount, CurrentSkippedItemCount.

EtlPipeline factories

Class-named factories over the fluent EtlPipeline chain, so XML sources and sinks compose without hand-wiring an extractor/loader:

  • XmlSingleStreamExtractor<T>(path) / (stream, options?) β€” seeds a pipeline from a single-root XML source. The path overload owns and closes the file stream.
  • XmlMultiStreamExtractor<T>(streams) β€” seeds a pipeline from a sequence of single-document XML streams (one record each).
  • XmlSingleStreamLoader<T>(path, options?) / (stream, options?) β€” terminates a pipeline into a single-root XML document. The path overload owns and closes the file stream.
  • XmlMultiStreamLoader<T>(streamFactory) β€” terminates a pipeline, writing one XML document per record to a per-record stream.

Constructor overloads

Each extractor and loader provides two public constructors (the first parameter varies by type):

  • XmlSingleStreamExtractor<T> / XmlSingleStreamLoader<T> β€” (stream) or (stream, settings, logger)
  • XmlMultiStreamExtractor<T> β€” (streams) or (streams, settings, logger) where streams is IEnumerable<Stream>
  • XmlMultiStreamLoader<T> β€” (streamFactory) or (streamFactory, settings, logger) where streamFactory is Func<T, Stream>

Progress reporting

var extractor = new XmlSingleStreamExtractor<Person>(xmlStream);
extractor.ReportingInterval = 100; // Report every 100ms

var progress = new Progress<XmlReport>(report =>
    Console.WriteLine($"Progress: {report.CurrentItemCount} items, {report.CurrentSkippedItemCount} skipped")
);

var transformer = new TestTransformer<Person>();
var loader = new TestLoader<Person>(collectItems: true);

await loader.LoadAsync(transformer.TransformAsync(extractor.ExtractAsync(progress)));

Skip and maximum item count

var extractor = new XmlSingleStreamExtractor<Person>(xmlStream);
extractor.SkipItemCount = 10;     // Skip first 10 items
extractor.MaximumItemCount = 5;   // Then take 5 items

var transformer = new TestTransformer<Person>();
var loader = new TestLoader<Person>(collectItems: true);

await loader.LoadAsync(transformer.TransformAsync(extractor.ExtractAsync()));
// extractor.CurrentItemCount == 5, extractor.CurrentSkippedItemCount == 10

🎯 Supported Frameworks

This library targets:

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

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

πŸ” Code Quality & Static Analysis

This project enforces strict code quality standards through 7 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

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)

Building from Source

Prerequisites

  • .NET 10.0 SDK (older SDKs work for restoring the older TFMs but the build/test matrix targets the full range up to .NET 10.0)
  • Optional: PowerShell Core for formatting scripts

Build Steps

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

# Restore dependencies
dotnet restore

# Build the solution
dotnet build --configuration Release

# Run tests
dotnet test --configuration Release

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

Code Formatting

This project uses .editorconfig and dotnet format:

# Format code
dotnet format

# Verify formatting
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

🀝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md for:

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

Acknowledgments

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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 was computed.  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.8.1 276 8/21/2026
0.8.0 373 8/13/2026
0.7.0 344 8/12/2026
0.6.0 251 8/10/2026
0.5.0 247 8/9/2026
0.4.0 167 8/8/2026
0.3.0 323 7/22/2026
0.2.2 112 7/12/2026
0.2.1 142 6/26/2026
0.2.0 117 4/28/2026
0.1.0 128 3/24/2026
0.0.0 111 3/24/2026