0

I'm working on a .NET Core application that utilizes EventBrite's API. Within the EventBrite API, many result sets are paginated. My use case requires me to retrieve the full results for multiple sets of data - Events, Attendees, etc.

Rather than have the same logic to handle each Paginated result set, I figured I could create a generic method to loop through the paginated results and get my results back - something like the following.

private List<T> GetPaginatedResult<T>(string path) where T : class
{
    var firstResult = GetEventBriteResult<PaginatedResponse<T>>(path);
    var pages = firstResult.Pagination.page_count;
    var results = firstResult.Objects;
    if (pages > 1)
    {
        for (int i = 1; i < pages; i++)
        {
            var paginatedPath = path + $"?page={i + 1}";
            var paginatedResult = GetEventBriteResult<PaginatedResponse<T>>(paginatedPath);
            results.AddRange(paginatedResult.Objects);
        }
    }
    return results;
}

Each of the EventBrite result sets contains a 'Pagination' object, as well as the list of the current pages datasets.

I created a class to handle the Paginated results, where the EventBritePagination class matches the EventBrite class to handle pagination. The Objects property is where I'm having issues.

public class PaginatedResponse<T> where T : class
{
    public EventBritePagination Pagination { get; set; } = new EventBritePagination();
    public virtual List<T> Objects { get; set; } = new List<T>();
}

The issue is that EventBrite has custom naming conventions for each of their classes. For example, looking at the 'Event' class and the 'Attendee' class, they would look like the following if I had built them out manually.

public class EventBriteEvent {
    EventBritePagination Pagination { get; set; }
    List<Event> Events { get; set; }
}

public class EventBriteAttendee {
    EventBritePagination Pagination { get; set; }
    List<Attendee> Attendees { get; set; }
}

Each class has the 'Pagination' object, but the Properties I'm attempting to map to the list of 'Objects' has a different name for each object type.

So when I go to deserialize the response, I end up needing to define multiple JsonPropertyNames in order to facilitate the fact that my Objects property may be named 'attendees' or 'events' or similar.

I know there's got to be a better way to do this with Generic types, but they aren't my strong suit.

Is there a way that I can define a class with a Generic property that can deserialize from a variety of JsonPropertyNames? Or a way to achieve the end goal with another method of inheritance?

Edit 1

For abundance of clarity, this is what the actual JSON response from EventBrite looks like.

First, the result for Attendees

{
  "pagination": {
    "page_number": 1,
    "page_count": 1
  },
  "attendees": [
    { "first" : "Jeff", ... }, 
    { "first" : "John", ... }
  ]

And secondly, the Events...

{
  "pagination": {
    "page_number": 1,
    "page_count": 1
  },
  "events": [
    { "name" : "Anime NebrasKon 2014", ... }, 
    { "name" : "Anime NebrasKon 2015", ... }
  ]

Both responses contain the 'Pagination' property, but differing second attributes. The second attribute will always be a List of objects, however.

My goal is to build a reusable method where I can grab either the Attendees or the Events (or any of the paginated results from EventBrite)

The biggest issue is that because the secondary attributes are named differently, I can't reference the specific secondary attribute that I need to - and if I keep it generic, as a List<T> Objects, then I can't deserialize the Objects list using JsonPropertyNames, as I can only specify one name, not multiple.

Chris Hobbs
  • 745
  • 2
  • 9
  • 27
  • is it possible to refactor your question, to simulate(mock) the `EventBrite API` in a few simple classes? then the readers can get the whole picture. – Lei Yang Jan 26 '22 at 05:45
  • The EventBrite API isn't really what matters in this case - essentially, I have 2 objects, with 1 shared Property that I want to use in a method using generics. I'll edit the question to to reflect this more explicityl. – Chris Hobbs Jan 26 '22 at 05:50
  • if not matter, please remove those from the question. – Lei Yang Jan 26 '22 at 05:50
  • similiar question: https://stackoverflow.com/questions/50781044/c-sharp-how-to-deserialize-the-json-to-a-generic-entity-use-base-class – Lei Yang Jan 26 '22 at 06:17

2 Answers2

1

I was working on this exact issue with Eventbrite API responses last week. I used the answer from @john-glen to get going.

public class Pagination
{
    public int ObjectCount { get; set; }
    public string? Continuation { get; set; }
    public int PageCount { get; set; }
    public int PageSize { get; set; }
    public bool HasMoreItems { get; set; }
    public int PageNumber { get; set; }
}

public interface IPaginatedResponse
{
    abstract Pagination Pagination { get; set; }
}

These classes above handle the shared behaviour. Then for each specific API entity you need two classes:

public class Order {
    public long Id { get; set; }
    public Costs Costs { get; set; }
    public DateTime Created { get; set; }
    public DateTime Changed { get; set; }
    public string DiscountType { get; set; }
    public Event Event { get; set; }
    public IEnumerable<Attendee> Attendees { get; set; }
}

public class OrderApiResponse : IPaginatedResponse  {
    public Pagination Pagination { get; set; }
    public virtual List<Order> Orders { get; set; }
}

Then you use a function like this to handle the selector for the Orders property.

    public async Task<List<TResultType>> GetPaginatedResultAsync<TResultType, TApiResponseType>
    (string path, Func<TApiResponseType, List<TResultType>> selector, Dictionary<string,string>? queryParams = null)
        where TResultType : class
        where TApiResponseType : IPaginatedResponse
    {
        var firstResult = await GetFromAPI<TApiResponseType>(path, queryParams);
        var pages = firstResult.Pagination.PageCount;
        var results = selector(firstResult);
        if (pages > 1)
        {
            for (int i = 1; i < pages; i++)
            {
                if (queryParams == null) {
                    queryParams = new Dictionary<string, string>();
                }

                queryParams["page"] = $"{i + 1 }";

                var paginatedResult = await GetFromAPI<TApiResponseType>(path, queryParams);
                results.AddRange(selector(paginatedResult));
            }
        }
        return results;
    }

GetFromAPI is just a function that does an HTTP get and de-serializes json to the passed Type.

Here's the method being called and doing it's magic:

var orders = await _apiClient.GetPaginatedResultAsync<Order, OrderApiResponse>($"organizations/{orgId}/orders/", x => x.Orders, options);
AlasdairC
  • 190
  • 1
  • 1
  • 14
  • 1
    Funnily enough, I thought you somehow stole my code, because this looks so darn similar to what I ended up implementing. The only difference that I had was that I'm creating a list of Tasks, in order to run them all in parallel. It drastically helped my return times, considering we have a few thousand attendees we're ingesting. – Chris Hobbs Sep 04 '22 at 02:43
0

You can use a property selector Func like so to pass in the property that you want to use.

public static PaginatedResponse<ResultType> ResponseFactory<ResponseType, ResultType>(ResponseType response, Func<ResponseType, List<ResultType>> selector)
{
    var pagResp = new PaginatedResponse<ResultType>();
    // use the selector to map the list from the response to objects
    pagResp.Objects = selector(response);
    return pagResp;
}

You would call the above like so:

var response = ResponseFactory<EventBriteEvent, Event>(apiReponse, x => x.Events);

You will have to modify this example to incorporate into your GetPaginatedResult function, but this should get you moving in the right direction. I opted to keep the example separate to help the concept stand out.

John Glenn
  • 1,469
  • 8
  • 13
  • To be clear, I think implementing the Func on your GetPaginatedResult function makes the most sense. I'm not suggesting that you use the factory function in my example. It is just to illustrate the concept. – John Glenn Jan 26 '22 at 06:24