Инверсия зависимости и System.Data.Common.DbDataReader

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Если мы не используем EF (такое случается), то нам нужно как-то устроить загрузку объектов из базы данных. Вариант: берём DataSet, делаем ему SomeDataAdapter.fill(...), а из него берём данные для строительства нужных объектов. При этом класс, который умеет заполнять DataSet, не знает, для объектов какого класса он это делает. Абстракция, низкая связанность, всё хорошо.

Однако, мы ждём, пока заполнится DataSet, только после этого можем начать отправку ответа клиенту.

Ранее в публикации JSON-сериализация асинхронного стрима мы рассмотрели способ, как отправлять данные по мере поступления с помощью SomeDataReader (например, OracleDataReader). Там был приведён пример:

private async IAsyncEnumerable<ICatForListing> GetCatsList()
{
		using OracleConnection conn = GetConnection();

    var taskOpenConnection = conn.OpenAsync();

    OracleCommand command = conn.CreateCommand();
    command.CommandText = "select STRING_ID, INT_ID, breed, name from T_CATS";

    await taskOpenConnection;

    OracleDataReader dataReader = (OracleDataReader) await command.ExecuteReaderAsync();

    while (true)
    {
    		Task<bool> canRead = dataReader.ReadAsync();
        Cat item = new();
        item.Id = new StringIntId();
        if(! await canRead)
        {
        		yield break;
        }
        item.Id.StringId = dataReader["STRING_ID"].ToString()!.Trim();
        item.Id.IntId = int.Parse(dataReader["INT_ID"].ToString()!.Trim());
        item.Breed = dataReader["BREED"].ToString()!.Trim();
        item.Name = dataReader["NANE"].ToString()!.Trim();
        yield return item;
    }
}

...
  
public async Task GetCats(HttpContext httpContext)
{
		await httpContext.Response.WriteAsJsonAsync(
      GetCatsList(), 
      new JsonSerializerOptions { PropertyNameCaseInsensitive = false }
    );
}

И так делать, конечно, некрасиво. Потому что в таком случае у нас класс, который умеет работать с базой данных зависит от интерфейса ICatForListing и класса Cat, относящихся к бизнес-модели.

Можно попробовать получить абстрактный DbDataReader и крутить его уже на более высоком уровне:

// низкоуровневый класс, работающий с базой данных
public class Database 
{
  	public async Task<DbDataReader> GetCatsDataReader()
    {
      	using OracleConnection conn = GetConnection();
		    var taskOpenConnection = conn.OpenAsync();
    		OracleCommand command = conn.CreateCommand();
    		command.CommandText = "select STRING_ID, INT_ID, breed, name from T_CATS";
    		await taskOpenConnection;
    		return (DbDataReader) await command.ExecuteReaderAsync();
    }
}

// контроллер ASP.NET
public class CatsController 
{
  	private Database _database = new();
  
  	private async IAsyncEnumerable<ICatForListing> GetCatsList()
		{
      	using DbDataReader dataReader = await _database.GetCatsDataReader();
        while (true)
        {
            Task<bool> canRead = dataReader.ReadAsync();
            Cat item = new();
            item.Id = new StringIntId();
            if(! await canRead)
            {
                yield break;
            }
            item.Id.StringId = dataReader["STRING_ID"].ToString()!.Trim();
            item.Id.IntId = int.Parse(dataReader["INT_ID"].ToString()!.Trim());
            item.Breed = dataReader["BREED"].ToString()!.Trim();
            item.Name = dataReader["NANE"].ToString()!.Trim();
            yield return item;
        }
    }
  
  	...
  
    public async Task GetCats(HttpContext httpContext)
    {
        await httpContext.Response.WriteAsJsonAsync(
          GetCatsList(), 
          new JsonSerializerOptions { PropertyNameCaseInsensitive = false }
        );
    }
}  

Оказывается, это не работает:

System.InvalidOperationException : Invalid attempt to call Read when reader is closed.

Это из-за using

using OracleConnection conn = GetConnection();

При выходе из метода соединение закрывается, так как вызывается Dispose(). Придётся using убрать:

// низкоуровневый класс, работающий с базой данных
public class Database 
{
  	public async Task<DbDataReader> GetCatsDataReader()
    {
      	/*using*/ OracleConnection conn = GetConnection();
		    var taskOpenConnection = conn.OpenAsync();
    		OracleCommand command = conn.CreateCommand();
    		command.CommandText = "select STRING_ID, INT_ID, breed, name from T_CATS";
    		await taskOpenConnection;
    		return (DbDataReader) await command.ExecuteReaderAsync();
    }
}

Теперь котики выводятся, но у нас висит незакрытое соединение, что также некрасиво.

И тут нас посещает идея: а давайте возвращать тот же DbDataReader, но не целиком, а на каждой строке данных через IAsyncEnumerator<DbDataReader>! Это означает, что мы останемся внутри метода GetCatsDataReader() до конца данных, соединение закроется правильно и наша первоначальная цель также будет достигнута. Итак:

// низкоуровневый класс, работающий с базой данных
public class Database 
{
  	public async IAsyncEnumerable<DbDataReader> GetCatsDataReader()
    {
      	using OracleConnection conn = GetConnection();
		    var taskOpenConnection = conn.OpenAsync();
    		OracleCommand command = conn.CreateCommand();
    		command.CommandText = "select STRING_ID, INT_ID, breed, name from T_CATS";
    		await taskOpenConnection;
      	using OracleDataReader dr =  (OracleDataReader) await cmd.ExecuteReaderAsync();

        while (await dr.ReadAsync())
        {
            yield return (DbDataReader)dr;
        }
    }
}

// контроллер ASP.NET
public class CatsController 
{
  	private Database _database = new();
  
  	private async IAsyncEnumerable<ICatForListing> GetCatsList()
		{
        await foreach (DbDataReader dataReader in GetCatsDataReader())
        {
            Cat item = new();
            item.Id = new StringIntId();
            item.Id.StringId = dataReader["STRING_ID"].ToString()!.Trim();
            item.Id.IntId = int.Parse(dataReader["INT_ID"].ToString()!.Trim());
            item.Breed = dataReader["BREED"].ToString()!.Trim();
            item.Name = dataReader["NANE"].ToString()!.Trim();
            yield return item;
        }
    }
  
  	...
  
    public async Task GetCats(HttpContext httpContext)
    {
        await httpContext.Response.WriteAsJsonAsync(
          GetCatsList(), 
          new JsonSerializerOptions { PropertyNameCaseInsensitive = false }
        );
    }
}  

Проверено, работает!

Источник: https://habr.com/ru/post/662976/


Интересные статьи

Интересные статьи

Всем привет. Текст состоит из двух частей:1. Небольшая шпаргалка по параметрам настроек по умолчанию;2. Текст о том, почему вообще существование такой шпаргалки может кому-то понадобится.
В этой статье мы расскажем, как оптимизировать крупный проект в «Битрикс24» и увеличить его производительность в 3 раза, изменяя настройки MySQL и режим питания CPU. Дано Корпоративн...
От скорости сайта зависит многое: количество отказов, брошенных корзин. Согласно исследованию Google, большинство посетителей не ждёт загрузки больше 3 секунд и уходит к конкурентам. Бывает, что сайт ...
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...
На протяжении тысяч лет человечество стремилось в небеса, желая уподобиться птицам. Многие великие умы, от Леонардо да Винчи до братьев Райт, проектировали самые разнообразные летательные апп...