Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Applies To:# OData Web API 8 supported OData Web API v8
In server-driven paging, the server returns the first page of results. If total number of results is greater than the page size, the server returns the first page along with a "next link" that can be used to fetch the next page of results. Each subsequent page should include a next link except for the last page. Using this approach, the client needs to fetch pages sequentially. It cannot jump to an arbitrary page. The next link is included in the response using the @odata.nextLink
annotation.
Here's an example of response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Products",
"value": [
{
"Id": 1,
"Name": "Product 1"
},
{
"Id": 2,
"Name": "Product 2"
},
],
"@odata.nextLink": "http://localhost:5000/odata/Products?$skiptoken=foobar"
}
In this example, the client would make a request to GET http://localhost:5000/odata/Products?$skiptoken=foobar
to retrieve the next page.
The format of the next link is opaque to the client. This means that the client should not try to decode the link, modify it or make any assumptions about how it is generated or what its components mean. It can only assume that it's a valid URL that will return the next page. This allows the server freedom in how it chooses to generate and interpret the next link. It also allows the server to change the underlying implementation of the next link without affecting the client.
Configuring paging using PageSize
ASP.NET Core OData allow you to configure the page size on the server side using the PageSize
property of the [EnableQuery]
attribute. This will automatically limit the number of items in the response and include an @odata.nextLink
property.
The following code snippet demonstrates how to configure the PageSize
on a controller action:
public class ProductsController : ODataController
{
[EnableQuery(PageSize = 2)]
public IQueryable<Product> Get()
{
return productsCollection;
}
}
This sets the number of items per page to 2. Let's assume the entity set has a total of 5 products. To fetch the first page, simply fetch the entity set:
GET http://localhost:5000/Products
Response:
{
"@odata.context": "http://localhost:5000/$metadata#Products",
"value": [
{
"Id": 1,
"Name": "Product 1"
},
{
"Id": 2,
"Name": "Product 2"
}
],
"@odata.nextLink": "http://localhost:5000/Products?$skip=2"
}
The next link will fetch the second page:
GET http://localhost:5000/Products?$skip=2
{
"@odata.context": "http://localhost:5000/$metadata#Products",
"value": [
{
"Id": 3,
"Name": "Product 3"
},
{
"Id": 4,
"Name": "Product 4"
}
],
"@odata.nextLink": "http://localhost:5000/Products?$skip=4"
}
And finally, the last page:
GET http://localhost:5000/Products?$skip=4
{
"@odata.context": "http://localhost:5000/$metadata#Products",
"value": [
{
"Id": 3,
"Name": "Product 3"
}
]
}
The last page does not include a next link. This is how the client knows that it has reached the end of the collection.
If the request contains query options, they will also be included in the generated next link. Let's take the following request for example:
GET http://localhost:5000/Products?$filter=Price gt 1000&$select=Name
The generated next link will look like:
http://localhost:5000/Products?$filter=Price%20gt%201000&$select=Name&$skip=2
Combining PageSize
and $top
PageSize
automatically limits the number of items in the response. The client can still apply $top
to the request. In this case, $top
will limit the total number of results rather than the number of items per page.
If the client-requested $top
is greater than PageSize
, then the service should return the first page of results, with a next link that fetches the next page of results up to the maximum specified in the client's $top
. If the $top
is less that then page size, then server will return the number of items specified in the $top
query option with no next link.
To illustrate this, let's use our entity set of 5 products as an example, with PageSize
set to 2. The client makes a request with $top=3
GET http://localhost:5000/Products?$top=3
Response:
{
"@odata.context": "http://localhost:5000/$metadata#Products",
"value": [
{
"Id": 1,
"Name": "Product 1"
},
{
"Id": 2,
"Name": "Product 2"
}
],
"@odata.nextLink": "http://localhost:5000/Products?$top=1&$skip=2"
}
The response contains 2 products and a next link to the next page. The client fetches the next page using the next link:
GET http://localhost:5000/Products?$top=1&$skip=2
Response:
{
"@odata.context": "http://localhost:5000/$metadata#Products",
"value": [
{
"Id": 3,
"Name": "Product 3"
}
]
}
This is now the last page. The server returns only one item and no next link.
Improving paging with $skiptoken
You may have noticed that ASP.NET Core OData generates next links using $skip
. Using $skip
on large collections may lead to performance degradation in some databases. For more information about the drawbaks of $skip
, visit the client-driven paging article.
OData provides a $skiptoken
query option that can be used in server-driven paging to encode information about the next page of a result. The content of $skiptoken
is opaque and service-specific. The client should not try to interpret the value or rely on its format. OData clients must not use $skiptoken
when constructing requests.
ASP.NET Core OData 8 provides support for paging based on $skiptoken
, but it's not enabled by default. You can enable it using the ODataOptions.SkipToken()
method when configuring OData services in your application:
services.AddControllers().AddOData(options =>
options.SetMaxTop(null).SkipToken());
Assuming we have the same products entity set as in the previous section and a PageSize
of 2, here's what the first page would look like:
GET http://localhost:5000/Products
Response
{
"@odata.context": "http://localhost:5000/$metadata#Products",
"value": [
{
"Id": 1,
"Name": "Product 1"
},
{
"Id": 2,
"Name": "Product 2"
}
],
"@odata.nextLink": "http://localhost:5000/Products?$skiptoken=Id-2"
}
The next link is now generated based on $skiptoken
rather than $skip
. This is what the second page would look like:
GET http://localhost:5000/Products?$skiptoken=Id-2
{
"@odata.context": "http://localhost:5000/$metadata#Products",
"value": [
{
"Id": 3,
"Name": "Product 3"
},
{
"Id": 4,
"Name": "Product 4"
}
],
"@odata.nextLink": "http://localhost:5000/Products?$skiptoken=Id-4"
}
The $skiptoken
value is generated using an implementation of SkipTokenHandler
. By default, the built-in DefaultSkipTokenHandler
class generates the $skiptoken
based on the values of the key fields and fields in the $orderby
query option if present. It encodes the key of the last value in the response in the $skiptoken
's value. When fetching the next page, it will use this value to determine where it left off and generate a query that's conceptually similar to:
SELECT * FROM products
WHERE id > 4
LIMIT 2;
This query is more efficient and scalable than using OFFSET
(assuming the id
field is properly indexed).
You can customize the $skiptoken
by providing your own implementation of SkipTokenHandler
and injecting it into the OData service dependency injection (DI) container:
services.AddControllers().AddOData(options =>
options.SetMaxTop(null).SkipToken()
.AddRouteComponents("", edmModel, routeServices =>
{
routeServices.AddSingleton<SkipTokenHandler, CustomSkipTokenHandler>();
}));
To learn more about customizing the SkipTokenHandler
, visit this tutorial.