[英]How to return ActionResult along with async foreach and 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 的建議做,在第一個之后使用foreach
和break
,在你的情況下看起來像這樣:
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" ]
};
}
我發現的其他幾件事:您不能從try
中yield
,但您可以將所有內容(包括) cmd.ExecuteReaderAsync()
到try
或返回 DataReader 的單獨方法中。 或者您可以將await foreach
包裝在try
塊中; 我認為問題在於向嘗試之外的調用者讓步(這是有道理的,在你考慮之后)。
如果您使用另一種方法生成閱讀器,請將連接傳遞給該方法,以便您控制其生命周期。 如果您的方法創建連接、執行命令並返回SqlDataReader
,則連接將在您從閱讀器讀取之前關閉(如果您使用了“使用”)。 再一次,如果你仔細想想,這很有意義,但它讓我絆倒了幾分鍾。
祝你好運,我希望這對其他人有所幫助!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.