Tuesday, April 21, 2015

Paged List for WebAPI

One of my favorite quotes is "there is nothing as embarrassing as yesterday's code." I blogged about a paged list class a while back, but I no longer like that implementation...so here is a new one that includes WebAPI serialization support!

...but why is this useful?

You can use the simple IPagedList interface to pass paged data around all of your application, and then any object returned from your WebAPI that implements IPagedList will be automatically serialized for you. This allows you to create very consistent APIs that support paging.

IPagedList Interfaces

public interface IPagedList
{
    int PageIndex { get; }
 
    int PageSize { get; }
 
    int TotalCount { get; }
 
    IList List { get; }
}
 
public interface IPagedList<T> : IPagedList
{
    new IList<T> List { get; }
}

Sample Controller

[RoutePrefix("Paged")]
[PagedListActionFilter] // TODO Register this globally!
public class PagedController : ApiController
{
    [Route]
    [HttpGet]
    public IHttpActionResult Get()
    {
        // Create a list of numbers.
        var all = Enumerable.Range(1, 10).ToList();
 
        // Get page information for query string.
        var index = Request.GetPageIndex();
        var size = Request.GetPageSize();
 
        // Take the selected page.
        var page = all.TakePage(index, size);
 
        // Send the IPagedListe back, and the ActionFilter
        // will serialize it and add paging headers for you!
        return Ok(page);
    }
}

PagedListActionFilterAttribute

public class PagedListActionFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(
        HttpActionExecutedContext actionExecutedContext)
    {
        base.OnActionExecuted(actionExecutedContext);
 
        var objectContent = actionExecutedContext.Response.Content
            as ObjectContent;
        if (objectContent == null)
        {
            return;
        }
       
        var pagedList = objectContent.Value as IPagedList;
        if (pagedList == null)
        {
            return;
        }
 
        actionExecutedContext.Response.Headers.Add(
            "X-Page-Index", 
            pagedList.PageIndex.ToString(CultureInfo.InvariantCulture));
        actionExecutedContext.Response.Headers.Add(
            "X-Page-Size", 
            pagedList.PageSize.ToString(CultureInfo.InvariantCulture));
        actionExecutedContext.Response.Headers.Add(
            "X-Total-Count", 
            pagedList.TotalCount.ToString(CultureInfo.InvariantCulture));
 
        var listType = pagedList.List.GetType();
        actionExecutedContext.Response.Content = new ObjectContent(
            listType, 
            pagedList.List, 
            objectContent.Formatter);
    }
}

PagedList Implementation

public class PagedList<T> : IPagedList<T>
{
    public PagedList(
        IList<T> list, 
        int pageIndex, 
        int pageSize, 
        int totalCount)
    {
        List = list;
        PageIndex = pageIndex;
        PageSize = pageSize;
        TotalCount = totalCount;
    }
 
    public int PageIndex { get; private set; }
 
    public int PageSize { get; private set; }
 
    public int TotalCount { get; private set; }
 
    public IList<T> List { get; private set; }
 
    IList IPagedList.List
    {
        get { return (IList)List; }
    }
}

Extension Methods

public static class ListExtensions
{
    public static IPagedList<T> ToPagedList<T>(
        this IList<T> list, 
        int pageIndex, 
        int pageSize, 
        int totalCount)
    {
        return new PagedList<T>(list, pageIndex, pageSize, totalCount);
    }
 
    public static IPagedList<T> TakePage<T>(
        this IList<T> items, 
        int pageIndex, 
        int pageSize)
    {
        var collection = items
            .Skip(pageIndex * pageSize)
            .Take(pageSize)
            .ToArray();
        return new PagedList<T>(collection, pageIndex, pageSize, items.Count);
    }
}
 
public static class RequestExtensions
{
    public static int GetPageSize(
        this HttpRequestMessage requestMessage, 
        int defaultSize = 5)
    {
        return GetIntFromQueryString(requestMessage, "pageSize", defaultSize);
    }
 
    public static int GetPageIndex(
        this HttpRequestMessage requestMessage, 
        int defaultIndex = 0)
    {
        return GetIntFromQueryString(requestMessage,"pageIndex",defaultIndex);
    }
 
    public static int GetIntFromQueryString(
        this HttpRequestMessage requestMessage, 
        string key, 
        int defaultValue)
    {
        var pair = requestMessage
            .GetQueryNameValuePairs()
            .FirstOrDefault(p => p.Key.Equals(key, 
                StringComparison.InvariantCultureIgnoreCase));
 
        if (!string.IsNullOrWhiteSpace(pair.Value))
        {
            int value;
            if (int.TryParse(pair.Value, out value))
            {
                return value;
            }
        }
 
        return defaultValue;
    }
}

Enjoy,
Tom

6 comments:

  1. Thanks you gave me a great idea.
    I Copied PagedListActionFilterAttribute and added it to my WebApiConfig so i don´t have to insert the attribute in each controller:
    config.Filters.Add(new PagedListActionFilterAttribute());

    Others things I didn´t use them but I´ll use them when needed.
    Thanks for sharing!


    ReplyDelete
  2. Hi, I´ll give you the code for people interested. PagedListActionFilterAttribute is executed after the api method call. If return value is IPagedList => Add TotalCountHeader. Otherwise continue with normal execution.

    public class PagedListActionFilterAttribute : ActionFilterAttribute
    {
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
    base.OnActionExecuted(actionExecutedContext);

    if (actionExecutedContext.Response == null)
    return;

    var objectContent = actionExecutedContext.Response.Content as ObjectContent;
    if (objectContent == null)
    {
    return;
    }
    // Check return value if is IPagedList o IList if you return that type
    var pagedList = objectContent.Value as IPagedList;
    if (pagedList == null)
    {
    return;
    }
    // add TotalCount header
    actionExecutedContext.Response.Headers.Add(
    "TotalCount", pagedList.TotalItemCount.ToString(CultureInfo.InvariantCulture));

    var listType = pagedList.GetType();
    actionExecutedContext.Response.Content = new ObjectContent(
    listType,
    pagedList,
    objectContent.Formatter);
    }
    }

    public static class WebApiConfig
    {
    public static void Register(HttpConfiguration config)
    {
    // Web API configuration and services
    // Configure Web API to use only bearer token authentication.
    ...
    config.Filters.Add(new PagedListActionFilterAttribute());
    ...
    }
    }


    An example of api method is
    ..
    [HttpPost]
    [Route("Filter")]
    public async Task> GetByFilter(ClienteFilter filter)
    {
    IPagedList retVal = await filter.GetPagedListAsync(_clienteDao.GetAll());
    return retVal;
    }

    ReplyDelete
  3. Is there a special request header for this? I got xml response that i couldn't serialize to the same class

    ReplyDelete
  4. And the response was from my browser get request

    ReplyDelete
  5. "There is nothing as embarassing as yesterday's code". A very nice quote indeed

    ReplyDelete

Real Time Web Analytics