Skip to content

How To Configure ASP.NET Web API

NickBehrends edited this page Aug 27, 2015 · 2 revisions

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.

ASP.NET Web API Example

Here is an example of how to talk with angular-paginate-anything using the ASP.NET Web API 2.x framework.

Processing the HTTP Request

In order to make our code reusable with different controllers and actions, we'll leverage a simple data-transfer object class to hold the incoming paging data from the HTTP request:

public class PagingModel
{
	public PagingModel()
	{
		From = 0;
		To = 9;
	}

	public int From { get; set; }
	public int To { get; set; }
	public int TotalRecordCount { get; set; }

	public int Page { get { return (To / Size) + 1; } }
	public int Size { get { return To - From + 1; } }
}

The class below creates an action filter attribute to parse out the paging data from the request header:

public class WithPagingAttribute : ActionFilterAttribute
{
	private readonly string _paramName;
	private static readonly Regex RangeHeaderRegex = new Regex(@"(?<from>\d+)-(?<to>\d*)", RegexOptions.Compiled);

	public WithPagingAttribute(string paramName = "pagingModel")
	{
		_paramName = paramName;
	}

	public override void OnActionExecuting(HttpActionContext actionContext)
	{
		object data;
		if (actionContext.ActionArguments.TryGetValue(_paramName, out data))
		{
			//manipulate/inject paging info from headers
			var pagingModel = data as PagingModel;
			pagingModel = IncomingPagingModel(actionContext.Request.Headers, pagingModel);
			actionContext.ActionArguments[_paramName] = pagingModel;
		}
	}

	private static PagingModel IncomingPagingModel(HttpRequestHeaders headers, PagingModel pagingModel)
	{
		if (pagingModel == null)
			pagingModel = new PagingModel();

		IEnumerable<string> rangeUnitValues;
		IEnumerable<string> rangeValues;

		if (headers.TryGetValues("Range-Unit", out rangeUnitValues)
			&& rangeUnitValues.First().Equals("items", StringComparison.OrdinalIgnoreCase)
			&& headers.TryGetValues("Range", out rangeValues))
		{
			var rangeHeaderMatch = RangeHeaderRegex.Match(rangeValues.First());
			if (!string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["from"].Value))
				pagingModel.From = int.Parse(rangeHeaderMatch.Groups["from"].Value);

			if (!string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["to"].Value))
				pagingModel.To = int.Parse(rangeHeaderMatch.Groups["to"].Value);
		}

		return pagingModel;
	}
}

You can then decorate controller actions with the attribute in order to read the request:

[HttpGet]
[WithPaging]
public IHttpActionResult GetSomeData([FromUri] PagingModel pagingModel)
{
	//call code to query data and pass
	var data = GetData(pagingModel);
	...
}

Creating the HTTP Response

In order to provide the response with the correct Content-Range and Range-Unit headers, you can create a custom action result that extends the Web API's OkNegotiatedContentResult<T> and the ContentRangeHeaderValue type to build the Content-Range header with built-in validation based on the RFC2616 Range Headers specification, without having to manipulate strings ourselves:

public class PartialNegotiatedContentResult<T> : OkNegotiatedContentResult<T>
{
	readonly int _requestedFrom;
	readonly int _requestedTo;
	readonly int _totalCount;

	public PartialNegotiatedContentResult(T content, ApiController controller, int requestedFrom, int requestedTo, int totalCount)
		: base(content, controller)
	{
		_requestedFrom = requestedFrom;
		_requestedTo = requestedTo;
		_totalCount = totalCount;

		if (_requestedTo > _totalCount && _totalCount > 0)
			_requestedTo = _totalCount;
	}

	public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
	{
		var response = await base.ExecuteAsync(cancellationToken);
		const string unit = "items";

		response.Headers.Add("Accept-Ranges", unit);
		response.Headers.Add("Range-Unit", unit);

		if (_totalCount > 0)
		{
			response.StatusCode = HttpStatusCode.PartialContent;
			response.Content.Headers.ContentRange = new ContentRangeHeaderValue(_requestedFrom, _requestedTo, _totalCount)
			{
				Unit = unit
			};
		}
		else
		{
			response.StatusCode = HttpStatusCode.NoContent;
			response.Content.Headers.ContentRange = new ContentRangeHeaderValue(0);
		}

		return response;
	}
}

To simplify using this custom action result, we create an extension method for the Web API's ApiController:

public static class ApiControllerExtensions
{
	public static IHttpActionResult PagedPartialContent<T>(
		this ApiController controller, 
		IEnumerable<T> content, int from, int to, int count) where T : class 
	{
		return new PartialNegotiatedContentResult<IEnumerable<T>>(content, controller, from, to, count);
	}
}

And finally, to tie it all together, you can use the extension method above in the previous controller action to return the HTTP response:

[HttpGet]
[WithPaging]
public IHttpActionResult GetSomeData([FromUri] PagingModel pagingModel)
{
	var data = GetData(pagingModel);	
	return this.PagedPartialContent(data, pagingModel.From, pagingModel.To, data.totalCount);
}