0

I am working on an internal console app that allows users to get a full set of results from our paged JSON API. We have many different types but all fall into the same base structure.

public class Tickets
{
    public Ticket[] tickets { get; set; }
    public string nextPageURL { get; set; }
    public string previousPageURL { get; set; }
    public int recordCount { get; set; }
}

public class Ticket
{
 ...
}

I have an Async task to make the call and loop through the paged results and fire the results into a single JSON file. But I'd like to make it generic instead of essentially the same code repeated 17 times, one for each type.

My current code is:

    private static async Task<List<Ticket>> GetTicketsAsync(Action<Tickets> callBack = null)
    {
        var tickets = new List<Ticket>();
        HttpClient httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Basic", 
            Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes($"-----")));
        httpClient.BaseAddress = new Uri("-----");
        var nextUrl = "/api/tickets";
        
        do
        {
            await httpClient.GetAsync(nextUrl)
                .ContinueWith(async (ticketSearchTask) =>
                {
                    var response = await ticketSearchTask;
                    if (response.IsSuccessStatusCode)
                    {
                        string jsonString = await response.Content.ReadAsStringAsync();
                        try
                        {
                            var result = JsonSerializer.Deserialize<Tickets>(jsonString);
                            if (result != null)
                            {
                                // Build the full list to return later after the loop.
                                if (result.tickets.Any())
                                    tickets.AddRange(result.tickets.ToList());

                                // Run the callback method, passing the current page of data from the API.
                                if (callBack != null)
                                    callBack(result);

                                // Get the URL for the next page
                                nextUrl = (result.nextPageURL != null) ? result.nextPageURL : string.Empty;
                            }
                        } catch (Exception ex)
                        {
                            Console.WriteLine($"\n We ran into an error: {ex.Message}");
                            nextUrl = string.Empty;
                        }
                       
                    }
                    else
                    {
                        // End loop if we get an error response.
                        nextUrl = string.Empty;
                    }
                });

        } while (!string.IsNullOrEmpty(nextUrl));
        return tickets;
    }

    private static void TicketsCallBack(Tickets tickets)
    {
        if (tickets != null && tickets.count > 0)
        {
            foreach (var ticket in tickets.tickets)
            {
                Console.WriteLine($"fetched ticket: {ticket.id}");
            }
        }
    }

I've made a start into a generic method but referencing the second level (In his instance the ticket object) is causing me problems as well as getting the nextPageURL. It would also be nice to keep the call back structure to keep the console ticking over with with the data that has been processed.

    private static async Task<List<T>> GetAsync<T>(string type, Action<T> callBack = null, string ticketId = null)
    {
        var results = new List<T>();
        HttpClient httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Basic",
            Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes($"-----")));
        httpClient.BaseAddress = new Uri("-----");
        var nextUrl = $"/api/{type}.json";

        do
        {
            await httpClient.GetAsync(nextUrl)
                .ContinueWith(async (searchTask) =>
                {
                    var response = await searchTask;
                    if (response.IsSuccessStatusCode)
                    {
                        string jsonString = await response.Content.ReadAsStringAsync();
                        try
                        {
                            var result = JsonSerializer.Deserialize<T>(jsonString);
                            if (result != null)
                            {
                                // Build the full list to return later after the loop.
                                if (result.tickets.Any())
                                     results.AddRange(result.tickets.ToList());

                                // Run the callback method, passing the current page of data from the API.
                                if (callBack != null)
                                    callBack(result);

                                // Get the URL for the next page
                                nextUrl = (result.GetType().GetProperty("nextPageURL") != null) ? result.GetType().GetProperty("nextPageURL").ToString() : string.Empty;
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine($"\nWe ran into an error: {ex.Message}");
                            nextUrl = string.Empty;
                        }


                    }
                    else
                    {
                        // End loop if we get an error response.
                        nextUrl = string.Empty;
                    }
                });

        } while (!string.IsNullOrEmpty(nextUrl));
        return results;
    }

Any help greatly appreciated.

3
  • 2
    I see several issues with your code here, even without getting to the generic part. First of all, I don't think you know what .ContinueWith means, you really don't need that here. Just await the GetAsync and use the result directly. Next is that you have so many levels of nesting here and that to me is a huge code smell. Commented Dec 10, 2024 at 16:52
  • The following may be of interest: using statement - ensure the correct use of disposable objects Commented Dec 10, 2024 at 17:26
  • Thats fair about the .ContinueWith thanks. Removed and it's happily still functioning as expected. Not sure I follow with the problem with the levels of nesting. They seem needed to me? - Aside from potentially the try catch. Other than that, with the removal of .ContinueWith it's essentially loop, check for valid response, work with the response object Commented Dec 10, 2024 at 17:38

1 Answer 1

0

To start with, if you have multiple objects following the same structure you could likely make that generic:

public class Paged<T>
{
    T[] Data { get;  set;}
    string nextPageURL { get;  set;}
    string previousPageURL { get; set;}
    int recordCount { get; set; }
}

You should be able to use it more or less in place of the tickets object:

private static async Task<List<T>> GetAsync<T>(string type, Action<Paged<T>> callBack = null)
    ...
    var result = JsonSerializer.Deserialize<Paged<T>>(jsonString);

If you cant use a common type due to backwards compatibility, you could still create a common interface for all similar classes:

public interface IPaged<T>
{
    public T[] Data{ get;}
    public string nextPageURL { get; set; }
    public string previousPageURL { get; set; }
    public int recordCount { get; set; }
}
public class Tickets : IPaged<Ticket>
{
    public Ticket[] Data => tickets;
    public Ticket[] tickets { get; set; }
    public string nextPageURL { get; set; }
    public string previousPageURL { get; set; }
    public int recordCount { get; set; }
}

private static async Task<List<TData>> GetAsync<T, TData>(string type, Action<T> callBack = null) where T : IPaged<TData>
{
...
     var result = JsonSerializer.Deserialize<T>(jsonString);
...
}
var allTickets = await GetAsync<Tickets, Ticket>(...);

However, I would consider using the IAsyncEnumerable interface instead, something like:

private static async IAsyncEnumerable<T[]> GetAsync<T>(string type){
   ...
   var result = JsonSerializer.Deserialize<Paged<T>>(jsonString);
   if (result != null)
   {
        yield return result.Data;
   }
   ...

That should provide cleaner separation between the code to fetch data, and the code that processes the data.

Sign up to request clarification or add additional context in comments.

3 Comments

If you go the IAsyncEnumerable route, I wouldn't yield return arrays. I would yield return individual elements by iterating over the received array.
@ILMTitan If you use Rx you can easily flatten the list with a selectMany if needed, and in some cases it might be useful with chunks of data, say when doing UI updates where you usually want to avoid doing updates for each individual item.
Amazing. Pointed me in the right direction. Thank you for answering the question I asked!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.