Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 110 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,117 @@
# EnumerableDataReaderAdapter

An adapter that allows converting an IEnumerable<T> to IDataReader that can be used as a data source for SqlBulkCopy.
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.

## Features

- **Streaming** -- rows are read lazily from the enumerable; the entire collection is never buffered in memory.
- **Automatic property mapping** -- public properties are discovered automatically when no explicit mapping is provided.
- **Fluent column mapping API** -- choose exactly which columns to expose using expression-based or delegate-based mappings.
- **Computed columns** -- map constant values or derived expressions that don't correspond to a property.
- **Multi-target** -- supports .NET 8.0, .NET 9.0, and .NET 10.0.

## Installation

Add a project reference or include the source in your solution. The library has no external runtime dependencies.

## Usage

```c#
var data = Enumerable.Range(1, 10000).Select(x => new { Id = x, Name = $"name-{x}" });
### Basic usage with SqlBulkCopy

```csharp
var data = Enumerable.Range(1, 10_000)
.Select(x => new { Id = x, Name = $"name-{x}" });

using var reader = data.ToDataReader(map => map
.Add(x => x.Id)
.Add(x => x.Name));

using var bulkCopy = new SqlBulkCopy(connectionString);
bulkCopy.DestinationTableName = "dbo.People";
bulkCopy.EnableStreaming = true;
bulkCopy.ColumnMappings.Add("Id", "Id");
bulkCopy.ColumnMappings.Add("Name", "Name");

await bulkCopy.WriteToServerAsync(reader);
```

### Default mapping (auto-discover all public properties)

If no mapping is configured, every public instance property on `T` is exposed as a column:

```csharp
var reader = products.ToDataReader();
```

### Expression-based mapping

Use lambda expressions that point to properties. The column name and type are inferred automatically:

```csharp
var reader = products.ToDataReader(map => map
.Add(p => p.Id)
.Add(p => p.Name)
.Add(p => p.Price));
```

### Delegate-based mapping with explicit name and type

For full control -- including computed/constant columns -- specify the column name, CLR type, and a value delegate:

```csharp
var reader = orders.ToDataReader(map => map
.Add("OrderId", typeof(int), o => o.Id)
.Add("Total", typeof(decimal), o => o.Quantity * o.UnitPrice)
.Add("Source", typeof(string), _ => "Import"));
```

### Mixing mapping styles

The two `Add` overloads can be freely combined in a single mapping configuration:

```csharp
var reader = items.ToDataReader(map => map
.Add(i => i.Id)
.Add("DisplayName", typeof(string), i => $"{i.FirstName} {i.LastName}")
.Add(i => i.CreatedAt));
```

## API Reference

### `EnumerableExtensions`

| Method | Description |
|--------|-------------|
| `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. |
| `ToDataReader<T>(this IEnumerable<T>, ColumnMappings<T>)` | Creates an `IDataReader` using a pre-built `ColumnMappings<T>` instance. |

### `ColumnMappings<T>`

| Method | Description |
|--------|-------------|
| `Add(Expression<Func<T, object?>>)` | Adds a column from a property expression. Column name and type are inferred from the member. |
| `Add(string, Type, Func<T, object?>)` | Adds a column with an explicit name, CLR type, and value delegate. |

Both `Add` methods return `this`, so calls can be chained fluently.

## Building

```bash
dotnet build
```

## Running tests

```bash
dotnet test
```

## Benchmarks

```bash
dotnet run --project benchmarks/EnumerableDataReaderAdapter.Benchmarks -c Release
```

using var reader = data.ToDataReader(map => map.Add(x => x.Id).Add(x => x.Name));
## License

using var sqlBulkCopy = new SqlBulkCopy(connectionString);
sqlBulkCopy.BatchSize = 10000;
sqlBulkCopy.ColumnMappings.Add("Id", "Id");
sqlBulkCopy.ColumnMappings.Add("Name", "Name");
sqlBulkCopy.DestinationTableName = "dbo.TableName";
sqlBulkCopy.EnableStreaming = true;
await sqlBulkCopy.WriteToServerAsync(reader);
```
[MIT](LICENSE) -- Copyright (c) 2018 Dimo Terziev