Saturday, May 9, 2015

Paging in WEBAPI

Lets talk about paging using webapi.There are multiple ways we can achieve pagination. We will talk about only 2 here -
1) OData :-
OData is liberary provided by Microsoft.
OData has its own query parameters and its data structure defined which can be used to achieve paging. Lot of details are given here –
http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options

Lets try to understand it using an example. Lets say i have a controller named ODataSample where i am implementing paging for a Get method.
public PageResult Get(ODataQueryOptions queryOptions)
{
    List returnValue = new List() { 
"1","2","3","4","5","6","7","8"
    };
    ODataQuerySettings settings = new ODataQuerySettings() { PageSize = 2 };
    IQueryable results = queryOptions.ApplyTo(returnValue.AsQueryable(),settings);
    return new PageResult(results as IEnumerable, Request.GetNextPageLink(), Request.GetInlineCount());
}

The above example implements simple paging using power of Odata. Lets start analyzing it one by one -
1) Query parameter - ODataQueryOptions - 
public PageResult Get(ODataQueryOptions queryOptions)
this the the query options where all the odata specific query parameters will be parsed. This class has method named as ApplyTo which will be used to filter the result based on query params passed.
2) settings - 
ODataQuerySettings settings = new ODataQuerySettings() { PageSize = 1 };
ODataQuerySetting is by which we can set the default page size. The same thing can be achieved by Attributes also.
3) ApplyTo - 
IQueryable results = queryOptions.ApplyTo(returnValue.AsQueryable(),settings);
apply the filter based on query parameter passed. By this method Odata is saving you from writing custom logic for the same.
4) Return type - PageResult
return new PageResult(results as IEnumerable, Request.GetNextPageLink(), Request.GetInlineCount());
PageResult is data structure provided by Odata library. 
GetNextPageLink generate the next page link whenever required. 
GetInlineCount will give you the number of items available.

lets try to analyze few sample URLs and their responses -
get 1 item, and count of all - 
http://localhost:52252/api/ODataSample?$top=1&$inlinecount=allpages
{"Items":["1"],"NextPageLink":null,"Count":8}

get 2 items and all count
http://localhost:52252/api/ODataSample?$top=2&$inlinecount=allpages
{"Items":["1","2"],"NextPageLink":null,"Count":8}

get 3 items and count - 
http://localhost:52252/api/ODataSample?$top=3&$inlinecount=allpages
{"Items":["1","2"],"NextPageLink":"http://localhost:52252/api/ODataSample?$top=1&$inlinecount=allpages&$skip=2","Count":8}
(only first page is returned and next page link is been provided)

get 1 item after skiping 2 - 
http://localhost:52252/api/ODataSample?$top=1&$inlinecount=allpages&$skip=2
{"Items":["3"],"NextPageLink":null,"Count":8}

get 5 items and count - 
http://localhost:52252/api/ODataSample?$top=5&$inlinecount=allpages
{"Items":["1","2"],"NextPageLink":"http://localhost:52252/api/ODataSample?$top=3&$inlinecount=allpages&$skip=2","Count":8}

If you observe we asked for 5 items and in the response we got 2 and next item link saying ask for $top=3 and skip 2 items. 
Once we hit it again we get – 
{"Items":["3","4"],"NextPageLink":"http://localhost:52252/api/ODataSample?$top=1&$inlinecount=allpages&$skip=4","Count":8}

Another 2 items and link for the last one – 
http://localhost:52252/api/ODataSample?$top=1&$inlinecount=allpages&$skip=4
which eventually returns - {"Items":["5"],"NextPageLink":null,"Count":8}

Whenever the request is not able to accommodate all data in the single response, w get the link of next page in it.

2) Custom :- In case of custom paging, we can follow the same design like OData followed. Based on some query parameter we can identify which page we need to send back. The easiest way to implement this is using LINQ.
Lets try to understand this approch using an example  -

public object Get(int page = 0)
{
    return GetAccounts(page,Request);          
}
const int PAGE_SIZE = 2;
private static object GetAccounts(int page, HttpRequestMessage request)
{
    var output = new string[] { "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8" };
    var result = output.Skip(PAGE_SIZE * page)
.Take(PAGE_SIZE);
    return new {
Results = result
    };
}

lets say we implement the Get method which take which page to return.
Using LINQ skip and Take method we can filter the result and return it back.

The basic requirement of pagination is done. Now we can make it more intutive for user to use it. For example, with above implementation no client will get to know how many pages are there, and also there is no way i can identify the URL of next/previous page.

Getting the total count -
var totalCount = output.Count();
    var totalPages = Math.Ceiling((double)totalCount / PAGE_SIZE);

Creating the link for next and previous page -
var helper = new UrlHelper(request);
    var prevUrl = page>0 ? helper.Link("DefaultApi", new { page = page - 1 }) : "";
    var nextUrl = page<totalPages-1 ?  helper.Link("DefaultApi", new { page = page + 1 }) : "";

helper take the route name and route value. Routename in this case is DefaultApi only and for previous page we need to pass page number 1 less than current, and for next page it will be 1 more than current.

Hence the final code for the same becomes -
private static object GetAccounts(int page, HttpRequestMessage request)
    var output = new string[] { "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8" };
    var totalCount = output.Count();
    var totalPages = Math.Ceiling((double)totalCount / PAGE_SIZE);
    var helper = new UrlHelper(request);
    var prevUrl = page>0 ? helper.Link("DefaultApi", new { page = page - 1 }) : "";
    var nextUrl = page<totalPages-1 ?  helper.Link("DefaultApi", new { page = page + 1 }) : "";
    var result = output.Skip(PAGE_SIZE * page)
.Take(PAGE_SIZE);
    return new {
Pagination = new  {
   TotalCount = totalCount,
   TotalPages = totalPages,
   PrevPageUrl = prevUrl,
   NextPageUrl = nextUrl,
},              
Results = result
    };
}

Its good idea to pass all the pagination information in separate object, rather tahn combining it with the actual result.


No comments:

Post a Comment