Введение

Можно считать это продолжением публикации Кастомный JsonConverter: уменьшаем связность и экономим ресурсы. Там при рассмотрении списков верхнего уровня упор был сделан на десериализацию из JSON. Но чтобы что-то десериализовать, нужно сначала это сериализовать. Посмотрим, чем нам может помочь возможность сериализации IAsyncEnumerable<T> объекта.

Хорошая новость...

... заключается в том, что System.Text.Json.JsonSerializer.SerializeAsync(...) делает это без какого-либо кастомного конвертера, "из коробки".

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

Было:

/// <summary>
///     Метод, возвращающий всех кошек сразу.
/// </summary>
public static async Task GetCats(HttpContext httpContext, int count, 
                                      double delay)
{
		List<Cat> cats = new();
  	await foreach(Cat cat in GenerateManyCats(count, delay))
  	{
  			cats.Add(cat);
    }
  	await httpContext.Response.WriteAsJsonAsync<List<Cat>>(cats);
}

Стало:

public static async Task GetCats(HttpContext httpContext, int count, 
                                 double delay)
{
		await httpContext.Response.WriteAsJsonAsync(
      GenerateManyCats(count, delay), 
      new JsonSerializerOptions { PropertyNameCaseInsensitive = false }
    );
}

Если задача заключается просто в передаче списка на клиента, то можно это делать прямо из базы данных или откуда-то ещё, откуда берутся данные.

Например:

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

    var taskOpenConnection = conn.OpenAsync();

    OracleCommand command = conn.CreateCommand();
    command.CommandText = "select 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 }
    );
}

JsonSerializer.SerializeAsync() и PartialLoader

Описанный в публикации Как не дать пользователю заснуть во время загрузки большого набора данных PartialLoader теперь тоже реализует интерфейсIAsyncEnumerable<T>, поэтому можно проще записать метод, возвращающий большой список котиков партиями:

public static async Task GetCatsJson(HttpContext context, int count, 
                                     int timeout, int paging, double delay)
{
    PartialLoader<Cat> partialLoader;
    string key = null!;

    // Получаем хранилище через механизм внедрения зависимостей.
    CatsLoaderStorage loaderStorage = 
      context.RequestServices.GetRequiredService<CatsLoaderStorage>();

    if (!context.Request.Headers.ContainsKey(
      Constants.PartialLoaderSessionKey))
    {
        // Если это первый запрос, то создаём PartialLoader и стартуем 
        // генерацию.
        partialLoader = context.RequestServices.
          GetRequiredService<PartialLoader<Cat>>()
            .SetTimeout(TimeSpan.FromMilliseconds(timeout))
            .SetPaging(paging)
            .SetDataProvider(GenerateManyCats(count, delay))
            .SetIsNullEnding(true)
        ;
        key = Guid.NewGuid().ToString();
        loaderStorage.Data[key] = partialLoader;
    }
    else
    {
        // Если это последующий запрос, то берём PartialLoader из хранилища и 
      	// продолжаем генерацию.
        key = context.Request.Headers[Constants.PartialLoaderSessionKey];
        partialLoader = loaderStorage.Data[key];
    }

    JsonSerializerOptions jsonOptions = new JsonSerializerOptions { 
      PropertyNameCaseInsensitive = false };
    jsonOptions.Converters.Add(new TransferJsonConverterFactory(
      context.RequestServices)
        .AddTransient<ICat>());

		// Добавляем заголовок ответа с идентификатором серии запросов.
    context.Response.Headers.Add(Constants.PartialLoaderSessionKey, key);

    // Получаем порцию данных, одновременно записывая их в поток
    await context.Response.WriteAsJsonAsync(partialLoader, jsonOptions).ConfigureAwait(false);

    if (partialLoader.State is PartialLoaderState.Full)
    {
        if (key is { })
        {
            loaderStorage.Data.Remove(key);
        }
    }
}

Следует обратить внимание, что когда мы начинаем записывать данные в выходной поток, мы ещё не знаем, последняя это партия или нет, поэтому не можем установить соответствующий заголовок. Мы сообщаем клиенту, что партия последняя, передавая null последним элементом последней партии. Для этого мы устанавливаем в true флаг IsNullEnding:

partialLoader = context.RequestServices.
          GetRequiredService<PartialLoader<Cat>>()
            .SetTimeout(TimeSpan.FromMilliseconds(timeout))
            .SetPaging(paging)
            .SetDataProvider(GenerateManyCats(count, delay))
            .SetIsNullEnding(true) // последний элемент последней партии
// будет null  					
        ;

Соответственно, клиент должен проверять последний элемент каждой партии на предмет его равенства null и принимать решение о продолжении запросов.

В случае использования TransferJsonConverterFactory, о котором рассказано в Кастомный JsonConverter: уменьшаем связность и экономим ресурсы, эта проверка в него уже встроена:

...
var converter = new TransferJsonConverterFactory(null)
                    .AddTransient<ICatForListing, Cat>();
JsonSerializerOptions jsonOptions = new JsonSerializerOptions { 
  	PropertyNameCaseInsensitive = false };
jsonOptions.Converters.Add(converter);

...
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, 
                                                    "/path/params");
HttpResponseMessage response = await _client.SendAsync(request);
while (response is { } 
       && response.StatusCode == System.Net.HttpStatusCode.OK 
       && !converter.EndOfData)
{
		converter.Target = Cats;
    await JsonSerializer.DeserializeAsync<AppendableList<ICat>>(
      response.Content.ReadAsStream(),
                                    jsonOptions);
  	request = new HttpRequestMessage(HttpMethod.Get, "/path");
  	request.Headers.Add(Constants.PartialLoaderSessionKey, 
                        response.Headers.GetValues(
                          Constants.PartialLoaderSessionKey).First());
  	response = await _client.SendAsync(request);
}

Ссылки

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