Skip to main content
Tweeted twitter.com/StackCodeReview/status/997108518414110720
added 17 characters in body
Source Link
TomSelleck
  • 373
  • 1
  • 9

When I have all matching objects - I pass them to a "Paging" method within the service (I think I have to do this in the service rather than the repository because a generic repo paging method won't work across some resources e.g "orderBy" a property nested within a class in a an aggregate object).

When I have all matching objects - I pass them to a "Paging" method within the service (I think I have to do this in the service rather than the repository because a generic repo paging method won't work across some resources e.g "orderBy" a property nested within a class in a ).

When I have all matching objects - I pass them to a "Paging" method within the service (I think I have to do this in the service rather than the repository because a generic repo paging method won't work across some resources e.g "orderBy" a property nested within a class in an aggregate object).

Source Link
TomSelleck
  • 373
  • 1
  • 9

Setting up pagination links inside a WebAPI Controller

I'm implementing paging for the first time and have the following code which achieves what I want.

There are just a couple of things that feel a bit weird to me and would like if somebody could give it a once over and see if it's okay.

At the moment, I call a service to retrieve objects from a DB using a repository class.

When I have all matching objects - I pass them to a "Paging" method within the service (I think I have to do this in the service rather than the repository because a generic repo paging method won't work across some resources e.g "orderBy" a property nested within a class in a ).

Once paging is complete, the result is passed back to the WebAPI Controller where I have another method to setup the page linking information (current, next, previous, first and last page links.)

This method feels a bit hacky and doesn't feel like it should be in the controller - it could probably be used across other endpoints, maybe it should go into a Util class or pull it out into the pipeline.

Controller.cs

[Route("v1/endpoint")]
[HttpGet]
//[ValidateFilter] TODO Allow optional params + validate
public async Task<IHttpActionResult> GetConnectionsAsync([FromUri]List<ConnectionStatus> connectionStatus, [FromUri]int page = 1,
    [FromUri]int pageSize = 20, [FromUri]string orderBy = "name", [FromUri]bool ascending = true)
{
        var id = await AuthorizationService.GetAzureObjectIdentifierAsync();
                
        var objects = await Service.GetObjectsAsync(id, connectionStatus, page, pageSize, orderBy, ascending);

        //TODO Should this be in the controller? Or Should it be pulled out into the pipeline?
        objects.PagingMetaData.PageLinkInfo = SetupPagingLinks(Url, page, pageSize, objects.PagingMetaData.TotalNumberOfPages);

        return Content(HttpStatusCode.OK, objects);
}

//Feels hacky
private PageLinkInfo SetupPagingLinks(UrlHelper url, int page, int pageSize, int totalPages)
{
    PageLinkInfo linkInfo = new PageLinkInfo(); 

    var qs = Url.Request.RequestUri.ParseQueryString();

    //Set default params if missing
    if (qs["page"] == null)
    {
        qs.Set("page", page.ToString());
    }
    if (qs["pageSize"] == null)
    {
        qs.Set("pageSize", pageSize.ToString());
    }

    var baseUrl = Url.Request.RequestUri.GetLeftPart(UriPartial.Path);
    linkInfo.CurrentPageUrl = $"{baseUrl}?{qs}";

    //First page
    qs.Set("page", "1");
    linkInfo.FirstPageUrl = $"{baseUrl}?{qs}";

    //Last page
    qs.Set("page", $"{totalPages}");
    linkInfo.LastPageUrl = $"{baseUrl}?{qs}";

    //Next page
    if (page + 1 <= totalPages)
    {
        qs.Set("page", $"{page + 1}");
        linkInfo.NextPageUrl = $"{baseUrl}?{qs}";
    }
    else
    {
        linkInfo.NextPageUrl = null;
    }

    //Previous page
    if (page - 1 >= 1)
    {
        qs.Set("page", $"{page - 1}");
        linkInfo.PreviousPageUrl = $"{baseUrl}?{qs}";
    }
    else
    {
        linkInfo.PreviousPageUrl = null;
    }

    return linkInfo;
}

Service.cs

public async Task<PagedResults<Object>> GetObjectsAsync(string id, IEnumerable<ConnectionStatus> connectionStatus, int page, int pageSize,
    string orderBy, bool ascending)
{
    var objects = await Repository.SearchForAsync(object => object.id == id && connectionStatus.Contains(object.ConnectionStatus));
    
    var result = PageObjects(objects, page, pageSize, orderBy, ascending);

    return result;
}

private PagedResults<Object> PageObjects(IEnumerable<ConsumerConnectionGetDto> connections, int page, int pageSize, string orderBy, bool ascending)
{
    switch (orderBy) {
        case "dateConnected":
            connections = connections.OrderBy(x => x.DateConnected).Reverse();
            break;
        case "lastCommunication":
            connections = connections.OrderBy(x => x.LastCommunication).Reverse();
            break;
        case "name":
            connections = connections.OrderBy(x => x.Enterprise.Name);
            break;
        default:
            connections = connections.OrderBy(x => x.Enterprise.Name);
            break;
    }

    if (!ascending) {
        connections = connections.Reverse();
    }

    var skipAmount = pageSize * (page - 1);
    var totalNumberOfRecords = connections.Count();
    var mod = totalNumberOfRecords % pageSize;
    var totalPageCount = (totalNumberOfRecords / pageSize) + (mod == 0 ? 0 : 1);

    var results = connections
        .Skip(skipAmount)
        .Take(pageSize)
        .AsQueryable();

    return new PagedResults<Object>
    {
        Results = results,
        PagingMetaData = new PagingMetaData()
        {
            CurrentPageNumber = page,
            PageSize = pageSize,
            TotalNumberOfRecords = totalNumberOfRecords,
            TotalNumberOfPages = totalPageCount,
            PageLinkInfo = new PageLinkInfo()
            {
                CurrentPageUrl = "",
                FirstPageUrl = "",
                LastPageUrl = "",
                NextPageUrl = "",
                PreviousPageUrl = ""
            }
        }

    };
}

PagedResults.cs

public class PagedResults<T>
{
    [JsonProperty(PropertyName = "_metadata")]
    public PagingMetaData PagingMetaData { get; set; }

    [JsonProperty(PropertyName = "results")]
    public IEnumerable<T> Results { get; set; }
}

public class PagingMetaData
{
    [JsonProperty(PropertyName = "currentPageNumber")]
    public int CurrentPageNumber { get; set; }

    [JsonProperty(PropertyName = "pageSize")]
    public int PageSize { get; set; }

    [JsonProperty(PropertyName = "totalNumberOfPages")]
    public int TotalNumberOfPages { get; set; }

    [JsonProperty(PropertyName = "totalNumberOfRecords")]
    public int TotalNumberOfRecords { get; set; }

    [JsonProperty(PropertyName = "links")]
    public PageLinkInfo PageLinkInfo { get; set; }
}

public class PageLinkInfo
{
    [JsonProperty(PropertyName = "nextPageUrl")]
    public string NextPageUrl { get; set; }
    [JsonProperty(PropertyName = "previousPageUrl")]
    public string PreviousPageUrl { get; set; }
    [JsonProperty(PropertyName = "firstPageUrl")]
    public string FirstPageUrl { get; set; }
    [JsonProperty(PropertyName = "lastPageUrl")]
    public string LastPageUrl { get; set; }
    [JsonProperty(PropertyName = "currentPageUrl")]
    public string CurrentPageUrl { get; set; }
}

Any advice or comments would be appreciated.