Если мы не используем 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 }
        );
    }
}  

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

Комментарии (1)


  1. fedorro
    26.04.2022 16:06
    +1

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

    Возвращать из GetCatsDataReader MyDbDataReader, в котором есть ссылки на DbDataReader и на Connection, и в его Dispose закрывать и то и другое.