Wolfgang.Etl.SqlBulkCopy
0.9.0
Prefix Reserved
dotnet add package Wolfgang.Etl.SqlBulkCopy --version 0.9.0
NuGet\Install-Package Wolfgang.Etl.SqlBulkCopy -Version 0.9.0
<PackageReference Include="Wolfgang.Etl.SqlBulkCopy" Version="0.9.0" />
<PackageVersion Include="Wolfgang.Etl.SqlBulkCopy" Version="0.9.0" />
<PackageReference Include="Wolfgang.Etl.SqlBulkCopy" />
paket add Wolfgang.Etl.SqlBulkCopy --version 0.9.0
#r "nuget: Wolfgang.Etl.SqlBulkCopy, 0.9.0"
#:package Wolfgang.Etl.SqlBulkCopy@0.9.0
#addin nuget:?package=Wolfgang.Etl.SqlBulkCopy&version=0.9.0
#tool nuget:?package=Wolfgang.Etl.SqlBulkCopy&version=0.9.0
Wolfgang.Etl.SqlBulkCopy
A loader that uses SqlBulkCopy for fast inserts into a Microsoft SQL database
π¦ Installation
dotnet add package Wolfgang.Etl.SqlBulkCopy
NuGet Package: Wolfgang.Etl.SqlBulkCopy
π License
This project is licensed under the MIT License. See the LICENSE file for details.
π Documentation
- GitHub Repository: https://github.com/Chris-Wolfgang/ETL-SqlBulkCopy
- API Documentation: https://Chris-Wolfgang.github.io/ETL-SqlBulkCopy/
- Formatting Guide: README-FORMATTING.md
- Contributing Guide: CONTRIBUTING.md
- Architecture Decisions: docs/adr/index.md
- Migration Guides: docs/migrations/
π Quick Start
using Microsoft.Data.SqlClient;
using Wolfgang.Etl.SqlBulkCopy;
public sealed record Customer
{
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
public decimal Balance { get; init; }
}
async IAsyncEnumerable<Customer> ReadSourceAsync()
{
// your source: file, API, another database, etc.
yield return new Customer { Id = 1, Name = "Acme", Balance = 100m };
yield return new Customer { Id = 2, Name = "Contoso", Balance = 250m };
}
using var connection = new SqlConnection("Server=.;Database=Sandbox;Integrated Security=True;Encrypt=True;");
await connection.OpenAsync();
var loader = new SqlBulkCopyLoader<Customer>
(
connection,
new SqlBulkCopyLoaderOptions<Customer>
{
BatchSize = 10_000,
PreAction = PreAction.TruncateTable,
}
);
await loader.LoadAsync(ReadSourceAsync(), CancellationToken.None);
Configuring the loader
Everything about a load is configured through SqlBulkCopyLoaderOptions<T>, a record with init-only members, passed
as the second constructor argument ((connection, options, transaction, logger), all three optional). It carries the
loader's own settings β BulkCopyOptions, BatchSize, BulkCopyTimeout, the destination overrides, validation, the
pre/post actions and IsDryRun β and the ones every loader shares (ReportingInterval, MaximumItemCount,
SkipItemCount, ErrorPolicy). A loader constructed without a record keeps every default. The settable properties on
the loader are deprecated and will be removed in a later release.
β¨ Features
| Feature | Description |
|---|---|
| Streaming bulk load | Consumes IAsyncEnumerable<T> and writes to SQL Server via SqlBulkCopy |
| Type-driven mapping | [Table] / [Column] / [NotMapped] attributes drive schema/table/column names β no manual ColumnMappings |
| Nested tables | Recursively writes child collections to their own tables inside the same bulk-copy session |
| Pre/post actions | Built-in TruncateTable / DeleteAllRecords; custom-action delegates for schema-aware work |
| Progress reporting | IProgress<SqlBulkCopyReport> β rows written (CurrentItemCount), rows skipped, batch count |
| Data validation | Opt in with EnableDataValidation; DataAnnotations failures throw or skip per ValidationFailureBehavior, with OnValidationFailed / OnNestedValidationFailed callbacks |
| Transactions | Optional SqlTransaction participates in the bulk load and pre/post commands |
| Dry run | Set IsDryRun = true on SqlBulkCopyLoaderOptions<T> (or the loader) to run the full pipeline β enumerate, map, validate, report β with no SQL side effects (skips pre/post actions and the bulk insert) |
| Async-only | Banned-symbol analyzer enforces WriteToServerAsync / ExecuteNonQueryAsync β no sync fallbacks |
| Native AOT ready | Opt a record into compile-time source-generated accessors with [BulkCopyable] β no runtime IL emission on the hot path (net5.0+) |
| Multi-targeted | net462, net481, netstandard2.0, net5.0, net6.0, net7.0, net8.0, net10.0 |
Examples:
- Truncate before load:
PreAction = PreAction.TruncateTableon the options record (shown above). - Custom pre-action:
PreAction = PreAction.CustomActionandPreLoadCustomAction = async p => { /* p.Connection, p.Transaction, p.Columns, p.CancellationToken */ }on the options record. - Nested table: decorate a
[NotMapped]-freeIEnumerable<TChild>property; the child rows write to the child's[Table]in the same session. - Transaction across multiple files: build each loader with
new SqlBulkCopyLoader<T>(connection, options, transaction)and eitherCommit()once for all-or-nothing, or commit per file for restartability (worked examples on the constructor's XML docs). - Dry run: set
IsDryRun = trueon the options record to run the full pipeline without writing β it still enumerates, maps, validates, counts, and logs, so mapping/validation errors surface without touching the destination.
See the API documentation for the full surface.
π― 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
See the NuGet package page for the authoritative per-TFM compatibility matrix.
β‘ Native AOT & trimming
By default the per-row hot path compiles a property getter at runtime with
System.Linq.Expressions β fast under the JIT, but it emits IL at run time, so
under Native AOT it falls back to the slower expression interpreter.
Opt a record into compile-time source-generated accessors by marking it
[BulkCopyable]:
using System.ComponentModel.DataAnnotations.Schema;
using Wolfgang.Etl.SqlBulkCopy;
[BulkCopyable]
[Table("People")]
public sealed class Person
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public Status Status { get; set; } // enum columns are handled too
}
The bundled source generator emits strongly-typed getters (and enumβunderlying
converters) at compile time, and the loader uses them automatically β no runtime
Expression.Compile, so the marked type's hot path is AOT-clean and keeps
the compiled-getter throughput.
| Opt-in & additive | Unmarked types keep working exactly as before via the runtime-compiled getter. Marking a type never changes its mapping β only how the getters are produced. |
| One package | The generator ships inside Wolfgang.Etl.SqlBulkCopy; there is nothing extra to install or reference. |
net5.0+ |
Generated registration uses module initializers. On older targets the attribute is a no-op and the type uses the runtime getter β correct on those JIT-only frameworks. |
Note: a full Native-AOT publish of an app that opens a SQL connection also depends on
Microsoft.Data.SqlClient's own AOT support, which is outside this library's control.[BulkCopyable]makes this library's mapping path AOT-clean. See ADR 0006 for the full rationale.
π 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
Why the 10.0 SDK when the library targets frameworks as old as
net462? The SDK is the build toolchain, not a runtime requirement. It must be at least as new as the highest framework in<TargetFrameworks>βnet10.0here β because an older SDK has no targeting pack for a newer framework and fails restore withNETSDK1045: The current .NET SDK does not support targeting .NET 10.0. The SDK is backward-compatible, so the single 10.0 SDK builds every target in the list, fromnet462up.This applies only to building this repository. Consuming the package requires nothing of the sort β any runtime matching one of the shipped targets works (.NET Framework 4.6.2+, or anything
netstandard2.0-compatible, or .NET 8/10).
- Optional: PowerShell Core for formatting scripts
Build Steps
# Clone the repository
git clone https://github.com/Chris-Wolfgang/ETL-SqlBulkCopy.git
cd ETL-SqlBulkCopy
# 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
π€ Contributing
Contributions are welcome! Please see CONTRIBUTING.md for:
- Code quality standards
- Build and test instructions
- Pull request guidelines
- Analyzer configuration details
π Acknowledgments
Built on top of Wolfgang.Etl.Abstractions and Microsoft.Data.SqlClient.
| 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.11)
- Microsoft.Data.SqlClient (>= 7.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- System.ComponentModel.Annotations (>= 5.0.0)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
.NETFramework 4.8.1
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.11)
- Microsoft.Data.SqlClient (>= 7.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- System.ComponentModel.Annotations (>= 5.0.0)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.11)
- Microsoft.Data.SqlClient (>= 7.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- System.ComponentModel.Annotations (>= 5.0.0)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net10.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.11)
- Microsoft.Data.SqlClient (>= 7.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- System.ComponentModel.Annotations (>= 5.0.0)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net5.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.11)
- Microsoft.Data.SqlClient (>= 7.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net6.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.11)
- Microsoft.Data.SqlClient (>= 7.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net7.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.11)
- Microsoft.Data.SqlClient (>= 7.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Wolfgang.Etl.Abstractions (>= 0.26.0)
-
net8.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.11)
- Microsoft.Data.SqlClient (>= 7.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- 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.