diff --git a/Dapper.StrongName/Dapper.StrongName.csproj b/Dapper.StrongName/Dapper.StrongName.csproj
index f202f8f6c..40533b2d4 100644
--- a/Dapper.StrongName/Dapper.StrongName.csproj
+++ b/Dapper.StrongName/Dapper.StrongName.csproj
@@ -19,10 +19,12 @@
+
+
diff --git a/Dapper/CollectorT.cs b/Dapper/CollectorT.cs
new file mode 100644
index 000000000..29d532a1f
--- /dev/null
+++ b/Dapper/CollectorT.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Dapper;
+
+///
+/// Allows efficient collection of data into lists, arrays, etc.
+///
+/// This is a mutable struct; treat with caution.
+///
+[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
+[SuppressMessage("Usage", "CA2231:Overload operator equals on overriding value type Equals", Justification = "Equality not supported")]
+public struct Collector
+{
+ ///
+ /// Create a new collector using a size hint for the number of elements expected.
+ ///
+ public Collector(int capacityHint)
+ {
+ oversized = capacityHint > 0 ? ArrayPool.Shared.Rent(capacityHint) : [];
+ capacity = oversized.Length;
+ }
+
+ ///
+ public readonly override string ToString() => $"Count: {count}";
+
+ ///
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public readonly override bool Equals([NotNullWhen(true)] object? obj) => throw new NotSupportedException();
+
+ ///
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public readonly override int GetHashCode() => throw new NotSupportedException();
+
+ private T[] oversized;
+ private int count, capacity;
+
+ ///
+ /// Gets the current capacity of the backing buffer of this instance.
+ ///
+ internal readonly int Capacity => capacity;
+
+ ///
+ /// Gets the number of elements represented by this instance.
+ ///
+ public readonly int Count => count;
+
+ ///
+ /// Gets the underlying elements represented by this instance.
+ ///
+ public readonly Span Span => new(oversized, 0, count);
+
+ ///
+ /// Gets the underlying elements represented by this instance.
+ ///
+ public readonly ArraySegment ArraySegment => new(oversized, 0, count);
+
+ ///
+ /// Gets the element at the specified index.
+ ///
+ public readonly ref T this[int index]
+ {
+ get
+ {
+ return ref index >= 0 & index < count ? ref oversized[index] : ref OutOfRange();
+
+ static ref T OutOfRange() => throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ ///
+ /// Add an element to the collection.
+ ///
+ public void Add(T value)
+ {
+ if (capacity == count) Expand();
+ oversized[count++] = value;
+ }
+
+ ///
+ /// Add elements to the collection.
+ ///
+ public void AddRange(ReadOnlySpan values)
+ {
+ EnsureCapacity(count + values.Length);
+ values.CopyTo(new(oversized, count, values.Length));
+ count += values.Length;
+ }
+
+ private void EnsureCapacity(int minCapacity)
+ {
+ if (capacity < minCapacity)
+ {
+ var newBuffer = ArrayPool.Shared.Rent(minCapacity);
+ Span.CopyTo(newBuffer);
+ var oldBuffer = oversized;
+ oversized = newBuffer;
+ capacity = newBuffer.Length;
+
+ if (oldBuffer is not null)
+ {
+ ArrayPool.Shared.Return(oldBuffer);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void Expand() => EnsureCapacity(Math.Max(capacity * 2, 16));
+
+ ///
+ /// Release any resources associated with this instance.
+ ///
+ public void Clear()
+ {
+ count = 0;
+ if (capacity != 0)
+ {
+ capacity = 0;
+ ArrayPool.Shared.Return(oversized);
+ oversized = [];
+ }
+ }
+
+ ///
+ /// Create an array with the elements associated with this instance, and release any resources.
+ ///
+ public T[] ToArrayAndClear()
+ {
+ T[] result = [.. Span]; // let the compiler worry about the per-platform implementation
+ Clear();
+ return result;
+ }
+
+ ///
+ /// Create an array with the elements associated with this instance, and release any resources.
+ ///
+ public List ToListAndClear()
+ {
+ List result = [.. Span]; // let the compiler worry about the per-platform implementation (net8+ in particular)
+ Clear();
+ return result;
+ }
+}
diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj
index af5febfb8..dfe52194f 100644
--- a/Dapper/Dapper.csproj
+++ b/Dapper/Dapper.csproj
@@ -26,10 +26,12 @@
+
+
diff --git a/Dapper/DefaultTypeMap.cs b/Dapper/DefaultTypeMap.cs
index 98029741a..0e90bf329 100644
--- a/Dapper/DefaultTypeMap.cs
+++ b/Dapper/DefaultTypeMap.cs
@@ -51,12 +51,12 @@ internal static List GetSettableProps(Type t)
return t
.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(p => GetPropertySetter(p, t) is not null)
- .ToList();
+ .AsList();
}
internal static List GetSettableFields(Type t)
{
- return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList();
+ return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).AsList();
}
///
@@ -115,7 +115,7 @@ internal static List GetSettableFields(Type t)
public ConstructorInfo? FindExplicitConstructor()
{
var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
- var withAttr = constructors.Where(c => c.GetCustomAttributes(typeof(ExplicitConstructorAttribute), true).Length > 0).ToList();
+ var withAttr = constructors.Where(c => c.GetCustomAttributes(typeof(ExplicitConstructorAttribute), true).Length > 0).AsList();
if (withAttr.Count == 1)
{
diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt
index 456aa8bb1..022442f63 100644
--- a/Dapper/PublicAPI.Shipped.txt
+++ b/Dapper/PublicAPI.Shipped.txt
@@ -177,6 +177,7 @@ static Dapper.SqlMapper.AddTypeHandlerImpl(System.Type! type, Dapper.SqlMapper.I
static Dapper.SqlMapper.AddTypeMap(System.Type! type, System.Data.DbType dbType) -> void
static Dapper.SqlMapper.AddTypeMap(System.Type! type, System.Data.DbType dbType, bool useGetFieldValue) -> void
static Dapper.SqlMapper.AsList(this System.Collections.Generic.IEnumerable? source) -> System.Collections.Generic.List!
+static Dapper.SqlMapper.AsListAsync(this System.Collections.Generic.IAsyncEnumerable? source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
static Dapper.SqlMapper.AsTableValuedParameter(this System.Data.DataTable! table, string? typeName = null) -> Dapper.SqlMapper.ICustomQueryParameter!
static Dapper.SqlMapper.AsTableValuedParameter(this System.Collections.Generic.IEnumerable! list, string? typeName = null) -> Dapper.SqlMapper.ICustomQueryParameter!
static Dapper.SqlMapper.ConnectionStringComparer.get -> System.Collections.Generic.IEqualityComparer!
@@ -332,4 +333,20 @@ static Dapper.SqlMapper.ThrowDataException(System.Exception! ex, int index, Syst
static Dapper.SqlMapper.ThrowNullCustomQueryParameter(string! name) -> void
static Dapper.SqlMapper.TypeHandlerCache.Parse(object! value) -> T?
static Dapper.SqlMapper.TypeHandlerCache.SetValue(System.Data.IDbDataParameter! parameter, object! value) -> void
-static Dapper.SqlMapper.TypeMapProvider -> System.Func!
\ No newline at end of file
+static Dapper.SqlMapper.TypeMapProvider -> System.Func!
+
+Dapper.Collector
+Dapper.Collector.Collector() -> void
+Dapper.Collector.Collector(int capacityHint) -> void
+Dapper.Collector.Count.get -> int
+Dapper.Collector.Span.get -> System.Span
+Dapper.Collector.ArraySegment.get -> System.ArraySegment
+Dapper.Collector.Clear() -> void
+Dapper.Collector.Add(T value) -> void
+Dapper.Collector.AddRange(System.ReadOnlySpan values) -> void
+Dapper.Collector.ToListAndClear() -> System.Collections.Generic.List!
+Dapper.Collector.ToArrayAndClear() -> T[]!
+Dapper.Collector.this[int index].get -> T
+override Dapper.Collector.ToString() -> string!
+override Dapper.Collector.GetHashCode() -> int
+override Dapper.Collector.Equals(object? obj) -> bool
\ No newline at end of file
diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs
index eade08cb2..799d05c14 100644
--- a/Dapper/SqlMapper.Async.cs
+++ b/Dapper/SqlMapper.Async.cs
@@ -447,7 +447,7 @@ private static async Task> QueryAsync(this IDbConnection cnn,
if (command.Buffered)
{
- var buffer = new List();
+ var buffer = new Collector();
var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
while (await reader.ReadAsync(cancel).ConfigureAwait(false))
{
@@ -456,7 +456,7 @@ private static async Task> QueryAsync(this IDbConnection cnn,
}
while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ }
command.OnCompleted();
- return buffer;
+ return buffer.ToListAndClear();
}
else
{
@@ -546,6 +546,32 @@ public static Task ExecuteAsync(this IDbConnection cnn, CommandDefinition c
}
}
+ ///
+ /// Asynchronously collect a sequence of data into a list.
+ ///
+ /// The type of element in the list.
+ /// The enumerable to return as a list.
+ /// A to observe while waiting for the task to complete.
+ public static Task> AsListAsync(this IAsyncEnumerable? source, CancellationToken cancellationToken = default)
+ {
+ if (source is null) return null!; // GIGO
+
+ return EnumerateAsync(source, cancellationToken);
+
+ static async Task> EnumerateAsync(IAsyncEnumerable source, CancellationToken cancellationToken)
+ {
+ var buffer = new Collector(); // amortizes intermediate buffers
+ await using (var iterator = source.GetAsyncEnumerator(cancellationToken))
+ {
+ while (await iterator.MoveNextAsync().ConfigureAwait(false))
+ {
+ buffer.Add(iterator.Current);
+ }
+ }
+ return buffer.ToListAndClear();
+ }
+ }
+
private readonly struct AsyncExecState
{
public readonly DbCommand Command;
@@ -941,7 +967,7 @@ private static async Task> MultiMapAsync(null, CommandDefinition.ForCallback(command.Parameters, command.Flags), map, splitOn, reader, identity, true);
- return command.Buffered ? results.ToList() : results;
+ return command.Buffered ? results.AsList() : results;
}
finally
{
@@ -989,7 +1015,7 @@ private static async Task> MultiMapAsync(this IDbC
using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader);
using var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false);
var results = MultiMapImpl(null, default, types, map, splitOn, reader, identity, true);
- return command.Buffered ? results.ToList() : results;
+ return command.Buffered ? results.AsList() : results;
}
finally
{
diff --git a/Dapper/SqlMapper.GridReader.Async.cs b/Dapper/SqlMapper.GridReader.Async.cs
index a32d53124..72e22f93d 100644
--- a/Dapper/SqlMapper.GridReader.Async.cs
+++ b/Dapper/SqlMapper.GridReader.Async.cs
@@ -229,12 +229,12 @@ private async Task> ReadBufferedAsync(int index, Func();
+ var buffer = new Collector();
while (index == ResultIndex && await reader!.ReadAsync(cancel).ConfigureAwait(false))
{
buffer.Add(ConvertTo(deserializer(reader)));
}
- return buffer;
+ return buffer.ToListAndClear();
}
finally // finally so that First etc progresses things even when multiple rows
{
diff --git a/Dapper/SqlMapper.GridReader.cs b/Dapper/SqlMapper.GridReader.cs
index 1370f7cc9..08ec3c645 100644
--- a/Dapper/SqlMapper.GridReader.cs
+++ b/Dapper/SqlMapper.GridReader.cs
@@ -202,7 +202,7 @@ private IEnumerable ReadImpl(Type type, bool buffered)
cache.Deserializer = deserializer;
}
var result = ReadDeferred(index, deserializer.Func, type);
- return buffered ? result.ToList() : result;
+ return buffered ? result.AsList() : result;
}
private T ReadRow(Type type, Row row)
@@ -283,7 +283,7 @@ private IEnumerable MultiReadInternal(Type[] types, Func Read(Func func, string splitOn = "id", bool buffered = true)
{
var result = MultiReadInternal(func, splitOn);
- return buffered ? result.ToList() : result;
+ return buffered ? result.AsList() : result;
}
///
@@ -299,7 +299,7 @@ public IEnumerable Read(Func Read(Func func, string splitOn = "id", bool buffered = true)
{
var result = MultiReadInternal(func, splitOn);
- return buffered ? result.ToList() : result;
+ return buffered ? result.AsList() : result;
}
///
@@ -316,7 +316,7 @@ public IEnumerable Read(Func Read(Func func, string splitOn = "id", bool buffered = true)
{
var result = MultiReadInternal(func, splitOn);
- return buffered ? result.ToList() : result;
+ return buffered ? result.AsList() : result;
}
///
@@ -334,7 +334,7 @@ public IEnumerable Read(Func
public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true)
{
var result = MultiReadInternal(func, splitOn);
- return buffered ? result.ToList() : result;
+ return buffered ? result.AsList() : result;
}
///
@@ -353,7 +353,7 @@ public IEnumerable Read Read(Func func, string splitOn = "id", bool buffered = true)
{
var result = MultiReadInternal(func, splitOn);
- return buffered ? result.ToList() : result;
+ return buffered ? result.AsList() : result;
}
///
@@ -373,7 +373,7 @@ public IEnumerable Read Read(Func func, string splitOn = "id", bool buffered = true)
{
var result = MultiReadInternal(func, splitOn);
- return buffered ? result.ToList() : result;
+ return buffered ? result.AsList() : result;
}
///
@@ -387,7 +387,7 @@ public IEnumerable Read Read(Type[] types, Func
/// The type of element in the list.
/// The enumerable to return as a list.
- public static List AsList(this IEnumerable? source) => source switch
+ public static List AsList(this IEnumerable? source)
{
- null => null!,
- List list => list,
- _ => Enumerable.ToList(source),
- };
+ return source switch
+ {
+ null => null!, // GIGO
+ List list => list, // already a list
+ ICollection col => new(col), // handled efficiently internally
+ _ => Enumerate(source), // use custom implementation
+ };
+
+ static List Enumerate(IEnumerable source)
+ {
+ var buffer = new Collector(); // amortizes intermediate buffers
+ using (var iterator = source.GetEnumerator())
+ {
+ while (iterator.MoveNext())
+ {
+ buffer.Add(iterator.Current);
+ }
+ }
+ return buffer.ToListAndClear();
+ }
+ }
///
/// Execute parameterized SQL.
@@ -841,7 +862,7 @@ public static IEnumerable Query(this IDbConnection cnn, string sql, object
{
var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None);
var data = QueryImpl(cnn, command, typeof(T));
- return command.Buffered ? data.ToList() : data;
+ return command.Buffered ? data.AsList() : data;
}
///
@@ -950,7 +971,7 @@ public static IEnumerable