簡體   English   中英

如何使用 SqlDataReader 返回和使用 IAsyncEnumerable

[英]How to Return and Consume an IAsyncEnumerable with SqlDataReader

請看以下兩種方法。 第一個返回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"];處),我收到此異常:

閱讀器關閉時調用元數據的嘗試無效。

這是因為SqlDataReader已被釋放。 有人可以提供推薦的方法來以異步方式枚舉SqlDataReader以便調用方法可以使用每個結果記錄嗎? 謝謝你。

在這種情況下,LINQ 不是你的朋友,因為FirstAsync將在它返回結果之前關閉迭代器,這不是 ADO.NET 所期望的; 基本上:不要在這里使用 LINQ,或者至少:不是這樣。 您可能可以使用Select之類的東西在序列仍處於打開狀態時執行投影,或者將這里的所有工作卸載到像 Dapper 這樣的工具可能更容易。 或者,手動進行:

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

您可以通過不返回取決於仍然打開的連接的 object 來避免這種情況。 例如,如果您只需要VideoID ,則只需返回它(我假設它是一個int ):

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

或者投影到您自己的 class 中:

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;
}

當您公開一個打開的DataReader時,將其與基礎Connection一起關閉的責任現在屬於調用者,因此您不應處置任何東西。 相反,您應該使用接受CommandBehavior參數的DbCommand.ExecuteReaderAsync重載,並傳遞CommandBehavior.CloseConnection值:

執行命令時,關閉關聯的DataReader object時,關閉關聯的Connection object。

那你就只能希望調用者按規矩來,及時調用DataReader.Close方法,在object被垃圾回收之前不讓連接打開。 出於這個原因,公開一個開放的DataReader應該被認為是一種極端的性能優化技術,應該謹慎使用。

順便說一句,如果您返回IEnumerable<IDataRecord>而不是IAsyncEnumerable<IDataRecord> ,您也會遇到同樣的問題。

要添加到其他答案,您可以使實用程序方法通用並添加投影委托Func<IDataRecord, T> projection作為參數,如下所示:

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
    ...
}

然后在調用時傳入 lambda 或引用這樣的方法組:

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

這樣的:

GetRecordsAsync(connectionString, parameters, commandText, GetVideoId, CancellationToken.None);

在 2021 年末,我有這個確切的問題。 我找不到一個完整的例子,所以我只是把我能找到的東西弄亂了,直到我找到了一些工作。

這是我的完整代碼示例,雖然很簡單(因此您可以稍后對其進行擴展),並附有一些評論,詳細說明了我在此過程中遇到的一些坑:

// This function turns each "DataRow" into an object of Type T and yields
// it. You could alternately yield the reader itself for each row.
// In this example, assume sqlCommandText and connectionString exist.
public async IAsyncEnumerable<T> ReadAsync<T>( Func<SqlDataReader, T> func )
{
    // we need a connection that will last as long as the reader is open,
    // alternately you could pass in an open connection.
    using SqlConnection connection = new SqlConnection( connectionString );
    using SqlCommand cmd = new SqlCommand( sqlCommandText, connection );

    await connection.OpenAsync();
    var reader = await cmd.ExecuteReaderAsync();
    while( await reader.ReadAsync() )
    {
        yield return func( reader );
    }
}

然后在您的(異步)代碼的任何其他部分,您可以在await foreach循環中調用您的 function:

private static async Task CallIAsyncEnumerable()
{
    await foreach( var category in ReadAsync( ReaderToCategory ) )
    {
        // do something with your category; save it in a list, write it to disk,
        // make an HTTP call ... the whole world is yours!
    }
}

// an example delegate, which I'm passing into ReadAsync
private static Category ReaderToCategory( SqlDataReader reader )
{
    return new Category()
    {
        Code = ( string )reader[ "Code" ],
        Group = ( string )reader[ "Group" ]
    };
}

我發現的其他幾件事:您不能從tryyield ,但您可以將所有內容(包括) cmd.ExecuteReaderAsync()try或返回 DataReader 的單獨方法中。 或者您可以將await foreach包裝在try塊中; 我認為問題在於向嘗試之外的調用者讓步(這是有道理的,在你考慮之后)。

如果您使用另一種方法生成閱讀器,請將連接傳遞給該方法,以便您控制其生命周期。 如果您的方法創建連接、執行命令並返回SqlDataReader ,則連接將在您從閱讀器讀取之前關閉(如果您使用了“使用”)。 再一次,如果你仔細想想,這很有意義,但它讓我絆倒了幾分鍾。

祝你好運,我希望這對其他人有所幫助!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM