{"id":28917314,"url":"https://github.com/mrahhal/mr.entityframeworkcore.keysetpagination","last_synced_at":"2026-04-06T18:00:43.771Z","repository":{"id":37258216,"uuid":"292047833","full_name":"mrahhal/MR.EntityFrameworkCore.KeysetPagination","owner":"mrahhal","description":"Keyset/Seek/Cursor pagination for Entity Framework Core.","archived":false,"fork":false,"pushed_at":"2025-04-06T11:55:05.000Z","size":1657,"stargazers_count":269,"open_issues_count":7,"forks_count":13,"subscribers_count":4,"default_branch":"main","last_synced_at":"2026-03-06T09:23:14.064Z","etag":null,"topics":["cursor-pagination","dotnet","keyset-pagination","pagination","roslyn-analyzer","seek-pagination"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrahhal.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["mrahhal"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":null}},"created_at":"2020-09-01T16:18:40.000Z","updated_at":"2025-12-18T07:19:38.000Z","dependencies_parsed_at":"2024-10-17T00:51:05.798Z","dependency_job_id":null,"html_url":"https://github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/mrahhal/MR.EntityFrameworkCore.KeysetPagination","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrahhal%2FMR.EntityFrameworkCore.KeysetPagination","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrahhal%2FMR.EntityFrameworkCore.KeysetPagination/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrahhal%2FMR.EntityFrameworkCore.KeysetPagination/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrahhal%2FMR.EntityFrameworkCore.KeysetPagination/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrahhal","download_url":"https://codeload.github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrahhal%2FMR.EntityFrameworkCore.KeysetPagination/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31483380,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T17:22:55.647Z","status":"ssl_error","status_checked_at":"2026-04-06T17:22:54.741Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cursor-pagination","dotnet","keyset-pagination","pagination","roslyn-analyzer","seek-pagination"],"created_at":"2025-06-22T00:12:51.874Z","updated_at":"2026-04-06T18:00:43.765Z","avatar_url":"https://github.com/mrahhal.png","language":"C#","funding_links":["https://github.com/sponsors/mrahhal"],"categories":[],"sub_categories":[],"readme":"# MR.EntityFrameworkCore.KeysetPagination\n\n\u003ca href=\"http://use-the-index-luke.com/no-offset\"\u003e\n  \u003cimg src=\"https://github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination/blob/4be043bf88f49cb3c4e9f96d371699b283031167/images/no-offset.png\" alt=\"100% offset-free\" target=\"_blank\" align=\"right\" width=\"120\" height=\"120\"\u003e\n\u003c/a\u003e\n\n[![CI](https://github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination/actions/workflows/ci.yml/badge.svg)](https://github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination/actions/workflows/ci.yml)\n[![NuGet version](https://badge.fury.io/nu/MR.EntityFrameworkCore.KeysetPagination.svg)](https://www.nuget.org/packages/MR.EntityFrameworkCore.KeysetPagination)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)\n\nKeyset pagination for EF Core (Entity Framework Core). Also known as seek pagination or cursor pagination.\n\nLearn about why the standard offset based pagination (`Take().Skip()`) is bad in many common cases [here](http://use-the-index-luke.com/no-offset).\n\nCheck the [benchmarks](#benchmarks) section below for a quick look at the different performance characteristics between offset and keyset.\n\n\u003e [!NOTE]\n\u003e If you're using ASP.NET Core, you can use [MR.AspNetCore.Pagination](https://github.com/mrahhal/MR.AspNetCore.Pagination) which wraps this package and offers an easier to consume keyset pagination behavior with additional features for ASP.NET Core. This is a lower level library that implements keyset pagination for EF Core.\n\n## Usage\n\n`KeysetPaginate` is an extension method on `IQueryable\u003cT\u003e` (same as all other queryable Linq methods), and it takes a few arguments:\n\n```cs\nKeysetPaginate(\n    // This configures the keyset columns and their order.\n    b =\u003e b.Ascending(entity =\u003e entity.Id),\n    // The direction we want to walk relative to the order above (Forward/Backward). Default is Forward.\n    direction,\n    // The reference object (used to query previous/next pages). Default is null.\n    reference\n)\n```\n\nUsing this method we can do all kinds of keyset queries: first page, previous page, next page, last page.\n\nThese queries usually follow the same patterns, shown in the \"Common patterns\" section. Practical code examples are shown in the \"Getting the data\" section.\n\nBut first, let's talk a bit more about `KeysetPaginate` and how it works.\n\nHere's a small visual representation:\n\n\u003cimg src=\"images/exp.jpg\" width=\"300\" /\u003e\n\nThe columns and their configured order are used to order the data, and then the direction decides if we're getting the data before or after the reference row.\n\n\u003e [!WARNING]\n\u003e You'll want to reverse the result whenever you use `KeysetPaginationDirection.Backward` to get the proper order of the data, since walking `Backward` gives results in the opposite order to the configured columns order. There's a helper method on `KeysetContext` for this, shown in a snippet later.\n\n`KeysetPaginate` returns a context object which you can use to get secondary info and get the data result.\n\nIt can be called without direction and reference, in which case this is equivalent to querying the first page:\n\n```cs\nKeysetPaginate(\n    b =\u003e b.Ascending(entity =\u003e entity.Id)\n)\n```\n\nConfiguring a composite keyset is easy as well. Just add all the columns you want:\n\n```cs\nKeysetPaginate(\n    b =\u003e b.Ascending(entity =\u003e entity.Id).Ascending(entity =\u003e entity.Score),\n    ...\n)\n```\n\nYou can also mix ASC/DESC columns. `KeysetPaginate` knows how to handle that:\n\n```cs\nKeysetPaginate(\n    b =\u003e b.Ascending(entity =\u003e entity.Id).Descending(entity =\u003e entity.Score),\n    ...\n)\n```\n\n\u003e [!IMPORTANT]\n\u003e Make sure to read the \"Deterministic keysets\" and \"Indexing\" sections for important notes about configuring keysets.\n\n## Common patterns\n\nHere are the 4 most common patterns of using `KeysetPaginate`.\n\n#### First page\n\nNot specifying direction and reference gives you the first page of data.\n\n```cs\nKeysetPaginate(\n    b =\u003e ...\n)\n```\n\nThis is equivalent to the following:\n\n```cs\nKeysetPaginate(\n    b =\u003e ...,\n    KeysetPaginationDirection.Forward,\n    null\n)\n```\n\n#### Last page\n\nWe get the last page by specifying a `Backward` direction.\n\n```cs\nKeysetPaginate(\n    b =\u003e ...,\n    KeysetPaginationDirection.Backward\n)\n```\n\n#### Previous page\n\nYou get previous/next pages by providing a direction and a reference. In this case, the reference should be the first item of the current page, and the direction is `Backward`:\n\n```cs\nKeysetPaginate(\n    b =\u003e ...,\n    KeysetPaginationDirection.Backward,\n    reference\n)\n```\n\n#### Next page\n\nYou get previous/next pages by providing a direction and a reference. In this case, the reference should be the last item of the current page, and the direction is `Forward`:\n\n```cs\nKeysetPaginate(\n    b =\u003e ...,\n    KeysetPaginationDirection.Forward,\n    reference\n)\n```\n\n## Prebuilt keyset query definition\n\nAlthough all the examples here build the keyset directly inside the `KeysetPaginate` call for brevity, the recommended way of doing this is to prebuild the keyset query definition. Prebuilding will allow reusing of internal caches, leading to more performance and less allocations.\n\nTo prebuild, all you need to do is move the keyset building code out of the `KeysetPaginate` call and into a long lived instance (such as a static field).\n\n```cs\n// In the ctor or someplace similar, set this to a static field for example.\n_usersKeysetQuery = KeysetQuery.Build\u003cUser\u003e(b =\u003e b.Ascending(x =\u003e x.Id));\n\n// Then when calling KeysetPaginate, we use the prebuilt definition.\ndbContext.Users.KeysetPaginate(\n    _usersQueryKeyset,\n    ...);\n```\n\n## Getting the data\n\nLet's now see how to work with the context object that `KeysetPaginate` returns.\n\nThe following is a basic example usage. We're querying the data and getting back 20 items:\n\n```cs\nvar keysetContext = dbContext.Users.KeysetPaginate(...);\n\nvar users = await keysetContext\n    .Query\n    .Take(20)\n    .ToListAsync();\n\n// As noted in several places above, don't forget to ensure the data is correctly ordered:\nkeysetContext.EnsureCorrectOrder(users);\n```\n\n`KeysetPaginate` returns a context object that includes a `Query` property. This `Query` is what you'll chain more linq operators to and then use to get your data.\n\nThe context object itself can be further reused by other helper methods in this package such as `HasPreviousAsync`/`HasNextAsync` to get more info.\n\nAs a shortcut for when you don't need this context object, there's a `KeysetPaginateQuery` method:\n\n```cs\nvar users = await dbContext.Users\n    .KeysetPaginateQuery(...)\n    .Take(20)\n    .ToListAsync();\n```\n\nUsing the context object with helper methods:\n\n```cs\n// Store it in a variable because we'll be using it in more than one way.\nvar keysetContext = dbContext.Users\n    .KeysetPaginate(...);\n\n// First, we'll get our actual data. We do this by using the `Query` property.\nvar users = await keysetContext.Query\n    .Take(20)\n    .ToListAsync();\n// Make sure you call EnsureCorrectOrder before anything else.\nkeysetContext.EnsureCorrectOrder(users);\n\n// This is true when there is more data before the returned list.\nvar hasPrevious = await keysetContext.HasPreviousAsync(users);\n\n// This is true when there is more data after the returned list.\nvar hasNext = await keysetContext.HasNextAsync(users);\n```\n\n`HasPreviousAsync`/`HasNextAsync` are useful when you want to know when to render Previous/Next (Older/Newer) buttons.\n\n\u003e [!NOTE]\n\u003e The reference/data these methods accept are loosely typed to allow flexibility when projecting your models (to DTOs for example). For more info check [this document](docs/loose-typing.md).\n\nHere's another example showing how to obtain the total count for the data to display somewhere:\n\n```cs\n// Assuming we're in an api that should return admin users.\n\n// Prepare the base query first.\nvar query = dbContext.Users.Where(x =\u003e x.IsAdmin);\n\n// This will be the count of all admins.\nvar count = await query.CountAsync();\n\n// And then we apply keyset pagination at the end.\n// `KeysetPaginate` adds ordering and more predicates to the query so we have to get the count before we apply it.\nvar keysetContext = query.KeysetPaginate(...);\nvar admins = await keysetContext.Query\n    .Take(20)\n    .ToListAsync();\n\n// You can optionally use the context object too as explained above to get additional info.\nkeysetContext.EnsureCorrectOrder(admins);\n```\n\n## Nested properties\n\nNested properties are also supported when defining a keyset. Just make sure the reference contains the same nested chain of properties.\n\n```cs\n// If you're using a loaded entity for the reference.\nvar reference = await dbContext.Users\n    // Load it, otherwise you won't get the correct result.\n    .Include(x =\u003e x.Nested)\n    .FirstOrDefaultAsync(x =\u003e x.Id == id);\n\n// If you're using another type for the reference.\nvar reference = new\n{\n    Nested = new\n    {\n        Created = ...,\n    },\n};\n\nvar keysetContext = dbContext.Users.KeysetPaginate(\n    // Defining the keyset using a nested property.\n    b =\u003e b.Ascending(entity =\u003e entity.Nested.Created),\n    direction,\n    reference);\nvar result = await keysetContext.Query\n    // You'll want to load it here too if you plan on calling any context methods.\n    .Include(x =\u003e x.Nested)\n    .Take(20)\n    .ToListAsync();\n```\n\n## Deterministic keysets\n\nA deterministic keyset is a keyset that can uniquely identify entities. This is an important concept to understand, so let's start by looking at an example.\n\n```\nb.Ascending(x =\u003e x.Created)\n```\n\nThe keyset above consists of only one column that accesses `Created`. If by design multiple entities might have the same `Created`, then this is *not* a deterministic keyset.\n\nThere are a few problems with a non deterministic keyset. Most importantly, you'll be skipping over data when paginating. This is a side effect of how keyset pagination works.\n\nFixing this is easy enough. In most cases, you can just add more columns until it becomes deterministic. Most commonly, you can add a column that accesses `Id`.\n\n```\nb.Ascending(x =\u003e x.Created).Ascending(x =\u003e x.Id)\n```\n\nThis makes the keyset deterministic because the combination of these particular columns will always resolve to uniquely identified entities.\n\nIf you can maintain this rule, and if your keyset's data doesn't change, you'll never skip over or duplicate data, a behavior that offset based pagination can never guarantee. We call this behavior _stable pagination_.\n\nKeep in mind that to get the most performance out of this we should have proper indexing that takes into account this composite keyset. This is discussed in the next section.\n\n## Indexing\n\nKeyset pagination — as is the case with any other kind of database query — can benefit a lot from good database indexing. Said in other words, not having a proper index defeats the purpose of using keyset pagination in the first place.\n\nYou'll want to add a composite index that is compatible with the columns and the order of your keyset.\n\nHere's an example. Let's say we're doing the following:\n\n```cs\nKeysetPaginate(\n    b =\u003e b.Descending(entity =\u003e entity.Created),\n    ...\n)\n```\n\nWe should add an index on the `Created` column for this query to be as fast as it can.\n\nAnother more complex example:\n\n```cs\nKeysetPaginate(\n    b =\u003e b.Descending(entity =\u003e entity.Score).Ascending(entity =\u003e entity.Id),\n    ...\n)\n```\n\nIn this case you'll want to create a composite index on `Score` + `Id`, but make sure they're compatible with the order above. i.e You should make the index descending on `Score` and ascending on `Id` (or the opposite) for it to be effective.\n\n\u003e [!NOTE]\n\u003e Refer to [this document](https://docs.microsoft.com/en-us/ef/core/modeling/indexes) on how to create indexes with EF Core. Note that support for specifying sort order in a composite index was introduced in EF Core 7.0.\n\n## Benchmarks\n\nTo give you an idea of the performance gains, here's a graph comparing using offset pagination vs keyset pagination when querying first, middle, and last pages under different table records counts.\n\nThe following are the different methods being benchmarked:\n- FirstPage: Query the first page\n- MidPage: Query the middle page (i.e for N=1K this benchmark queries the data starting from the 500's record)\n- LastPage: Query the last page\n\nFor a common use case, this is when the data is ordered in `Created` descending (a `DateTime` property).\n\n\u003cimg src=\"images/benchmarks/benchmark-CreatedDesc-grid.png\" width=\"600\" /\u003e\n\nNotice that when querying the first page, offset pagination does just as well as keyset. Offset pagination starts falling behind remarkably the further away the page you want to read is. Do consider this when choosing what method you want to use.\n\nTo that point, the keyset bars (green) are barely visible in the MidPage and LastPage graphs. This shows a major advantage of keyset pagination over offset pagination, that is the stable performance characteristic over large amounts of data even when querying _further away_ pages.\n\n\u003c!-- Another example with a more complicated order, a composite keyset of `Created` descending + `Id` Descending.\n\n\u003cimg src=\"benchmarks/Benchmarks.Basic/Plot/out/benchmark-CreatedDescIdDesc.png\" width=\"600\" /\u003e --\u003e\n\nCheck the [benchmarks](benchmarks) folder for the source code.\n\nCheck this [blog post](https://github.com/mrahhal/blog/blob/main/posts/2023-05-14-offset-vs-keyset-pagination/post.md) for a more detailed look into the benchmarks.\n\n## Caveats\n\nCheck [this document](docs/caveats.md) on a few caveats to keep in mind when working with keyset pagination.\n\n## Samples\n\nCheck the [samples](samples) folder for project samples.\n\n- [Basic](samples/Basic): This is a quick example of a page that has First/Previous/Next/Last links (using razor pages).\n\n## Talks\n\n[.NET Standup session](https://www.youtube.com/watch?v=DIKH-q-gJNU) where we discuss pagination and showcase this package.\n\n[![.NET Standup session](https://img.youtube.com/vi/DIKH-q-gJNU/0.jpg)](https://www.youtube.com/watch?v=DIKH-q-gJNU)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrahhal%2Fmr.entityframeworkcore.keysetpagination","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrahhal%2Fmr.entityframeworkcore.keysetpagination","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrahhal%2Fmr.entityframeworkcore.keysetpagination/lists"}