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 an aggregate object).
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.