如何使用SqlDataReader返回和使用IAsyncEnumerable

时间:2019-11-12 19:49:48

标签: c# .net async-await sqldatareader iasyncenumerable

请参阅以下两种方法。第一个返回IAsyncEnumerable。第二个尝试消耗它。

using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

public static class SqlUtility
{
    public static async IAsyncEnumerable<IDataRecord> GetRecordsAsync(
        string connectionString, SqlParameter[] parameters, string commandText,
        [EnumeratorCancellation]CancellationToken cancellationToken)
    {
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
            using (SqlCommand command = new SqlCommand(commandText, connection))
            {
                command.Parameters.AddRange(parameters);
                using (var reader = await command.ExecuteReaderAsync()
                    .ConfigureAwait(false))
                {
                    while (await reader.ReadAsync().ConfigureAwait(false))
                    {
                        yield return reader;
                    }
                }
            }
        }
    }

    public static async Task Example()
    {
        const string connectionString =
            "Server=localhost;Database=[Redacted];Integrated Security=true";
        SqlParameter[] parameters = new SqlParameter[]
        {
            new SqlParameter("VideoID", SqlDbType.Int) { Value = 1000 }
        };
        const string commandText = "select * from Video where VideoID=@VideoID";
        IAsyncEnumerable<IDataRecord> records = GetRecordsAsync(connectionString,
            parameters, commandText, CancellationToken.None);
        IDataRecord firstRecord = await records.FirstAsync().ConfigureAwait(false);
        object videoID = firstRecord["VideoID"]; //Should be 1000.
        // Instead, I get this exception:
        // "Invalid attempt to call MetaData when reader is closed."
    }
}

当代码尝试读取结果IDataReader(在object videoID = firstRecord["VideoID"];时),出现此异常:

  

关闭阅读器后,无效的调用MetaData的尝试。

这是因为SqlDataReader被处置了。有人可以提供一种推荐的方法来以异步方式枚举SqlDataReader,以便每个结果记录可供调用方法使用吗?谢谢。

4 个答案:

答案 0 :(得分:3)

在这种情况下,LINQ不是您的朋友,因为FirstAsync将在之前关闭迭代器,返回的结果不是ADO.NET所期望的。基本上:不要在这里使用LINQ,或者至少:不要这样。您可以使用Select之类的东西在序列仍处于打开状态时执行投影 ,或者将这里的所有工作卸载到Dapper之类的工具中可能会更容易。或者,手动进行:

await foreach (var record in records)
{
    // TODO: process record
    // (perhaps "break"), because you only want the first
}

答案 1 :(得分:1)

您可以通过不返回依赖于连接仍处于打开状态的对象来避免这种情况。例如,如果您只需要VideoID,则只需将其返回(我假设它是int):

public static async IAsyncEnumerable<int> GetRecordsAsync(string connectionString, SqlParameter[] parameters, string commandText, [EnumeratorCancellation]CancellationToken cancellationToken)
{
    ...
                    yield return reader["VideoID"];
    ...
}

或投影到自己的班级中:

public class MyRecord {
    public int VideoId { get; set; }
}

public static async IAsyncEnumerable<MyRecord> GetRecordsAsync(string connectionString, SqlParameter[] parameters, string commandText, [EnumeratorCancellation]CancellationToken cancellationToken)
{
    ...
                    yield return new MyRecord {
                        VideoId = reader["VideoID"]
                    }
    ...
}

或者按照Marc的建议,在第一个之后使用foreachbreak,在您的情况下看起来像这样:

IAsyncEnumerable<IDataRecord> records = GetRecordsAsync(connectionString, parameters, commandText, CancellationToken.None);
object videoID;
await foreach (var record in records)
{
    videoID = record["VideoID"];
    break;
}

答案 2 :(得分:1)

公开一个开放的DataReader时,将其与基础Connection一起关闭的责任现在属于调用者,因此您不应处置任何东西。相反,您应该使用接受DbCommand.ExecuteReaderAsync参数的CommandBehavior重载,并传递CommandBehavior.CloseConnection值:

  

执行命令时,如果关闭了关联的DataReader对象,则关闭了关联的Connection对象。

然后,您只希望调用者能够遵守规则并立即调用DataReader.Close方法,并且在对象被垃圾回收之前不会让连接打开。因此,公开DataReader公开应该被认为是一种极端的性能优化技术,应谨慎使用。

如果返回IEnumerable<IDataRecord>而不是IAsyncEnumerable<IDataRecord>,将会遇到同样的问题。

答案 3 :(得分:1)

要添加到其他答案中,您可以使实用程序方法通用,并添加投影委托console.log(link.url) 作为如下参数:

Func<IDataRecord, T> projection

然后在调用时传入lambda或引用这样的方法组:

public static async IAsyncEnumerable<T> GetRecordsAsync<T>(
    string connectionString, SqlParameter[] parameters, string commandText,
    Func<IDataRecord, T> projection, // Parameter here
    [EnumeratorCancellation] CancellationToken cancellationToken)
{
    ...
                    yield return projection(reader); // Projected here
    ...
}

诸如:

public static object GetVideoId(IDataRecord dataRecord)
    => dataRecord["VideoID"];