Skip to content

Commit c2397e6

Browse files
committed
release: 6.0.0
Prepare the 6.0.0 release for the Paginable and Multi modules. - README: document 6.0 highlights, the per-package target framework matrix, keyset (seek) pagination, asynchronous paging and PaginableSettings; add building/testing and releasing sections; fix broken code samples. - build/version.props: make it the single source of truth by exporting Version/PackageVersion, and drop the hard-coded PackageVersion from all 11 project files so a bump here actually reaches the packages. - Publish.bat / PublishToMyget.bat: include DotNetCore.Collections.Multi, push .snupkg symbol packages, use `dotnet nuget push --skip-duplicate`, read the key from NUGET_API_KEY / MYGET_API_KEY and fail fast on error. Pack with -m:1 because DocumentationFile is one project-root file that parallel per-TFM builds race on (CS0016, "file used by another process"). - CI: apply the same -m:1 fix to both workflow build steps. - .gitignore: ignore the local nuget_pub output directory. Verified: solution builds with 0 errors, Paginable 76/76 and Multi 233/233 unit tests pass, and dotnet pack produces 22 artifacts (11 .nupkg + 11 .snupkg) all at 6.0.0.
1 parent 1645d49 commit c2397e6

18 files changed

Lines changed: 242 additions & 58 deletions

File tree

.github/workflows/multi-tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ jobs:
3030
dotnet-version: 10.0.x
3131

3232
- name: Build (netstandard2.0 / netstandard2.1 / net6.0)
33-
run: dotnet build src/DotNetCore.Collections.Multi/DotNetCore.Collections.Multi.csproj -c Release
33+
# -m:1: DocumentationFile is a single project-root file, so parallel per-TFM
34+
# builds race on it (CS0016). Serialising the inner builds keeps CI deterministic.
35+
run: dotnet build src/DotNetCore.Collections.Multi/DotNetCore.Collections.Multi.csproj -c Release -m:1
3436

3537
- name: Pack
3638
run: dotnet pack src/DotNetCore.Collections.Multi/DotNetCore.Collections.Multi.csproj -c Release --no-build

.github/workflows/paginable-tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ jobs:
3535
dotnet-version: 10.0.x
3636

3737
- name: Build core library (11 TFMs)
38-
run: dotnet build src/DotNetCore.Collections.Paginable/DotNetCore.Collections.Paginable.csproj -c Release
38+
# -m:1: DocumentationFile is a single project-root file, so parallel per-TFM
39+
# builds race on it (CS0016). Serialising the inner builds keeps CI deterministic.
40+
run: dotnet build src/DotNetCore.Collections.Paginable/DotNetCore.Collections.Paginable.csproj -c Release -m:1
3941

4042
- name: Pack (verify package generation incl. snupkg)
4143
run: dotnet pack src/DotNetCore.Collections.Paginable/DotNetCore.Collections.Paginable.csproj -c Release --no-build

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,6 @@ reports/
299299

300300
# 本地 .git 备份目录(沙箱 git 操作安全网,禁止提交)
301301
.git-backup*/
302+
303+
# 本地 NuGet 发布产物(Publish.bat / PublishToMyget.bat 输出目录,禁止提交)
304+
nuget_pub/

Publish.bat

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
@echo off
2+
setlocal
3+
4+
rem Publishes every DotNetCore.Collections package (Multi + Paginable family) to nuget.org.
5+
rem Usage: Publish.bat
6+
rem set NUGET_API_KEY=<key> && Publish.bat (non-interactive / CI)
7+
rem Requires: .NET SDK 8.0 or later on PATH.
8+
29
if not exist nuget_pub (
310
md nuget_pub
411
)
@@ -7,32 +14,60 @@ for /R "nuget_pub" %%s in (*) do (
714
del "%%s"
815
)
916

10-
set /p key=input key:
11-
12-
::Paginable
13-
dotnet pack src/DotNetCore.Collections.Paginable -c Release -o nuget_pub
14-
dotnet pack src/DotNetCore.Collections.Paginable.Chloe -c Release -o nuget_pub
15-
dotnet pack src/DotNetCore.Collections.Paginable.DosOrm -c Release -o nuget_pub
16-
dotnet pack src/DotNetCore.Collections.Paginable.EntityFramework -c Release -o nuget_pub
17-
dotnet pack src/DotNetCore.Collections.Paginable.EntityFrameworkCore -c Release -o nuget_pub
18-
dotnet pack src/DotNetCore.Collections.Paginable.FreeSql -c Release -o nuget_pub
19-
dotnet pack src/DotNetCore.Collections.Paginable.FreeSql.DbContext -c Release -o nuget_pub
20-
dotnet pack src/DotNetCore.Collections.Paginable.NHibernate -c Release -o nuget_pub
21-
dotnet pack src/DotNetCore.Collections.Paginable.SqlKata -c Release -o nuget_pub
22-
dotnet pack src/DotNetCore.Collections.Paginable.SqlSugar -c Release -o nuget_pub
23-
24-
for /R "nuget_pub" %%s in (*symbols.nupkg) do (
25-
del "%%s"
17+
if defined NUGET_API_KEY (
18+
set "key=%NUGET_API_KEY%"
19+
) else (
20+
set /p key=input nuget.org api key:
2621
)
2722

23+
if not defined key (
24+
echo ERROR: no api key provided.
25+
goto :failed
26+
)
27+
28+
rem -m:1 is REQUIRED: DocumentationFile is a single file in the project directory, so
29+
rem parallel per-TFM builds race on it and fail with CS0016 ("file is being used by another
30+
rem process"). Serialising the inner builds keeps packing deterministic.
31+
rem Long-term fix: move DocumentationFile under the per-TFM output directory.
32+
33+
rem ::Multi
34+
dotnet pack src/DotNetCore.Collections.Multi -c Release -m:1 -o nuget_pub || goto :failed
35+
36+
rem ::Paginable
37+
dotnet pack src/DotNetCore.Collections.Paginable -c Release -m:1 -o nuget_pub || goto :failed
38+
dotnet pack src/DotNetCore.Collections.Paginable.Chloe -c Release -m:1 -o nuget_pub || goto :failed
39+
dotnet pack src/DotNetCore.Collections.Paginable.DosOrm -c Release -m:1 -o nuget_pub || goto :failed
40+
dotnet pack src/DotNetCore.Collections.Paginable.EntityFramework -c Release -m:1 -o nuget_pub || goto :failed
41+
dotnet pack src/DotNetCore.Collections.Paginable.EntityFrameworkCore -c Release -m:1 -o nuget_pub || goto :failed
42+
dotnet pack src/DotNetCore.Collections.Paginable.FreeSql -c Release -m:1 -o nuget_pub || goto :failed
43+
dotnet pack src/DotNetCore.Collections.Paginable.FreeSql.DbContext -c Release -m:1 -o nuget_pub || goto :failed
44+
dotnet pack src/DotNetCore.Collections.Paginable.NHibernate -c Release -m:1 -o nuget_pub || goto :failed
45+
dotnet pack src/DotNetCore.Collections.Paginable.SqlKata -c Release -m:1 -o nuget_pub || goto :failed
46+
dotnet pack src/DotNetCore.Collections.Paginable.SqlSugar -c Release -m:1 -o nuget_pub || goto :failed
47+
2848
echo.
2949
echo.
3050

31-
set source=https://www.nuget.org/api/v2/package
51+
set source=https://api.nuget.org/v3/index.json
52+
53+
for /R "nuget_pub" %%s in (*.nupkg) do (
54+
call dotnet nuget push "%%s" --api-key %key% --source %source% --skip-duplicate || goto :failed
55+
echo.
56+
)
3257

33-
for /R "nuget_pub" %%s in (*.nupkg) do (
34-
call nuget push "%%s" %key% -Source %source%
35-
echo.
58+
rem .snupkg symbol packages are published to the same endpoint (SourceLink enabled since 6.0).
59+
for /R "nuget_pub" %%s in (*.snupkg) do (
60+
call dotnet nuget push "%%s" --api-key %key% --source %source% --skip-duplicate || goto :failed
61+
echo.
3662
)
3763

38-
pause
64+
echo.
65+
echo All packages published.
66+
endlocal
67+
exit /b 0
68+
69+
:failed
70+
echo.
71+
echo Publish FAILED - see the errors above.
72+
endlocal
73+
exit /b 1

PublishToMyget.bat

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
@echo off
2+
setlocal
3+
4+
rem Publishes every DotNetCore.Collections package (Multi + Paginable family) to the MyGet feed.
5+
rem Usage: PublishToMyget.bat
6+
rem set MYGET_API_KEY=<key> && PublishToMyget.bat (non-interactive / CI)
7+
rem Requires: .NET SDK 8.0 or later on PATH.
8+
rem Note: the MyGet v2 endpoint does not accept .snupkg, so only .nupkg is pushed here.
9+
210
if not exist nuget_pub (
311
md nuget_pub
412
)
@@ -7,21 +15,39 @@ for /R "nuget_pub" %%s in (*) do (
715
del "%%s"
816
)
917

10-
set /p key=input key:
11-
12-
::Paginable
13-
dotnet pack src/DotNetCore.Collections.Paginable -c Release -o nuget_pub
14-
dotnet pack src/DotNetCore.Collections.Paginable.Chloe -c Release -o nuget_pub
15-
dotnet pack src/DotNetCore.Collections.Paginable.DosOrm -c Release -o nuget_pub
16-
dotnet pack src/DotNetCore.Collections.Paginable.EntityFramework -c Release -o nuget_pub
17-
dotnet pack src/DotNetCore.Collections.Paginable.EntityFrameworkCore -c Release -o nuget_pub
18-
dotnet pack src/DotNetCore.Collections.Paginable.FreeSql -c Release -o nuget_pub
19-
dotnet pack src/DotNetCore.Collections.Paginable.FreeSql.DbContext -c Release -o nuget_pub
20-
dotnet pack src/DotNetCore.Collections.Paginable.NHibernate -c Release -o nuget_pub
21-
dotnet pack src/DotNetCore.Collections.Paginable.SqlKata -c Release -o nuget_pub
22-
dotnet pack src/DotNetCore.Collections.Paginable.SqlSugar -c Release -o nuget_pub
23-
24-
for /R "nuget_pub" %%s in (*symbols.nupkg) do (
18+
if defined MYGET_API_KEY (
19+
set "key=%MYGET_API_KEY%"
20+
) else (
21+
set /p key=input myget api key:
22+
)
23+
24+
if not defined key (
25+
echo ERROR: no api key provided.
26+
goto :failed
27+
)
28+
29+
rem -m:1 is REQUIRED: DocumentationFile is a single file in the project directory, so
30+
rem parallel per-TFM builds race on it and fail with CS0016 ("file is being used by another
31+
rem process"). Serialising the inner builds keeps packing deterministic.
32+
rem Long-term fix: move DocumentationFile under the per-TFM output directory.
33+
34+
rem ::Multi
35+
dotnet pack src/DotNetCore.Collections.Multi -c Release -m:1 -o nuget_pub || goto :failed
36+
37+
rem ::Paginable
38+
dotnet pack src/DotNetCore.Collections.Paginable -c Release -m:1 -o nuget_pub || goto :failed
39+
dotnet pack src/DotNetCore.Collections.Paginable.Chloe -c Release -m:1 -o nuget_pub || goto :failed
40+
dotnet pack src/DotNetCore.Collections.Paginable.DosOrm -c Release -m:1 -o nuget_pub || goto :failed
41+
dotnet pack src/DotNetCore.Collections.Paginable.EntityFramework -c Release -m:1 -o nuget_pub || goto :failed
42+
dotnet pack src/DotNetCore.Collections.Paginable.EntityFrameworkCore -c Release -m:1 -o nuget_pub || goto :failed
43+
dotnet pack src/DotNetCore.Collections.Paginable.FreeSql -c Release -m:1 -o nuget_pub || goto :failed
44+
dotnet pack src/DotNetCore.Collections.Paginable.FreeSql.DbContext -c Release -m:1 -o nuget_pub || goto :failed
45+
dotnet pack src/DotNetCore.Collections.Paginable.NHibernate -c Release -m:1 -o nuget_pub || goto :failed
46+
dotnet pack src/DotNetCore.Collections.Paginable.SqlKata -c Release -m:1 -o nuget_pub || goto :failed
47+
dotnet pack src/DotNetCore.Collections.Paginable.SqlSugar -c Release -m:1 -o nuget_pub || goto :failed
48+
49+
rem MyGet v2 endpoint rejects .snupkg - drop them before pushing.
50+
for /R "nuget_pub" %%s in (*.snupkg) do (
2551
del "%%s"
2652
)
2753

@@ -30,9 +56,18 @@ echo.
3056

3157
set source=https://www.myget.org/F/alexinea/api/v2/package
3258

33-
for /R "nuget_pub" %%s in (*.nupkg) do (
34-
call nuget push "%%s" %key% -Source %source%
35-
echo.
59+
for /R "nuget_pub" %%s in (*.nupkg) do (
60+
call dotnet nuget push "%%s" --api-key %key% --source %source% --skip-duplicate || goto :failed
61+
echo.
3662
)
3763

38-
pause
64+
echo.
65+
echo All packages published.
66+
endlocal
67+
exit /b 0
68+
69+
:failed
70+
echo.
71+
echo Publish FAILED - see the errors above.
72+
endlocal
73+
exit /b 1

README.md

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,37 @@
66

77
NCC Collections consists of a set of collection-based extensions and tools, such as paging extensions and multiset/multimap collections.
88

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+
940
## Nuget Packages
1041

1142
| Package Name | Version | Downloads |
@@ -52,7 +83,7 @@ Or use a more streamlined code:
5283
IEnumerable<ExampleModel> list = GetList();//...
5384
5485
//Get page 15th, each page has 50 items.
55-
ar page = list.GetPage(15, 50);
86+
var page = list.GetPage(15, 50);
5687

5788
for (var i = 0; i < page.CurrentPageSize; i++)
5889
{
@@ -222,9 +253,9 @@ Install-Package DotNetCore.Collections.Paginable.SqlSugar
222253
then:
223254

224255
```c#
225-
var sqlSugar = new SqlSugatClient(new ConnectionConfig{
256+
var sqlSugar = new SqlSugarClient(new ConnectionConfig{
226257
ConnectionString = connectionString,
227-
DbType = DbTypee.SqlServer,
258+
DbType = DbType.SqlServer,
228259
IsAutoCloseConnection = true
229260
});
230261

@@ -285,14 +316,70 @@ then:
285316
```c#
286317
using(var context = new ExampleDbContext())
287318
{
288-
var pagee = context.ExampleModels.GetPage(1, 9);
319+
var page = context.ExampleModels.GetPage(1, 9);
289320

290321
var totalPageCount = page.TotalPageCount;
291322
//...
292323
}
293324
//...
294325
```
295326

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+
296383
#### For SqlKata with Dapper
297384

298385
Install `DotNetCore.Collections.Paginable.SqlKata` package:
@@ -360,6 +447,31 @@ var lookup = map.AsLookup(); // LINQ-friendly ILookup view
360447

361448
- [Sample.Multi](https://github.com/dotnetcore/Collections/blob/dev/sample/Sample.Multi/Program.cs)
362449

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+
363475
## License
364476

365477
Member project of [The NCC](https://github.com/dotnetcore), MIT

build/version.props

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,11 @@
55
<VersionPatch>0</VersionPatch>
66
<VersionQuality>0</VersionQuality>
77
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch).$(VersionQuality)</VersionPrefix>
8+
9+
<!-- Single source of truth for the whole repo: every package ships the same version,
10+
bump here only. Do NOT set <Version> or <PackageVersion> in individual .csproj
11+
files - they are evaluated after this import and would silently win. -->
12+
<Version>$(VersionPrefix)</Version>
13+
<PackageVersion>$(VersionPrefix)</PackageVersion>
814
</PropertyGroup>
9-
</Project>
15+
</Project>

0 commit comments

Comments
 (0)