1

In a Maui Xaml app, i have a page and its viewmodel with an ObservableCollection of items that i want to retrieve with an api call. I want to have the api call in another service where all those same api calls are placed. I'm using Maui CommunityToolkit mvvm etc by the way, so the InitializeCommand is binded to the UI with the eventToCommmandBehaviour OnAppearing.

What I want to archieve is that items get added while they arrives and not in a block like normally reading the response and materializing the items in an array.

In the view model I have:

private async Task InitializeCommand()
{
    items.Clear();
    var result = await _apiService.GetItemsAsync();
    await foreach(var item in result)
    {
        items.Add(item);
    }
}

The apiService method does this:

public async Task<IAsyncEnumerable<Item>> GetItemsAsync()
{
     using var client = httpClientFactory.CreateClient();
     using var response = await client
        .GetAsync("myUrl", HttpCompletionOption.ResponseHeadersRead)
     using var stream = await response.Content.ReadAsStreamAsync();
     return JsonSerializer.DeserializeAsyncEnumerable<Item>(stream);
}

Now there is a problem in this method, with the using keyword. First of all i'm not sure if I need to specify it 3 times, for the client, the response and the stream. But this aside, by disposing the response as soon as it goes out of scope, you get an exception when consuming it in the InitializeCommand method! I'm not sure how to dispose of all of this request in the proper manner, without doing it on the caller side. Also does calling await.Response.Content.ReadAsStreamAsync(); wait for all the stream to be read before continuing? I don't want that, i want the deserialization to the IAsyncEnumerable to start as soon the headers are read. Should i return the stream and deserialize it in the calling method and then dispose of it?

  • `ReadAsStreamAsync` doc says *"The returned Task object will complete after all of the stream that represents content has been read."* Sounds like the behavior you DON'T want. Though I do see mention of subclasses being able to implement differently, so it is not clear what an HttpResponse does. Can you write your own `IAsyncEnumerable` method, that does not end until the response is finished? So Dispose doesn't happen until source exhausted. Returning `IEnumerable` that would be `foreach (var item ...) yield return item;`, but I don't know how to do that for `IAsyncEnumerable`. – ToolmakerSteve Nov 20 '22 at 23:03

2 Answers2

0

i want the deserialization to the IAsyncEnumerable to start as soon the headers are read

Did you mean that you want to run the following two lines code at the same time?

using var stream = await response.Content.ReadAsStreamAsync();
return JsonSerializer.DeserializeAsyncEnumerable<Item>(stream);

That should be impossible, the principal is just like the read-write lock. An object can't be written and read at the same time. The first line code is writing the variable stream and the second line code is reading it.

I'm not sure how to dispose of all of this request in the proper manner

About the using keyword and the object dispose, you can refer to this case. If you don't want to use it, you can pass the object var stream to the method you want to use it again and call the stream.Dispose() to deal with it.

Liyun Zhang - MSFT
  • 8,271
  • 1
  • 2
  • 14
0

Your dispose issues are likely because you're using using (not async-aware) instead of await using (async-aware).

The following code works out of the box in net7.0:

public async IAsyncEnumerable<T?> GetItemsAsync<T>(string url)
{
    using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
    await using Stream stream = await response.Content.ReadAsStreamAsync();

    await foreach (T? item in JsonSerializer.DeserializeAsyncEnumerable<T>(stream, serializerOptions))
    {
        yield return item;
    }
}

In net6.0, however, the buffering works differently, and it doesn't stream! I did not dig into why, but I suspect there is a default configuration difference (or a bug fix) between the two.

Full working sample here (client, server, contracts): https://github.com/prosser/UsefulSamples/tree/main/StreamingIAsyncEnumerable