В то время как на IDataReader.Read не работает с возвратом доходности, но foreach on reader делает

Это широко распространенный шаблон ADO.NET для извлечения данных из базы данных с помощью устройства чтения данных, но, как ни странно, он не работает.

Не работает:

public static IEnumerable<IDataRecord> SelectDataRecord<T>(string query, string connString)
                                                          where T : IDbConnection, new()
{
    using (var conn = new T())
    {
        using (var cmd = conn.CreateCommand())
        {
            cmd.CommandText = query;
            cmd.Connection.ConnectionString = connString;

            cmd.Connection.Open();
            using (var reader = (DbDataReader)cmd.ExecuteReader())
            {
                // the main part
                while (reader.Read())
                {
                    yield return (IDataRecord)reader;
                }
            }
        }
    }

Это работает:

public static IEnumerable<IDataRecord> SelectDataRecord<T>(string query, string connString)
                                                          where T : IDbConnection, new()
{
    using (var conn = new T())
    {
        using (var cmd = conn.CreateCommand())
        {
            cmd.CommandText = query;
            cmd.Connection.ConnectionString = connString;

            cmd.Connection.Open();
            using (var reader = (DbDataReader)cmd.ExecuteReader())
            {
                // the main part
                foreach (var item in reader.Cast<IDataRecord>())
                {
                    yield return item;
                }
            }
        }
    }

Единственное соответствующее изменение, которое я вижу, это то, что в первом коде итератор возвращается из цикла while, а во втором - из цикла foreach.

Я называю это следующим образом:

// I have to buffer for some reason
var result = SelectDataRecord<SQLiteConnection>(query, connString).ToList(); 

foreach(var item in result)
{
    item.GetValue(0); // explosion
}

Я пробовал с SQLite коннектор .NET, а также MySQL. Результат тот же, то есть первый подход сбой, второй успешный.

Exception

SQLite

Необработанное исключение типа "Исправление System.InvalidOperationException" произошло в System.Data.SQLite.dll. Дополнительная информация: Нет текущей строки

MySQL

Необработанное исключение типа "System.Exception" произошло в MySql.Data.dll. Дополнительная информация: Нет текущего запроса в считывателе данных

Это из-за различий в реализации между reader.Read и reader.GetEnumerator в конкретных соединителях ADO.NET? Я не видел заметной разницы, когда я проверил источник проекта System.Data.SQLite, GetEnumerator вызывает Read внутренне. В идеале я предполагаю, что в обоих случаях ключевое слово yield предотвращает нетерпеливое выполнение метода, и циклы должны выполняться только после того, как перечисление перечислили извне.


Обновление:

Я использую этот шаблон как безопасный (по существу такой же, как и второй подход, но немного менее подробный),

using (var reader = cmd.ExecuteReader())
    foreach (IDataRecord record in reader as IEnumerable)
        yield return record;

Ответы

Ответ 1

Различие между двумя примерами состоит в том, что foreach имеет различную семантику из while, которая представляет собой простой цикл. Ниже приведено значение GetEnumerator of foreach.

Как говорит Джоэл, в первом примере, тот же объект-читатель получает на каждой итерации цикла while. Это связано с тем, что оба IDataReader, а также IDataRecord здесь одинаковы, что является неудачным. Когда в результирующей последовательности вызывается a ToList, завершение завершается, после чего блоки using закрывают объекты чтения и соединения, и вы получаете список расположенных объектов-читателей той же ссылки.

Во втором примере, устройство чтения foreach обеспечивает получение копии IDataRecord. GetEnumerator выполняется следующим образом:

public IEnumerator GetEnumerator()
{
    return new DbEnumerator(this); // the same in MySQL as well as SQLite ADO.NET connectors
}

где MoveNext класса System.Data.Common.DbEnumerator реализуется следующим образом:

IDataRecord _current;

public bool MoveNext() // only the essentials
{
    if (!this._reader.Read())
        return false;

    object[] objArray = new object[_schemaInfo.Length];
    this._reader.GetValues(objArray); // caching into obj array
    this._current = new DataRecordInternal(_schemaInfo, objArray); // a new copy made here
    return true;
}

DataRecordInternal - это фактическая реализация IDataRecord, которая получается из foreach, которая не является той же ссылкой, что и читатель, но кэшированной копией всех значений строки/записи.

System.Linq.Cast в этом случае является простым сохранением представления, которое ничего не делает для общего эффекта. Cast<T> будет реализован следующим образом:

public static IEnumerable<T> Cast<T>(this IEnumerable source)
{
    foreach (var item in source)
        yield return (T)item; // representation preserving since IDataReader implements IDataRecord
}

Можно показать пример без вызова Cast<T>, чтобы не выявить эту проблему.

using (var reader = cmd.ExecuteReader())
    foreach (var record in reader as IEnumerable)
        yield return record;

Приведенный выше пример просто отлично работает.


Важным отличием является то, что первый пример проблематичен, только если вы не используете значения, считанные из базы данных, в его первом перечислении. Это только последующие перечисления, которые бросаются, поскольку к тому времени читатель будет удален. Например,

using (var reader = cmd.ExecuteReader())
    while (reader.Read())
        yield return reader;

...
foreach(var item in ReaderMethod())
{
    item.GetValue(0); // runs fine
} 

...
foreach(var item in ReaderMethod().ToList())
{
    item.GetValue(0); // explosion
} 

Ответ 2

Это не while vs foreach, что делает разницу. Это вызов .Cast<T>().

В первом примере вы получаете один и тот же объект на каждой итерации цикла while. Если вы не будете осторожны, вы закончите заполнение итератора производительности до фактического использования данных, и DataReader уже будет удален. Это может произойти, если вы хотите, например, вызвать .ToList() после вызова этого метода. Лучшее, на что вы могли надеяться, было бы для каждой записи в списке иметь такое же значение.
(Pro tip: большую часть времени вы не хотите вызывать .ToList(), пока вам не понадобится. Лучше просто работать с записями IEnumerable).

Во втором примере, когда вы вызываете .Cast<T>() в datareader, вы фактически делаете копию данных по мере их повторения через каждую запись. Теперь вы больше не уступаете одному и тому же объекту.