Avalonia, Foreach of an API request with HttpClient

95 Views Asked by At

I currently have an API request that I make with Avalonia C# and I manage to return the entire JSON as a result but cannot retrieve each element in a foreach. Here is the current code:

// Get servers infos
private async Task GetServersInfos()
{

    using (var httpClient = new HttpClient())
    {
        using (var request = new HttpRequestMessage(new HttpMethod("GET"), "http://example.com/api"))
        {
            var response = await httpClient.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                string jsonObject = JsonSerializer.Serialize(response.Content);
                var results = await response.Content.ReadAsStringAsync();

                foreach (var result in results)
                {
                    TestText.Text = result["name"];
                }

            }
        }
    }
}

And the JSON result:

{
    "1": {
        "name": "Example 1",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit...",
    },
    "2": {
        "name": "Example 2",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit...",
    }
}

But I have the following error: Unable to apply indexing using [] to an expression of type 'char'.

2

There are 2 best solutions below

1
BionicCode On BEST ANSWER

You must use JsonSerializer.DeserializeAsync to convert the known JSON to a C# data type or use JsonDocument to convert the unknown JSON response to a read-only DOM model and traverse its element nodes.

To improve the performance, you should pass a Stream to JsonSerializer.SeserializeAsync as this eliminates one complete enumeration of the response message. To issue a HTTP GET request, simply use one of the Get... methods provided by the HttpClient API.
In our case we pick the HttpClient.GetStreamAsync method.

The response message is a single JSON object (and not an array) that is constructed of key-value pairs. That makes a list of key-value pairs. Which is exactly how the Dictionary structure is built. That's a crucial finding as this means we can deserialize every well-formed JSON object into a Dictionary<string, object>.
The only special case is if the JSON contains an array. In this case we must tell the JsonSerializer how to convert it, which means we have to create a custom JsonConverter. In any other case we can always use Dictionary<string, string> to deserialize or serialize a JSON object.
However, to handle complex unknown JSON content the recommended way would be to use JsonDocument.

Example 1

Deserialize a known simple JSON that contains no arrays in a very general manner using JsonSerializer (doesn't require a custom data structure).

In your example case we expect a list of nested JSON objects, which can be represented a nested Dictionary in the form of Dictionary<string, Dictionary<string, string>>:

// Get server JSON response as Stream
private async Task GetServersInfos()
{
  using var httpClient = new HttpClient()
  string url = "http://example.com/api";
  await using Stream responseStream = await httpClient.GetStreamAsync(url);;

  var serializerOptions = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
  Dictionary<string, Dictionary<string, string>>? responseData = await JsonSerializer.DeserializeAsync<Dictionary<string, Dictionary<string, string>>>(responseStream, serializerOptions);

  /* * * * * * * * * * * * * * * * * * * * * * * * *
   *                                               *
   *  Example how to handle the response object    *
   *                                               *
   * * * * * * * * * * * * * * * * * * * * * * * * */

  Dictionary<string, string> firstDataEntry = responseData["1"];
  string nameValue = firstDataEntry["name"]; // "Example 1"
  string descriptionValue = firstDataEntry["description"]; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."

  Dictionary<string, string> secondDataEntry = responseData["2"];
  nameValue = secondDataEntry["name"]; // "Example 2"
  descriptionValue = secondDataEntry["Description"]; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."

  // Or enumerate the response JSON:
  foreach (KeyValuePair<string, Dictionary<string, string>> entry in responseData)
  {
    string jsonObjectPropertyName = entry.Key; // "1"
    Dictionary<string, string> nestedJsonObject = entry.Value;
    foreach (KeyValuePair<string, string> nestedObjectEntry in nestedJsonObject)
    {
      jsonObjectPropertyName = nestedObjectEntry.Key; // "name" or "description"
      string jsonObjectPropertyValue = nestedObjectEntry.Value; // "Example 1" or "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."

      // For example, only get the "description" value
      if (nestedObjectEntry.Key.Equals("Description", StringComparison.OrdinalIgnoreCase))
      {
        string descriptionText = nestedObjectEntry.Value; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
      }
    }
  }
}

Example 2

Deserialize a known JSON that contains no arrays in a very specialized manner using JsonSerializer.

In your example case we expect a list of nested JSON objects, which enables us to create a C# type for improved convenience in terms of data handling:

DataSet.cs ``c# public class DataSet { public string Name { get; } public string Description { get; } }

```c#
// Get server JSON response as Stream
private async Task GetServersInfos()
{
  using var httpClient = new HttpClient()
  string url = "http://example.com/api";
  await using Stream responseStream = await httpClient.GetStreamAsync(url);

  var serializerOptions = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
  Dictionary<int, DataSet>? responseData = await JsonSerializer.DeserializeAsync<Dictionary<int, DataSet>>(responseStream, serializerOptions);

  /* * * * * * * * * * * * * * * * * * * * * * * * *
   *                                               *
   *  Example how to handle the response object    *
   *                                               *
   * * * * * * * * * * * * * * * * * * * * * * * * */

  DataSet firstDataEntry = responseData[1];
  string nameValue = firstDataEntry.Name; // "Example 1"
  string descriptionValue = firstDataEntry.Description; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."

  DataSet secondDataEntry = responseData[2];
  nameValue = secondDataEntry.Name; // "Example 2"
  descriptionValue = secondDataEntry.Description; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
}

Example 3

Deserialize an unknown JSON using JsonDocument.

We can traverse and inspect every JSON using the JSON's read-only DOM. If we need to manipulate the JSON document we would use JsonNode instead.
However, JsonDocument seems to be sufficient and it is faster than JsonNode:

// Get server JSON response as Stream
private async Task GetServersInfos()
{
  using var httpClient = new HttpClient()
  string url = "http://example.com/api";
  await using Stream responseStream = await httpClient.GetStreamAsync(url);
  using JsonDocument responseJson = await JsonDocument.ParseAsync(responseStream);

  /* * * * * * * * * * * * * * * * * * * * * * * * *
   *                                               *
   *  Example how to handle the response object    *
   *                                               *
   * * * * * * * * * * * * * * * * * * * * * * * * */

  // Because the response is an JSON object and not a JSON array
  // we have to call EnumerateObject instead of EnumerateArray
  if (responseJson.RootElement.ValueKind is JsonValueKind.Object)
  {
    foreach (JsonProperty jsonObject in responseJson.RootElement.EnumerateObject())
    {
      string propertyName = jsonObject.Name; // "1" and "2"
      JsonElement nestedObject = jsonObject.Value;
  
      // Enumerate the JSON object, which has two properties "name" and "description",
      // property by property (makes for two iterations)
      if (nestedObject.ValueKind is JsonValueKind.Object)
      {
        foreach (JsonProperty dataEntry in nestedObject.EnumerateObject())
        {
          string entryName = dataEntry.Name; // "name" and "description"
          string? entryValue = dataEntry.Value.GetString(); // "Example 1" and "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."

          // For example, only get the "description" propertyValue
          if (dataEntry.Name.Equals("Description", StringComparison.OrdinalIgnoreCase))
          {
            string descriptionText = dataEntry.Value.GetString();
          }
        }
      }
    }
  }
}
7
Christian Lachmann On

ReadAsStringAsync returns a simple string and not a list of objects. That's why you can't iterate over it. You can create a class for your content and let the serializer create a list of instances of your class and then you can iterate over it in a foreach.

Example:

Possible Business Objects:

public class data
{
    public string name { get; set; }
    public string description { get; set; }
}

public class Root
{
    public ICollection<Data> Data { get; set; }
}

Deserialze the to Business Objects:

var results = await response.Content.ReadAsStringAsync();
var deserialized = JsonSerializer.Deserialize<Root>(results);

foreach (var d in deserialzed.Data)
   ...