|
1 | 1 | # EnumerableDataReaderAdapter |
2 | 2 |
|
3 | | -An adapter that allows converting an IEnumerable<T> to IDataReader that can be used as a data source for SqlBulkCopy. |
| 3 | +A lightweight .NET library that converts `IEnumerable<T>` into an `IDataReader`, enabling streaming of in-memory collections into APIs that require `IDataReader` -- most notably `SqlBulkCopy` for high-performance bulk inserts into SQL Server. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Streaming** -- rows are read lazily from the enumerable; the entire collection is never buffered in memory. |
| 8 | +- **Automatic property mapping** -- public properties are discovered automatically when no explicit mapping is provided. |
| 9 | +- **Fluent column mapping API** -- choose exactly which columns to expose using expression-based or delegate-based mappings. |
| 10 | +- **Computed columns** -- map constant values or derived expressions that don't correspond to a property. |
| 11 | +- **Multi-target** -- supports .NET 8.0, .NET 9.0, and .NET 10.0. |
| 12 | + |
| 13 | +## Installation |
| 14 | + |
| 15 | +Add a project reference or include the source in your solution. The library has no external runtime dependencies. |
4 | 16 |
|
5 | 17 | ## Usage |
6 | 18 |
|
7 | | -```c# |
8 | | -var data = Enumerable.Range(1, 10000).Select(x => new { Id = x, Name = $"name-{x}" }); |
| 19 | +### Basic usage with SqlBulkCopy |
| 20 | + |
| 21 | +```csharp |
| 22 | +var data = Enumerable.Range(1, 10_000) |
| 23 | + .Select(x => new { Id = x, Name = $"name-{x}" }); |
| 24 | + |
| 25 | +using var reader = data.ToDataReader(map => map |
| 26 | + .Add(x => x.Id) |
| 27 | + .Add(x => x.Name)); |
| 28 | + |
| 29 | +using var bulkCopy = new SqlBulkCopy(connectionString); |
| 30 | +bulkCopy.DestinationTableName = "dbo.People"; |
| 31 | +bulkCopy.EnableStreaming = true; |
| 32 | +bulkCopy.ColumnMappings.Add("Id", "Id"); |
| 33 | +bulkCopy.ColumnMappings.Add("Name", "Name"); |
| 34 | + |
| 35 | +await bulkCopy.WriteToServerAsync(reader); |
| 36 | +``` |
| 37 | + |
| 38 | +### Default mapping (auto-discover all public properties) |
| 39 | + |
| 40 | +If no mapping is configured, every public instance property on `T` is exposed as a column: |
| 41 | + |
| 42 | +```csharp |
| 43 | +var reader = products.ToDataReader(); |
| 44 | +``` |
| 45 | + |
| 46 | +### Expression-based mapping |
| 47 | + |
| 48 | +Use lambda expressions that point to properties. The column name and type are inferred automatically: |
| 49 | + |
| 50 | +```csharp |
| 51 | +var reader = products.ToDataReader(map => map |
| 52 | + .Add(p => p.Id) |
| 53 | + .Add(p => p.Name) |
| 54 | + .Add(p => p.Price)); |
| 55 | +``` |
| 56 | + |
| 57 | +### Delegate-based mapping with explicit name and type |
| 58 | + |
| 59 | +For full control -- including computed/constant columns -- specify the column name, CLR type, and a value delegate: |
| 60 | + |
| 61 | +```csharp |
| 62 | +var reader = orders.ToDataReader(map => map |
| 63 | + .Add("OrderId", typeof(int), o => o.Id) |
| 64 | + .Add("Total", typeof(decimal), o => o.Quantity * o.UnitPrice) |
| 65 | + .Add("Source", typeof(string), _ => "Import")); |
| 66 | +``` |
| 67 | + |
| 68 | +### Mixing mapping styles |
| 69 | + |
| 70 | +The two `Add` overloads can be freely combined in a single mapping configuration: |
| 71 | + |
| 72 | +```csharp |
| 73 | +var reader = items.ToDataReader(map => map |
| 74 | + .Add(i => i.Id) |
| 75 | + .Add("DisplayName", typeof(string), i => $"{i.FirstName} {i.LastName}") |
| 76 | + .Add(i => i.CreatedAt)); |
| 77 | +``` |
| 78 | + |
| 79 | +## API Reference |
| 80 | + |
| 81 | +### `EnumerableExtensions` |
| 82 | + |
| 83 | +| Method | Description | |
| 84 | +|--------|-------------| |
| 85 | +| `ToDataReader<T>(this IEnumerable<T>, Action<ColumnMappings<T>>?)` | Creates an `IDataReader` with optional mapping configuration. When no columns are configured, all public properties of `T` are used. | |
| 86 | +| `ToDataReader<T>(this IEnumerable<T>, ColumnMappings<T>)` | Creates an `IDataReader` using a pre-built `ColumnMappings<T>` instance. | |
| 87 | + |
| 88 | +### `ColumnMappings<T>` |
| 89 | + |
| 90 | +| Method | Description | |
| 91 | +|--------|-------------| |
| 92 | +| `Add(Expression<Func<T, object?>>)` | Adds a column from a property expression. Column name and type are inferred from the member. | |
| 93 | +| `Add(string, Type, Func<T, object?>)` | Adds a column with an explicit name, CLR type, and value delegate. | |
| 94 | + |
| 95 | +Both `Add` methods return `this`, so calls can be chained fluently. |
| 96 | + |
| 97 | +## Building |
| 98 | + |
| 99 | +```bash |
| 100 | +dotnet build |
| 101 | +``` |
| 102 | + |
| 103 | +## Running tests |
| 104 | + |
| 105 | +```bash |
| 106 | +dotnet test |
| 107 | +``` |
| 108 | + |
| 109 | +## Benchmarks |
| 110 | + |
| 111 | +```bash |
| 112 | +dotnet run --project benchmarks/EnumerableDataReaderAdapter.Benchmarks -c Release |
| 113 | +``` |
9 | 114 |
|
10 | | -using var reader = data.ToDataReader(map => map.Add(x => x.Id).Add(x => x.Name)); |
| 115 | +## License |
11 | 116 |
|
12 | | -using var sqlBulkCopy = new SqlBulkCopy(connectionString); |
13 | | -sqlBulkCopy.BatchSize = 10000; |
14 | | -sqlBulkCopy.ColumnMappings.Add("Id", "Id"); |
15 | | -sqlBulkCopy.ColumnMappings.Add("Name", "Name"); |
16 | | -sqlBulkCopy.DestinationTableName = "dbo.TableName"; |
17 | | -sqlBulkCopy.EnableStreaming = true; |
18 | | -await sqlBulkCopy.WriteToServerAsync(reader); |
19 | | -``` |
| 117 | +[MIT](LICENSE) -- Copyright (c) 2018 Dimo Terziev |
0 commit comments