1

Background

I'm consuming a third party WebApi using ServiceStack. A number of their endpoints paginate the results according to a common schema.

Example JSON:

{
    "count": 23,
    "pageSize": 10,
    "pageNumber": 1,
    "_embedded": {
        "people": [
            {
                "id": 1,
                "name": "Jean-Luc Picard"
            },
            {
                "id": 2,
                "name": "William T. Riker"
            },
            [...]
        ]
    }
}

Implementation

Since each paginated request and response DTO will have common properties, I've created abstract classes to keep these DTOs DRY.

public abstract class PaginatedRequest<TResponseDto, TEmbeddedResponseDto> : IReturn<TResponseDto>
    where TResponseDto : PaginatedResponse<TEmbeddedResponseDto>
{
    public int PageSize { get; set; }
    public int PageNumber { get; set; }

    public List<TEmbeddedResponseDto> Paginate()
    {
        var list = new List<TEmbeddedResponseDto>();

        // Page through results, and add to list until all have been received.

        return list;
    }
}

public abstract class PaginatedResponse<T>
{
    public int Count { get; set; }
    public int PageSize { get; set; }
    public int PageNumber { get; set; }

    public Dictionary<string, List<T>> _embedded { get; set; }
}

These abstract classes are then inherited by the request and response DTOs, respectively.

public class PersonDto
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class PeopleDto : PaginatedResponse<PersonDto> { }

[Route("/people/{id}")]
public class GetPerson : IReturn<PersonDto>
{
    public int ID { get; set; }
}

[Route("/people")]
public class GetPeople : PaginatedRequest<PeopleDto, PersonDto> { }

This can then be called by creating the request DTO, and calling the Paginate function.

public List<PersonDto> GetPeople() => new GetPeople().Paginate();

Question

Is there a better way to implement this functionality, while remaining DRY? Is there any specific downside to this implementation that I've not considered? I know that Inheritance in DTOs is Considered Harmful, but I think abstract base classes avoid those particular issues.

Update #1

After submitting this, I think moving the Pagination out of the abstract class may alleviate anticipated concerns that the DTOs should be implementation free.

This could be implemented either as a method in my class library, or as an extension method on the JsonServiceClient (not shown).

public List<TEmbeddedResponseDto> GetPaginated<TResponseDto, TEmbeddedResponseDto>(PaginatedRequest<TResponseDto, TEmbeddedResponseDto> request)
    where TResponseDto : PaginatedResponse<TEmbeddedResponseDto>
{
    var list = new List<TEmbeddedResponseDto>();

    // Page through results, and add to list until all have been received.

    return list;
}

public List<PersonDto> GetPeople() => GetPaginated(new GetPeople());
lordcheeto
  • 1,092
  • 12
  • 16

1 Answers1

2

Have you looked at AutoQuery, the entire Request DTO to create a AutoQuery Service for people is:

[Route("/people")]
public class QueryPeople : QueryDb<Person> {}

For which AutoQuery will automatically provide the Service implementation to create a Service that lets you query and page through Person Table.

Which you can paginate with:

/people?take=100          # first 100 results
/people?skip=100&take=100 # next 100 results

See Include Total to see how to always include the total or include it on adhoc requests with ?Include=Total.

You'll also be able to use the AutoQuery UI to get an instant UI for your AutoQuery Services which lets you page through results and shows you the AutoQuery url used.

Live AutoQuery Viewer Examples

If you want to do it manually, consider using the same QueryResponse DTO all AutoQuery Services use. Then to implement this with OrmLite, would look something like:

public object Any(SearchPeople request)
{
    var q = Db.From<Person>();
    if (request.Age != null) //Query Example
        q.Where(x => x.Age == request.Age.Value);
    if (request.Skip != null)
        q.Skip(request.Skip.Value);
    if (request.Take != null)
        q.Take(request.Take.Value);

    return new QueryResponse<Person>
    {
        Offset = request.Skip ?? 0,
        Results = db.Select<Person>(q),
        Total = db.Count(Q),
    }
}

Please also read designing message-based APIs which recommends using a consistent nomenclature and reserving Get* Services for combintator queries like fetching by primary key(s) and use Search* or Find* for Services which filter results with additional arguments;

mythz
  • 141,670
  • 29
  • 246
  • 390
  • I hadn't looked at AutoQuery. It looks great, if I was designing the service, but this is a pre-existing third party service. I still plan on looking at RestSharp to see if it fits my use case better, but I'm still analyzing ServiceStack. Implementing a few endpoints, then I'll compare ease of use, ease of maintainability, and performance. I do like the note on nomenclature, and will implement that into my design. – lordcheeto Feb 28 '18 at 19:51