-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Description
Currently, IAsyncEnumerable
support is conditioned on .NET5 or greater. For example:
Dapper/Dapper/SqlMapper.Async.cs
Lines 1252 to 1347 in 6434c69
#if NET5_0_OR_GREATER | |
/// <summary> | |
/// Execute a query asynchronously using <see cref="IAsyncEnumerable{dynamic}"/>. | |
/// </summary> | |
/// <param name="cnn">The connection to query on.</param> | |
/// <param name="sql">The SQL to execute for the query.</param> | |
/// <param name="param">The parameters to pass, if any.</param> | |
/// <param name="transaction">The transaction to use, if any.</param> | |
/// <param name="commandTimeout">The command timeout (in seconds).</param> | |
/// <param name="commandType">The type of command to execute.</param> | |
/// <returns> | |
/// A sequence of data of dynamic data | |
/// </returns> | |
public static IAsyncEnumerable<dynamic> QueryUnbufferedAsync(this DbConnection cnn, string sql, object? param = null, DbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) | |
{ | |
// note: in many cases of adding a new async method I might add a CancellationToken - however, cancellation is expressed via WithCancellation on iterators | |
return QueryUnbufferedAsync<dynamic>(cnn, typeof(object), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); | |
} | |
/// <summary> | |
/// Execute a query asynchronously using <see cref="IAsyncEnumerable{T}"/>. | |
/// </summary> | |
/// <typeparam name="T">The type of results to return.</typeparam> | |
/// <param name="cnn">The connection to query on.</param> | |
/// <param name="sql">The SQL to execute for the query.</param> | |
/// <param name="param">The parameters to pass, if any.</param> | |
/// <param name="transaction">The transaction to use, if any.</param> | |
/// <param name="commandTimeout">The command timeout (in seconds).</param> | |
/// <param name="commandType">The type of command to execute.</param> | |
/// <returns> | |
/// A sequence of data of <typeparamref name="T"/>; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is | |
/// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). | |
/// </returns> | |
public static IAsyncEnumerable<T> QueryUnbufferedAsync<T>(this DbConnection cnn, string sql, object? param = null, DbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) | |
{ | |
// note: in many cases of adding a new async method I might add a CancellationToken - however, cancellation is expressed via WithCancellation on iterators | |
return QueryUnbufferedAsync<T>(cnn, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); | |
} | |
private static IAsyncEnumerable<T> QueryUnbufferedAsync<T>(this IDbConnection cnn, Type effectiveType, CommandDefinition command) | |
{ | |
return Impl(cnn, effectiveType, command, command.CancellationToken); // proxy to allow CT expression | |
static async IAsyncEnumerable<T> Impl(IDbConnection cnn, Type effectiveType, CommandDefinition command, | |
[EnumeratorCancellation] CancellationToken cancel) | |
{ | |
object? param = command.Parameters; | |
var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); | |
var info = GetCacheInfo(identity, param, command.AddToCache); | |
bool wasClosed = cnn.State == ConnectionState.Closed; | |
using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); | |
DbDataReader? reader = null; | |
try | |
{ | |
if (wasClosed) await cnn.TryOpenAsync(cancel).ConfigureAwait(false); | |
reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, cancel).ConfigureAwait(false); | |
var tuple = info.Deserializer; | |
int hash = GetColumnHash(reader); | |
if (tuple.Func is null || tuple.Hash != hash) | |
{ | |
if (reader.FieldCount == 0) | |
{ | |
yield break; | |
} | |
tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); | |
if (command.AddToCache) SetQueryCache(identity, info); | |
} | |
var func = tuple.Func; | |
var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; | |
while (await reader.ReadAsync(cancel).ConfigureAwait(false)) | |
{ | |
object val = func(reader); | |
yield return GetValue<T>(reader, effectiveType, val); | |
} | |
while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ } | |
command.OnCompleted(); | |
} | |
finally | |
{ | |
if (reader is not null) | |
{ | |
if (!reader.IsClosed) | |
{ | |
try { cmd?.Cancel(); } | |
catch { /* don't spoil any existing exception */ } | |
} | |
await reader.DisposeAsync(); | |
} | |
if (wasClosed) cnn.Close(); | |
} | |
} | |
} | |
#endif |
However, there shouldn't be any reason not to support IAsyncEnumerable
on every target currently supported by Dapper, since there exists a well-maintained BCL package that adds the interface on older frameworks:
To allow for IAsyncEnumerable
to work everywhere, it would just be a matter of including a dependency on the package above for the netstandard2.0
and net462
targets. This approach is used heavily by other libraries for this exact same purpose.
We currently have a solution that targets NET472 and we wanted to be able to tap into IAsyncEnumerable
in a couple of places where we are currently being forced to use Task<IEnumerable<T>>
methods such as QueryAsync
.
Could there be an update to the library that adds support for the IAsyncEnumerable
-returning methods for all frameworks based on the above suggestion?