Skip to content

How to configure ServiceStack for .NET

tafs7 edited this page Mar 30, 2015 · 1 revision

This directive uses HTTP headers to ask your server to paginate its response. Your server needs to read these headers and also return headers of its own to tell the directive which range is returned and the total number of elements.

ServiceStack Example

Here is an example of how to talk with angular-paginate-anything using the Service Stack 3.x framework.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using ServiceStack.Common.Extensions;
using ServiceStack.Common.Web;
using ServiceStack.ServiceInterface;

namespace MyNameSpace
{
    public static class PaginationExtensions
    {
        public delegate IEnumerable GetResultsFunc(int skip, int limit);
        private static Regex rangeHeaderRegex = new Regex(@"(?<from>\d+)-(?<to>\d*)", RegexOptions.Compiled);

        public static HttpResult Paginate(this Service service, Func<int> totalCountFunc, GetResultsFunc getResultsFunc)
        {
            return service.Paginate(totalCountFunc, getResultsFunc, 1000);
        }

        public static HttpResult Paginate(this Service service, Func<int> totalCountFunc, GetResultsFunc getResultsFunc,
            int maxResults)
        {
            var httpResult = new HttpResult
            {
                StatusCode = HttpStatusCode.OK,
                Headers =
                {
                    {"Range-Unit", "items"},
                    {HttpHeaders.AcceptRanges, "items"}
                }
            };

            var totalItems = totalCountFunc();

            var requestedFrom = 0;
            var requestedTo = double.PositiveInfinity;

            var rangeUnitHeader = service.RequestContext.GetHeader("Range-Unit") ?? string.Empty;
            var rangeHeader = service.RequestContext.GetHeader(HttpHeaders.Range) ?? string.Empty;

            if (StringComparer.Ordinal.Compare(rangeUnitHeader, "items") == 0)
            {
                var rangeHeaderMatch = rangeHeaderRegex.Match(rangeHeader);
                requestedFrom = !string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["from"].Value)
                    ? int.Parse(rangeHeaderMatch.Groups["from"].Value)
                    : 0;

                requestedTo = !string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["to"].Value)
                    ? int.Parse(rangeHeaderMatch.Groups["to"].Value)
                    : double.PositiveInfinity;
            }

            if (requestedFrom < 0 || requestedTo < 0 || //reject negative ranges
                requestedFrom > requestedTo ||
                (requestedFrom > 0 && requestedFrom >= totalItems))
            {
                httpResult.StatusCode = HttpStatusCode.RequestedRangeNotSatisfiable;
                httpResult.Headers["Content-Range"] = string.Format("items */{0}", totalItems);

                return httpResult;
            }

            var availableSkip = new[] {requestedTo, totalItems - 1, requestedFrom + maxResults - 1}.Min();
            var availableLimit = availableSkip - requestedFrom + 1;

            if (Math.Abs(availableLimit) < 1)
            {
                httpResult.Headers["Content-Range"] = "items */0";
                httpResult.StatusCode = HttpStatusCode.NoContent;
                return httpResult;
            }

            httpResult.Headers["Content-Range"] = string.Format("items {0}-{1}/{2}", requestedFrom, availableSkip,
                totalItems < int.MaxValue ? totalItems.ToString(NumberFormatInfo.InvariantInfo) : "*");

            if (availableLimit < totalItems)
                httpResult.StatusCode = HttpStatusCode.PartialContent;

            // get results
            httpResult.Response = getResultsFunc(requestedFrom, Convert.ToInt32(availableLimit));

            // generate content relation links / LINK header
            var links = new List<string>();
            var requestedLimit = requestedTo - requestedFrom + 1;

            if (availableSkip < totalItems - 1)
            {
                links.Add(string.Format("<{0}>; rel=\"next\"; items=\"{1}-{2}\"",
                    service.RequestContext.AbsoluteUri, availableSkip + 1,
                    SuppressInfinity(availableSkip + requestedLimit)));

                if (totalItems < int.MaxValue)
                {
                    links.Add(string.Format("<{0}>; rel=\"last\"; items=\"{1}-{2}\"",
                        service.RequestContext.AbsoluteUri, ((totalItems - 1)/availableLimit)*availableLimit,
                        SuppressInfinity((((totalItems - 1)/availableLimit)*availableLimit) + requestedLimit - 1)));
                }
            }

            if (requestedFrom > 0)
            {
                var previousFrom = new[] {0, requestedFrom - new[] {requestedLimit, maxResults}.Min()}.Max();

                links.Add(string.Format("<{0}>; rel=\"prev\"; items=\"{1}-{2}\"",
                    service.RequestContext.AbsoluteUri, previousFrom,
                    SuppressInfinity(previousFrom + requestedLimit - 1)));

                links.Add(string.Format("<{0}>; rel=\"first\"; items=\"0-{1}\"",
                    service.RequestContext.AbsoluteUri, SuppressInfinity(requestedLimit - 1)));
            }

            if (links.Any())
                httpResult.Headers["Link"] = links.Join(", ");

            return httpResult;
        }

        private static string SuppressInfinity(double number)
        {
            return double.IsPositiveInfinity(number)
                ? string.Empty
                : Convert.ToInt32(number).ToString(NumberFormatInfo.InvariantInfo);
        }
    }
}

You can use this extension method like this:

public class ToDoService : Service
{
    public object Get(FindToDos request)
    {
        var results = .....

        return this.Paginate(
            () => results.Count(),
            (skip, limit) => results.Skip(skip).Take(limit)
            );
    }
}

This example is inspired by Clean Pagination

Clone this wiki locally