|
6 | 6 |
|
7 | 7 | NCC Collections consists of a set of collection-based extensions and tools, such as paging extensions and multiset/multimap collections. |
8 | 8 |
|
| 9 | +## What's new in 6.0 |
| 10 | + |
| 11 | +6.0 is a modernization release covering both shipped modules (`Paginable` and `Multi`). |
| 12 | + |
| 13 | +| Area | 5.x | 6.0 | |
| 14 | +| --- | --- | --- | |
| 15 | +| Target frameworks (core) | `net451`; `net461`; `netstandard2.1`; `net5.0` | `net451`; `net461`; `net47`; `net48`; `netstandard2.0`; `netstandard2.1`; `net6.0` – `net10.0` (11 TFMs) | |
| 16 | +| Pagination algorithms | offset only | offset **and** keyset / seek (`GetPageByKeyset`) | |
| 17 | +| Async | 3 ORMs, synchronous SQL under the hood | true end-to-end async (`CountAsync` + `ToListAsync`) for EF Core / FreeSql / SqlSugar, with `CancellationToken` passthrough | |
| 18 | +| Enumeration performance | `ElementAt` → O(skip²) on non-indexed sources | single `Skip`/`Take` materialization; lazy one-shot materialization in NHibernate | |
| 19 | +| Correctness | `CurrentPageSize` wrong on exact-multiple last page; `MaxMemberItems` off-by-one; no argument validation | all fixed; `pageNumber >= 1` / `pageSize >= 1` enforced across core and every ORM integration | |
| 20 | +| `Multi` module | `netstandard2.0` only, minimal API surface | `netstandard2.0` / `netstandard2.1` / `net6.0`; rewritten `MultiList<T>` plus a complete `MultiDictionary<TKey, TValue>` | |
| 21 | +| Packaging | plain packages | deterministic build, SourceLink, `.snupkg` symbol packages, `packages.lock.json` | |
| 22 | +| Quality gates | none | 2 GitHub Actions workflows; 76 Paginable + 233 Multi unit tests, plus SQL Server integration tests | |
| 23 | + |
| 24 | +### Supported target frameworks |
| 25 | + |
| 26 | +| Package | Target frameworks | |
| 27 | +| --- | --- | |
| 28 | +| `DotNetCore.Collections.Paginable` | `net451`, `net461`, `net47`, `net48`, `netstandard2.0`, `netstandard2.1`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 29 | +| `DotNetCore.Collections.Paginable.Chloe` | `net461`, `net47`, `net48`, `netstandard2.0`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 30 | +| `DotNetCore.Collections.Paginable.DosORM` | `netstandard2.1`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 31 | +| `DotNetCore.Collections.Paginable.EntityFramework` | `net451`, `net461`, `net47`, `net48`, `netstandard2.1`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 32 | +| `DotNetCore.Collections.Paginable.EntityFrameworkCore` | `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 33 | +| `DotNetCore.Collections.Paginable.FreeSql` | `net451`, `net461`, `net47`, `net48`, `netstandard2.0`, `netstandard2.1`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 34 | +| `DotNetCore.Collections.Paginable.FreeSql.DbContext` | `net451`, `net461`, `net47`, `net48`, `netstandard2.0`, `netstandard2.1`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 35 | +| `DotNetCore.Collections.Paginable.NHibernate` | `net461`, `net47`, `net48`, `netstandard2.0`, `netstandard2.1`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 36 | +| `DotNetCore.Collections.Paginable.SqlKata` | `net451`, `net461`, `net47`, `net48`, `netstandard2.0`, `netstandard2.1`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 37 | +| `DotNetCore.Collections.Paginable.SqlSugar` | `net451`, `net461`, `net47`, `net48`, `netstandard2.1`, `net6.0`, `net7.0`, `net8.0`, `net9.0`, `net10.0` | |
| 38 | +| `DotNetCore.Collections.Multi` | `netstandard2.0`, `netstandard2.1`, `net6.0` | |
| 39 | + |
9 | 40 | ## Nuget Packages |
10 | 41 |
|
11 | 42 | | Package Name | Version | Downloads | |
@@ -52,7 +83,7 @@ Or use a more streamlined code: |
52 | 83 | IEnumerable<ExampleModel> list = GetList();//... |
53 | 84 |
|
54 | 85 | //Get page 15th, each page has 50 items. |
55 | | -ar page = list.GetPage(15, 50); |
| 86 | +var page = list.GetPage(15, 50); |
56 | 87 |
|
57 | 88 | for (var i = 0; i < page.CurrentPageSize; i++) |
58 | 89 | { |
@@ -222,9 +253,9 @@ Install-Package DotNetCore.Collections.Paginable.SqlSugar |
222 | 253 | then: |
223 | 254 |
|
224 | 255 | ```c# |
225 | | -var sqlSugar = new SqlSugatClient(new ConnectionConfig{ |
| 256 | +var sqlSugar = new SqlSugarClient(new ConnectionConfig{ |
226 | 257 | ConnectionString = connectionString, |
227 | | - DbType = DbTypee.SqlServer, |
| 258 | + DbType = DbType.SqlServer, |
228 | 259 | IsAutoCloseConnection = true |
229 | 260 | }); |
230 | 261 |
|
@@ -285,14 +316,70 @@ then: |
285 | 316 | ```c# |
286 | 317 | using(var context = new ExampleDbContext()) |
287 | 318 | { |
288 | | - var pagee = context.ExampleModels.GetPage(1, 9); |
| 319 | + var page = context.ExampleModels.GetPage(1, 9); |
289 | 320 |
|
290 | 321 | var totalPageCount = page.TotalPageCount; |
291 | 322 | //... |
292 | 323 | } |
293 | 324 | //... |
294 | 325 | ``` |
295 | 326 |
|
| 327 | +### Keyset (seek) pagination |
| 328 | + |
| 329 | +Offset pagination degrades on deep pages because the database still scans the skipped rows. |
| 330 | +Keyset (a.k.a. seek / cursor) pagination replaces `OFFSET n` with a `WHERE key > @lastKey` |
| 331 | +predicate, so every page costs the same and the `COUNT(*)` round trip is avoided. It is the |
| 332 | +recommended mode for infinite-scroll and cursor-style APIs. |
| 333 | + |
| 334 | +```c# |
| 335 | +IQueryable<ExampleModel> queryable = GetQueryable();//... |
| 336 | +
|
| 337 | +// First page: no anchor key yet. |
| 338 | +var first = queryable.GetFirstPageByKeyset(x => x.Id, pageSize: 50); |
| 339 | + |
| 340 | +// Subsequent pages: pass the ordering key of the last row of the previous page. |
| 341 | +var lastId = first.LastMember.Id; |
| 342 | +var next = queryable.GetPageByKeyset(x => x.Id, lastId, pageSize: 50); |
| 343 | + |
| 344 | +foreach (var item in next.Members) { /* ... */ } |
| 345 | + |
| 346 | +var hasMore = next.HasNext; // resolved without COUNT(*) |
| 347 | +``` |
| 348 | + |
| 349 | +`GetFirstPageByKeyset` / `GetPageByKeyset` also have `IEnumerable<T>` overloads for in-memory |
| 350 | +sources, and an optional `descending` switch for reverse ordering. Use keyset pagination when you |
| 351 | +do **not** need `TotalPageCount` / `TotalMemberCount`; use the offset APIs above when you do. |
| 352 | + |
| 353 | +### Asynchronous paging |
| 354 | + |
| 355 | +The core library exposes `ToPaginableAsync` / `GetPageAsync` for in-memory and `IQueryable<T>` |
| 356 | +sources, and the EF Core, FreeSql and SqlSugar integrations provide true end-to-end async |
| 357 | +(`CountAsync` + `ToListAsync`, no synchronous database calls) with `CancellationToken` support. |
| 358 | + |
| 359 | +```c# |
| 360 | +using(var context = new ExampleDbContext()) |
| 361 | +{ |
| 362 | + var page = await context.ExampleModels |
| 363 | + .GetPageAsync(pageNumber: 1, pageSize: 50, cancellationToken: ct); |
| 364 | + |
| 365 | + var totalMemberCount = page.TotalMemberCount; |
| 366 | +} |
| 367 | +``` |
| 368 | + |
| 369 | +### Configuration |
| 370 | + |
| 371 | +`PaginableSettingsManager` holds a process-wide settings snapshot. Values are validated on |
| 372 | +assignment, so any instance handed out by the library is always in a valid state — configure it |
| 373 | +once at startup and treat it as read-only afterwards. |
| 374 | + |
| 375 | +```c# |
| 376 | +PaginableSettingsManager.Settings = new PaginableSettings |
| 377 | +{ |
| 378 | + DefaultPageSize = 50, // must be >= 1 |
| 379 | + MaxMemberItems = 10_000_000 // must be >= 1 |
| 380 | +}; |
| 381 | +``` |
| 382 | + |
296 | 383 | #### For SqlKata with Dapper |
297 | 384 |
|
298 | 385 | Install `DotNetCore.Collections.Paginable.SqlKata` package: |
@@ -360,6 +447,31 @@ var lookup = map.AsLookup(); // LINQ-friendly ILookup view |
360 | 447 |
|
361 | 448 | - [Sample.Multi](https://github.com/dotnetcore/Collections/blob/dev/sample/Sample.Multi/Program.cs) |
362 | 449 |
|
| 450 | +## Building and testing |
| 451 | + |
| 452 | +```bash |
| 453 | +dotnet build DotNetCore.Collections.sln -c Release |
| 454 | + |
| 455 | +dotnet test tests/DotNetCore.Collections.Paginable.Tests -c Release |
| 456 | +dotnet test tests/DotNetCore.Collections.Multi.Tests -c Release |
| 457 | +``` |
| 458 | + |
| 459 | +The unit tests run offline. The integration tests in `tests/DotNetCore.Collections.Paginable.DbTests` |
| 460 | +need a SQL Server instance and read their connection string from the |
| 461 | +`PAGINABLE_DBTESTS_CONNECTION_STRING` environment variable; on CI they run against a SQL Server 2022 |
| 462 | +service container. |
| 463 | + |
| 464 | +Two GitHub Actions workflows gate the `dev` and `master` branches: |
| 465 | + |
| 466 | +- `paginable-tests.yml` — builds all 11 TFMs, verifies packing (including `.snupkg`), then runs the unit tests and the SQL Server integration tests. |
| 467 | +- `multi-tests.yml` — builds, packs and tests `DotNetCore.Collections.Multi`. |
| 468 | + |
| 469 | +## Releasing |
| 470 | + |
| 471 | +Versions are driven by `build/version.props`; every package is produced with `dotnet pack` and |
| 472 | +published by `Publish.bat` (nuget.org) or `PublishToMyget.bat` (MyGet). Both scripts cover all |
| 473 | +11 packages, `DotNetCore.Collections.Multi` included. |
| 474 | + |
363 | 475 | ## License |
364 | 476 |
|
365 | 477 | Member project of [The NCC](https://github.com/dotnetcore), MIT |
|
0 commit comments