Skip to content
Open
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ project.lock.json
project.fragment.lock.json
artifacts/

# BenchmarkDotNet
BenchmarkDotNet.Artifacts/

*_i.c
*_p.c
*_i.h
Expand Down
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A lightweight .NET library that converts `IEnumerable<T>` into an `IDataReader`,
- **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.
- **Multi-target** -- supports .NET 9.0 and .NET 10.0.

## Installation

Expand Down Expand Up @@ -112,6 +112,33 @@ dotnet test
dotnet run --project benchmarks/EnumerableDataReaderAdapter.Benchmarks -c Release
```

### Results

The benchmarks measure reading 10,000 rows through the `IDataReader` interface using different mapping strategies and column access patterns on .NET 10.0.

```
BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
unknown 2.10GHz, 1 CPU, 16 logical and 16 physical cores
.NET SDK 10.0.103
[Host] : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v4
.NET 10.0 : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v4

Job=.NET 10.0 Runtime=.NET 10.0
```

| Method | N | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio |
|----------------------------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|--------:|----------:|------------:|
| DefaultMapping_ByColumnIndex | 10000 | 1.977 ms | 0.0393 ms | 0.0623 ms | 1.00 | 0.04 | 1 | 85.9375 | 352.23 KB | 1.00 |
| MappingExpressions_ByColumnIndex | 10000 | 2.047 ms | 0.0406 ms | 0.0732 ms | 1.04 | 0.05 | 1 | 89.8438 | 366.67 KB | 1.04 |
| MappingExpressions_ByColumnIndex_CachedMapping | 10000 | 1.982 ms | 0.0385 ms | 0.0564 ms | 1.00 | 0.04 | 1 | 85.9375 | 352.29 KB | 1.00 |
| MappingDelegates_ByColumnIndex | 10000 | 1.970 ms | 0.0392 ms | 0.0575 ms | 1.00 | 0.04 | 1 | 85.9375 | 352.53 KB | 1.00 |
| MappingDelegates_ByColumnIndex_CachedMapping | 10000 | 1.969 ms | 0.0391 ms | 0.0841 ms | 1.00 | 0.05 | 1 | 85.9375 | 352.29 KB | 1.00 |
| DefaultMapping_ByColumnName | 10000 | 2.053 ms | 0.0408 ms | 0.0705 ms | 1.04 | 0.05 | 1 | 85.9375 | 352.23 KB | 1.00 |
| MappingExpressions_ByColumnName | 10000 | 2.268 ms | 0.0450 ms | 0.0776 ms | 1.15 | 0.05 | 2 | 89.8438 | 366.6 KB | 1.04 |
| MappingExpressions_ByColumnName_CachedMapping | 10000 | 2.006 ms | 0.0385 ms | 0.0804 ms | 1.02 | 0.05 | 1 | 85.9375 | 352.29 KB | 1.00 |
| MappingDelegates_ByColumnName | 10000 | 2.016 ms | 0.0397 ms | 0.0441 ms | 1.02 | 0.04 | 1 | 85.9375 | 352.53 KB | 1.00 |
| MappingDelegates_ByColumnName_CachedMapping | 10000 | 2.024 ms | 0.0389 ms | 0.0519 ms | 1.02 | 0.04 | 1 | 85.9375 | 352.37 KB | 1.00 |

## License

[MIT](LICENSE) -- Copyright (c) 2018 Dimo Terziev
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public DataStructure(

}

[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net80)]
[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net90)]
[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net10_0)]
[RPlotExporter, RankColumn]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

</Project>
41 changes: 25 additions & 16 deletions src/EnumerableDataReaderAdapter/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections;
using System.Collections.Frozen;
using System.Data;
using System.Data.Common;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -39,7 +40,8 @@ private sealed class EnumerableReaderAdapter<T> : DbDataReader
private bool _isClosed = false;
private IEnumerator<T> _enumerator;
private T _current = default!;
private readonly Lazy<Dictionary<string, int>> _columnLookup;
private readonly FrozenDictionary<string, int> _columnLookup;
private readonly FrozenDictionary<string, int>.AlternateLookup<ReadOnlySpan<char>> _alternateLookup;
private long _rowCount = 0;

public EnumerableReaderAdapter(
Expand All @@ -48,15 +50,14 @@ public EnumerableReaderAdapter(
{
_enumerator = rows.GetEnumerator();
_mappings = mappings;
_columnLookup = new Lazy<Dictionary<string, int>>(() =>

var dict = new Dictionary<string, int>(mappings.Length);
for (int i = 0; i < mappings.Length; i++)
{
var result = new Dictionary<string, int>(_mappings.Length);
for (int i = 0; i < _mappings.Length; i++)
{
result.Add(_mappings[i].ColumnName, i);
}
return result;
});
dict.Add(mappings[i].ColumnName, i);
}
_columnLookup = dict.ToFrozenDictionary();
_alternateLookup = _columnLookup.GetAlternateLookup<ReadOnlySpan<char>>();
}

public override bool HasRows => true;
Expand Down Expand Up @@ -125,20 +126,21 @@ public override void Close()
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetOrdinal(string name) => _columnLookup.Value[name];
public override int GetOrdinal(string name) => _alternateLookup[name.AsSpan()];

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override object GetValue(int i) => _mappings[i].ValueGetter(_current)!;

public override int GetValues(object?[] values)
{
var max = values.Length < _mappings.Length
? values.Length
: _mappings.Length;
var max = Math.Min(values.Length, _mappings.Length);

for (int i = 0; i < max; i++)
ReadOnlySpan<(string ColumnName, Type ColumnType, Func<T, object?> ValueGetter)> mappingSpan = _mappings.AsSpan(0, max);
Span<object?> valuesSpan = values.AsSpan(0, max);

for (int i = 0; i < mappingSpan.Length; i++)
{
values[i] = _mappings[i].ValueGetter(_current);
valuesSpan[i] = mappingSpan[i].ValueGetter(_current);
}

return max;
Expand Down Expand Up @@ -167,7 +169,14 @@ public override int GetValues(object?[] values)
public override string GetString(int i) => (string)GetValue(i);
public override decimal GetDecimal(int i) => (decimal)GetValue(i);
public override DateTime GetDateTime(int i) => (DateTime)GetValue(i);
public override bool IsDBNull(int i) => GetValue(i) == null || GetValue(i) == DBNull.Value;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsDBNull(int i)
{
var value = GetValue(i);
return value is null or DBNull;
}

public override int FieldCount => _mappings.Length;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>

<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down