如何使用 SqlDataReader 返回和使用 IAsyncEnumerable

use*_*179 3 .net c# sqldatareader async-await 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."
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Mar*_*ell 6

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

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


Gab*_*uci 6

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

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

或者投影到你自己的类中:

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"]
                    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

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

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