Skip to content

[Feature Request] Add support for IAsyncEnumerable (QueryUnbufferedAsync) on all .NET targets #2143

Description

@julealgon

Currently, IAsyncEnumerable support is conditioned on .NET5 or greater. For example:

#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>
publicstaticIAsyncEnumerable<dynamic>QueryUnbufferedAsync(thisDbConnectioncnn,stringsql,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
returnQueryUnbufferedAsync<dynamic>(cnn,typeof(object),newCommandDefinition(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>
publicstaticIAsyncEnumerable<T>QueryUnbufferedAsync<T>(thisDbConnectioncnn,stringsql,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
returnQueryUnbufferedAsync<T>(cnn,typeof(T),newCommandDefinition(sql,param,transaction,commandTimeout,commandType,CommandFlags.None,default));
}
privatestaticIAsyncEnumerable<T>QueryUnbufferedAsync<T>(thisIDbConnectioncnn,TypeeffectiveType,CommandDefinitioncommand)
{
returnImpl(cnn,effectiveType,command,command.CancellationToken);// proxy to allow CT expression
staticasyncIAsyncEnumerable<T>Impl(IDbConnectioncnn,TypeeffectiveType,CommandDefinitioncommand,
[EnumeratorCancellation]CancellationTokencancel)
{
object?param=command.Parameters;
varidentity=newIdentity(command.CommandText,command.CommandTypeDirect,cnn,effectiveType,param?.GetType());
varinfo=GetCacheInfo(identity,param,command.AddToCache);
boolwasClosed=cnn.State==ConnectionState.Closed;
usingvarcmd=command.TrySetupAsyncCommand(cnn,info.ParamReader);
DbDataReader?reader=null;
try
{
if(wasClosed)awaitcnn.TryOpenAsync(cancel).ConfigureAwait(false);
reader=awaitExecuteReaderWithFlagsFallbackAsync(cmd,wasClosed,CommandBehavior.SequentialAccess|CommandBehavior.SingleResult,cancel).ConfigureAwait(false);
vartuple=info.Deserializer;
inthash=GetColumnHash(reader);
if(tuple.Funcisnull||tuple.Hash!=hash)
{
if(reader.FieldCount==0)
{
yieldbreak;
}
tuple=info.Deserializer=newDeserializerState(hash,GetDeserializer(effectiveType,reader,0,-1,false));
if(command.AddToCache)SetQueryCache(identity,info);
}
varfunc=tuple.Func;
varconvertToType=Nullable.GetUnderlyingType(effectiveType)??effectiveType;
while(awaitreader.ReadAsync(cancel).ConfigureAwait(false))
{
objectval=func(reader);
yieldreturnGetValue<T>(reader,effectiveType,val);
}
while(awaitreader.NextResultAsync(cancel).ConfigureAwait(false)){/* ignore subsequent result sets */}
command.OnCompleted();
}
finally
{
if(readeris not null)
{
if(!reader.IsClosed)
{
try{cmd?.Cancel();}
catch{/* don't spoil any existing exception */}
}
awaitreader.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?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions