All notable changes to the DotNetCore.Collections packages are documented here.
Versions follow Semantic Versioning; every package in this
repository ships the same version (see build/version.props).
BiDictionary<TLeft, TRight>- a strict one-to-one (bijective) map (F6-03): every left value maps to exactly one right value and no right value is shared by two lefts, with both directions answered in O(1) by two indexes kept in step on every write path — so a removed or rebound entry frees its partner immediately on the other side. Conflict handling is strict (R3-01 decision):AddthrowsArgumentExceptionwhen the left value already has a binding or when the right value is already bound to a different left value,TryAddreports the same conditions without throwing, and there is deliberately no silently-overwriting setter — overwriting a right value would silently unbind the left value it used to belong to, an entry the caller never mentioned, so breaking an existing binding is always an explicitRemove(left)orRemoveRight(right)first. The type implementsIReadOnlyDictionary<TLeft, TRight>for the forward direction and exposes the reverse direction throughTryGetLeft/GetLeft(right)/ContainsRightplusAsReverse(), a live read-onlyIReadOnlyDictionary<TRight, TLeft>view served from the same indexes.nullis accepted on both sides through dedicated buckets (the binding(null, null)is expressible and occupies one entry);ToDictionary()exports an independent snapshot that omits anull-left binding, because aDictionary<TLeft, TRight>can not key onnull— the remark says so explicitly.
Release covering both shipped modules (Paginable and Multi); every package ships version
6.2.0.0 (see build/version.props).
OrderedMultiList<T>- an ordered multiset (sorted bag), the ordered counterpart ofMultiList<T>and the equivalent of PowerCollections'OrderedBag<T>. It sharesMultiList<T>'s copy-counting semantics - one distinct element with N copies, duplicates consecutive and expanded on enumeration, the sameUnionWith/IntersectionWith/ExceptWith/SymmetricExceptWithand subset / superset judgments - and adds an order: enumeration is ascending, and lookup, insertion and removal are O(log n) worst case. Elements are compared with an injectableIComparer<T>, deliberately not anIEqualityComparer<T>: an equality comparer supplies hash codes but no ordering, whereas a red-black tree has to know which of two elements comes first, and it is the comparison result0that decides two elements are the same element (the stored element is the first one added).nullis supported - underComparer<T>.Defaultit sorts first, while a custom comparer decides for itself wherenullbelongs and may reject it.OrderedMultiList<T>ordered access:GetFirst()/GetLast()(each O(log n), throwingInvalidOperationExceptionon an empty multiset),Reverse()for descending enumeration, andGetRange(from, to)plus aGetRange(from, to, inclusiveFrom, inclusiveTo)overload for range queries. Subtrees outside the bounds are skipped, so a query costs O(log n + k) for k copies reported rather than a full traversal. As withMultiList<T>, the type implementsICollection<T>andIReadOnlyCollection<T>(copy-expandedCount), and its set operations treat their argument as a multiset, so multiplicities count.- F6-01's O(log n) claim is proved rather than timed. The storage engine is a self-implemented
left-leaning red-black tree, and the test suite asserts the red-black invariants - black
root, no red node with a red child, equal black height, plus the left-leaning rule - together
with the height bound
height <= 2 * log2(n + 1). It re-checks them after every single removal of a 300-key drain, and across 5,000 randomized operations compared with aSortedDictionarymodel; 10,000 sequentially ascending inserts stay under 30 levels where an unbalanced tree would be 10,000 deep. Timing baselines are deliberately avoided - they are noise on CI. - Two deliberate omissions and one carry-over, so that neither is mistaken for an oversight.
OrderedMultiList<T>does not overrideEquals/GetHashCode: the package's rule is thatMultiList<T>is the only type carrying structural equality, and adding equality later is a non-breaking change while removing it would not be. It has noToDictionary()either, because itsIComparer<T>orders elements but supplies no hash codes for a dictionary to use, soEntrySet()is the export path. AndAdd(item, times)/Remove(item, times)keepMultiList<T>'s legacy "a non-positivetimesis coerced to one copy" behaviour verbatim, which means M6-05 (times <= 0throws) has to cover this type as well when it lands. DotNetCore.Collections.Multinow declaresInternalsVisibleTofor its test assembly. The red-black invariants live on internal members and asserting them is how F6-01 justifies its complexity, so the suite has to reach them. Nothing becomes public: the members stay internal.OrderedMultiDictionary<TKey, TValue>- the ordered counterpart ofMultiDictionary<TKey, TValue>(F6-02): a multimap whose keys are kept in ascending order by anIComparer<TKey>(aSortedDictionaryaxis) and whose per-key values are kept in ascending order by anIComparer<TValue>(anOrderedMultiList<TValue>in the default duplicating configuration, aSortedSet<TValue>when duplicate values are disallowed). Adding, looking up and removing a single pair costs O(log n) worst case on both axes. The per-key value-set operations (UnionWith/IntersectionWith/ExceptWith/SymmetricExceptWith/RemoveRange) keepMultiDictionary<TKey, TValue>'s semantics verbatim: the argument is a set of values, a value stored N times survivesRemoveRangewith N-1 copies,ExceptWithdrops every occurrence, and a key whose value collection empties is removed automatically. The type also implementsIReadOnlyDictionary<TKey, IReadOnlyCollection<TValue>>and shipsAsLookup()/AsReadOnly()/Clone()/EntrySet(),ValueCount(key)/TotalValueCount,ContainsValue, andToString()walking keys ascending. Identity on both axes is decided by the respective comparers (nullkeys are rejected withArgumentNullException;nullvalues follow the value comparer, sorting first under the default one), and thenotnullkey constraint of the annotatedSortedDictionaryis suppressed file-locally, exactly as inMultiDictionary, soTKeystays nullable-friendly.TwoKeyDictionary<K1, K2, V>now answers its second axis from a maintained reverse index (F6-21). The underlying trie is keyed by(K1, K2)in that order, so the second component is not a prefix and a subtree walk can not reach it;GetBySecondKey,CountOfSecondKey,ContainsSecondKeyandRemoveBySecondKeytherefore used to scan the whole map. They now read aK2 -> set of K1index kept in step on every write path -Add(both overloads),TryAdd,Remove(both overloads), theRemoveByFirstKeycascade,RemoveBySecondKeyandClear- so only the requested slice is visited: O(1) forCountOfSecondKeyandContainsSecondKey, and O(s) trie lookups for a slice of s entries for the other two. The index uses the same injected comparers as the map (the second one keys the index, the first one the per-second-key set), and anullsecond component gets a dedicated bucket becauseDictionary<TKey, TValue>rejects anullkey. The cost is one extra dictionary operation per write and one set entry per stored pair, plus one temporary list perRemoveByFirstKeycascade.Keys2deliberately still walks the trie, preserving its first-encounter enumeration order. TheGetBySecondKeyXML docs no longer describe the method as O(n).- Explicit serialization entry points for
MultiList<T>andMultiDictionary<TKey, TValue>(M6-06):ToSerializableModel()and the staticFromModel(), backed by two new plain models.MultiListModel<T>carriesItems(the distinct elements) plusCounts(one multiplicity per item);MultiDictionaryModel<TKey, TValue>carriesKeysplusValues(one value list per key). Both are ordinary mutable POCOs - public settable properties, no attributes, no interface implementations, no base type - so serializing them needs no particular serializer and the library takes a dependency on none;System.Text.Jsonis one option among many rather than a requirement, and the tests assert that the shipped assembly references no serializer at all. Three points are deliberate. The models carry data only: comparers and the multimap's inner-collection strategy are configuration rather than data, so they are not serialized andFromModeltakes them as arguments (the rebuilt collection is therefore only as faithful as the comparer passed back in). The models handle whatToDictionary()can not: anullelement is an ordinary entry inItems, whereasToDictionary()has to throw because anullcan not be a dictionary key (anullkey still can not be rebuilt, since the multimap rejects those). And a model is a snapshot whileAsReadOnly()andToDictionary()'s inner collections are live views, which is the property a serializer needs.FromModelvalidates the model rather than trusting it (null model, null list, mismatched lengths, non-positive copy count, null inner value list), names the offending argument, and merges elements a comparer calls equal in the multiset reading. - Two caches under
MultiDictionary<TKey, TValue>(M6-07), both landing L-05 and L-06 and both invisible from the outside: no signature changed and no behaviour moved, so the full suite passes unchanged and the pair is a pure performance change.TotalValueCountis now a stored count maintained by every mutation rather than a walk over all inner collections (L-06), andContainsValueis answered by a backwards index - value → keys - maintained on the write path rather than by scanning every key's values (L-05). Both caches are updated by every path that can move them, which is the whole risk of the change:Add(including the branch where a custom inner factory hands back a non-empty collection),AddRange, bothRemoveoverloads,RemoveRange,IntersectionWith,ExceptWith,SymmetricExceptWith,ClearandClone(rebuilt through the publicAddso the caches are maintained rather than copied). Three details are deliberate. Anullvalue is an ordinary value and gets a dedicated bucket, because aDictionary<TValue, …>can not key onnull; value equality isEqualityComparer<TValue>.Default, the same notion the per-key collections use. Key membership in the index uses the map's ownIEqualityComparer<TKey>, so removing"a"clears the entry indexed under"A"when the comparer says they are the same key. And a value index entry is dropped as soon as its last key is gone, soContainsValuenever answers from a value nobody stores. The write-path cost the acceptance criteria asked to measure is pinned the same way the gain is: a value type counts its own equality and hash operations, and the suite asserts that maintaining the index during anAddcosts a constant few, whether 1 or 4,096 values are already stored, while aContainsValueon 4,096 values costs the same as on 64. Allocation of the read paths is asserted to be zero viaGC.GetAllocatedBytesForCurrentThread(). As usual the claim is proved by counting, not by a stopwatch. A 3,000-step randomized differential re-checks both caches after every single step against a naive recomputation. - The set operations of
MultiList<T>no longer copy their argument (M6-08, landing L-07). Every one of them -UnionWith,IntersectionWith,ExceptWith,SymmetricExceptWith, the four subset/superset judgments, andEquals/GetHashCode's helpers - used to start by materialisingotheras a whole secondMultiList<T>, so a chaineda.UnionWith(b)allocated a full multiset just to read it back; on a 32-element argument that was 1,712-2,440 bytes per call. When the argument already is aMultiList<T>whose element comparer is equivalent to the receiver's, it is now read in place: the operation walks its count table directly, which is a struct-enumerator walk. The three mutating operations additionally stage their target state in a single buffer that is allocated once per instance and reused (cleared afterwards, so it retains no element references). Measured withGC.GetAllocatedBytesForCurrentThread(): every one of the eight operations allocates 0 bytes/call in steady state, and five of them (the four judgments andUnionWith) allocate 0 bytes/call even on a receiver that has never run a set operation before - the remaining three pay ~310 bytes once, for that buffer, on a cold receiver only. Three points are deliberate. An argument of any other shape - an array, a LINQ sequence, or aMultiList<T>built with a different comparer - still goes through the original materialising path, because its multiplicities have to be counted under the receiver's comparer before the operation can define its result; that path is unchanged and still correct, and the tests assert the two argument shapes never disagree. Thenullbucket is handled separately and only touched when it can be non-empty, because for a value element type it is always empty anddefault!would otherwise name the elementdefault(T)-0in aMultiList<int>- and wipe it. And the behaviour is otherwise bit-for-bit what it was:IsSubsetOfBag/IsSupersetOfBagwere rewritten to walk the count tables instead ofEntrySet(), whose iterator allocated on every call, with the subset/superset andnullcomparisons preserved. A 3,000-step randomized differential runs each operation twice, once with a multiset argument and once with an equivalent plain array, and requires the two outcomes to agree at every step. ImmutableMultiList<T>,ImmutableMultiDictionary<TKey, TValue>,ConcurrentMultiDictionary<TKey, TValue>andConcurrentMultiList<T>(M6-09, closing L-09) — the thread-safe faces of the package. The two immutable types wrap a snapshot of the mutable type that is never touched after construction, so reading needs no locks or fences. Every mutation returns a new instance, or the receiver itself when nothing would change (an immutable instance may safely be shared, so "no change" needs no copy); bulk mutation is expected throughToBuilder(). The builder shares the source's state until its first write — a copy-on-write the caller can observe, becauseToImmutable()on an untouched builder hands back the very source instance — and a freeze moves the working state into the frozen instance while the builder continues on a private copy, so writes after a freeze can never leak into it. The two concurrent types keep the mutable semantics under contention.ConcurrentMultiDictionary<TKey, TValue>routes keys to shards, each an independentMultiDictionary<TKey, TValue>behind its own lock, so writes on different keys proceed in parallel, while whole-map reads take a consistent snapshot by locking every shard once, in index order.ConcurrentMultiList<T>deliberately stays single-lock, because a bag has one global state its operations compare against, and sharding would trade that simple semantics for little gain. Enumeration on both concurrent types is over a snapshot, immune to concurrent writes. The stress tests run eight writers over a shared key domain while three readers hammer snapshots and whole-map reads mid-flight; the settled state is exactly predictable, because every writer removes only occurrences it added itself.ThreeKeyDictionary<K1, K2, K3, V>(M6-13) — the three-component counterpart ofTwoKeyDictionary<K1, K2, V>, kept deliberately thin (R2-03): the same axis-tag scheme — every component stored wrapped in a tag recording the position it came from, so three same-typed axes can not collide and each axis's injected comparer is dispatched by position rather than runtime type — a typed three-axis indexer, and per-axis projections (Keys1/Keys2/Keys3). The first axis is a prefix and its slice (GetByFirstKey/CountOfFirstKey/RemoveByFirstKey) is a trie walk; the second and third axes are deliberately not backed by an index here, their slices scan at O(n), and the XML remarks say so — when a later axis must be queried hard,MultiKeyDictionary<TKey, TValue>with a key order that puts that axis first is the tool. No logic was sunk out ofTwoKeyDictionaryand nothing in it changed.PageCreationOptionsand strict fragment checking (F6-11). 6.1 tolerated a fragment shorter than its metadata says - a concurrent delete upstream must not make the page unbuildable - and that stays the default:Paginable.CreatePage(fragment, info), the four-argumentCreatePageandfragment.ToPage(...)behave exactly as before. When a short fragment is more likely a bug than a race - a staletotalMemberCount, a fragment sliced by the wrong query - passPageCreationOptions.Strictthrough the new overloadsCreatePage(fragment, info, options)/CreatePage(fragment, pageNumber, pageSize, totalMemberCount, options)/fragment.ToPage(..., options): the same situation throwsArgumentExceptionnaming the fragment. Only the short-fragment behaviour differs - the over-long checks throw in both modes - and anulloptions argument is rejected like any other.Paginable.CreateSinglePageSetandPaginableSinglePage<T>(P6-03) - the set shape of the same fragment API, for callers whose signature wants anIPaginable<T>(or anIEnumerable<IPage<T>>) while the data in hand is one already-assembled page. The fragment is wrapped as-is, soGetPage(1)is exactly the pageCreatePagewould have built, with identical metadata and members, and the same metadata andPageCreationOptionsarguments apply.PageCountis always one, which is deliberately not the wrapped page's ownTotalPageCount: a set must be able to serve every page it claims and this one holds a single page with no source to slice the others from. The set layer therefore answers "how many pages am I handing you" while the page keeps the source-wide numbering (a fragment of page 3 of 12 still reports page 3 of 12), andMemberCountreports the source-wide total, asPaginableSetBase<T>does.GetPageaccepts only one and reports anything else asArgumentOutOfRangeException. The factories return the concretePaginableSinglePage<T>, not the bare interface, becauseIPaginableexposes onlyPageSizeandMemberCount- without the concrete type thePageCountguarantee would be unreadable.Paginable.CreatePageAsync(P6-04) - the fragment entry points in an awaitable shape, so a caller that pages asynchronously does not have to special-case the path where the data is already in hand. It completes synchronously: a fragment is already in memory, soTask.FromResultwraps the same synchronous result and the task is finished before it is returned. Nothing here pretends to be I/O - use a provider-specific async extension when real I/O has to be awaited. Two consequences ofTask.FromResultare documented on every overload because they invert what an…Asyncname usually promises: validation throws synchronously from the call itself rather than through a faulted task, and theCancellationTokenparameter is accepted for signature symmetry but never observed. Both matchToPaginableAsync/GetPageAsyncas they have been since 6.0. All four overloads mirrorCreatePage,PageCreationOptionsincluded.
DotNetCore.Collections.Paginable.SqlKatano longer carries vulnerable transitives in its dependency graph (E6-03). SqlKata's two usable lines are both frozen with advisories still open underneath them, and neither can be upgraded past the problem:2.2.0is the last release that targetsnet451,3.2.3is what thenetstandard2.x/net6.0/net7.0group can take, and4.xrequiresnet8.0. The three affected transitives are therefore pinned forward, one framework group at a time. Onnet451,NETStandard.Library 1.6.1(via SqlKata 2.2.0) used to supplySystem.Net.Http 4.3.0(CVE-2018-8292) andSystem.Text.RegularExpressions 4.3.0(CVE-2019-0820); they are now4.3.4and4.3.1. Becausenet451takes both of them from the framework — their NuGet assets are the_._placeholders — that pin costs no assembly and only settles the audit. Onnetstandard2.0/2.1/net6.0/net7.0,Dapper 1.50.5(viaSqlKata.Execution 3.2.3) used to pinSystem.Data.SqlClient 4.4.0(CVE-2024-0056 / CVE-2022-41064); it is now4.8.6, the first release past both advisories.net461/net47/net48resolve the twonetstandard1.xtransitives from the framework andnet8.0and above run on SqlKata 4.0.1, so those groups were already clean and are untouched. The audit is settled by fixing the graph rather than byNoWarn, and the raised floors are recorded in the shippedpackages.lock.json.- The
<example>sections ofPaginableCalc.GetRealMemberCount/GetRealPageCountshow correctly-arity samples (P6-02). Both were added by the same docs pass and the first calledGetRealMemberCount(0, 50, 120)— three arguments for a two-argument method — so the shipped IntelliSense sample did not compile. The pair is now a worked example that mirrors the actual call sites (GetRealMemberCount(limitedMemberCount, count)thenGetRealPageCount(realMemberCount, size)), and the "null means unlimited" contract is stated where a reader meets it.
-
An out-of-range argument is now reported as
ArgumentOutOfRangeExceptionby every paging entry point, replacing theIndexOutOfRangeExceptionthat theGetPagefamily and the keyset (GetFirstPageByKeyset/GetPageByKeyset) family used to throw. The change covers the coreIEnumerable<T>/IQueryable<T>/Task<IQueryable<T>>paths, both keyset extension classes including the EF Core async pair, and all nine ORM integration packages — 40 throw sites in 10 files. It closes a split the 6.1 fragment API opened:Paginable.CreatePage/fragment.ToPagealready answered an out-of-range argument withArgumentOutOfRangeExceptionand named the offending parameter, so the same mistake (pageNumber: 0) reported a different type depending on which entry point the caller happened to use.ArgumentOutOfRangeExceptionderives fromArgumentException, so callers catchingArgumentException(orException) are unaffected; a caller that specifically caught or asserted onIndexOutOfRangeExceptionmust be updated. Only the type changes — the rejection conditions are untouched (an empty source still yields a single empty page), and the exceptions now carryParamName. The XML<exception>docs declare the type on all 21 affected public members, and the 15 new tests inGetPageExceptionTypeTestpin the exact type and parameter name per path. -
MultiList<T>.Add(item, times)/MultiList<T>.Remove(item, times)and theirOrderedMultiList<T>counterparts now throwArgumentOutOfRangeExceptionwhentimesis less than or equal to zero, replacing the legacy behaviour that silently coerced a non-positive count to one copy. The coercion hid mistakes:Add(item, 0)reported success while inserting a copy nobody asked for, andRemove(item, 0)silently removed one. Zero copies is already expressible as a no-op by simply not calling, so any non-positive value is now treated as a programming error and rejected with the offending parameter named. The single-copyAdd(item)/Remove(item)overloads andAddRange(items)are unchanged. The XML<exception>docs declare the behaviour on all four affected members, and the tests pin the exception type on both classes.
Release covering both shipped modules (Paginable and Multi); every package ships version
6.1.0.0 (see build/version.props).
- Paging objects can now be created directly from a materialized fragment plus paging metadata:
Paginable.CreatePage(fragment, pageNumber, pageSize, totalMemberCount), thePageFragmentInfooverload, thePageFragmentInfo.FromMetadata/ToMetadataround trip, and theIEnumerable<T>.ToPage(...)sugar. The fragment is never re-sliced and member item numbers match full-source pagination exactly, so a page built from a Dapper / hand-written SQL result, a cached page or an upstream API response (items+totalCount) is indistinguishable from one sliced out of the full source. The metadata is validated eagerly:nullfragment, non-positive page numbers, an out-of-range page, a negative or oversized total count, and a fragment larger than the page it claims to be are all rejected at construction time. A fragment shorter than the metadata expects is tolerated andCurrentPageSizekeeps reporting the metadata value. Closes #8. MultiList<T>now implementsIEquatable<MultiList<T>>: multiset structural equality, where two multisets are equal when both hold the same distinct elements with the same number of copies in any order, plus aGetHashCodethat agrees with it (structurally equal multisets collapse into a singleHashSet<MultiList<T>>entry). Element matching goes through the comparer of each side, exactly like the existing subset and superset judgments, and copy counts take part, so this is bag equality rather than set equality.Equals(object)is overridden to match;==/!=are deliberately left as reference comparisons, and no otherMultitype gains equality.MultiDictionary<TKey, TValue>can now delete several values under a key in one call, and report how many it holds:RemoveRange(key, values)removes one occurrence per distinct argument value — the batch form ofRemove(key, value), so a value stored N times keeps N-1 copies (ExceptWith(key, values)stays the way to drop every occurrence) — andValueCount(key)returns the number of values stored under a key, or0for a missing key instead of throwing. Both follow the conventions the type already had: the argument is a set,nullelements are ordinary values, matching goes through the inner collection's own comparer, and the key is recycled as soon as its last value is removed. The batch form is a separate name rather than an overloadRemove(key, IEnumerable<TValue>)because that overload is a source-breaking change:map.Remove(key, null)would become ambiguous (CS0121), sincenullconverts to bothTValueandIEnumerable<TValue>;RemoveRangealso mirrors the existingAddRange(key, values).
6.0.0 - 2026-09-10
Modernization release covering both shipped modules (Paginable and Multi).
- Keyset (seek) pagination:
GetFirstPageByKeyset/GetPageByKeysetoverIQueryable<T>andIEnumerable<T>, with an optionaldescendingswitch. Each page is a singleWHERE key > @lastKey ORDER BY key LIMIT @sizequery — noOFFSETscan and noCOUNT(*)round trip. - True end-to-end asynchronous paging for EF Core, FreeSql and SqlSugar
(
CountAsync+ToListAsync, no synchronous database calls), withCancellationTokenpassthrough throughout the core and ORM integrations. Multimodule rewritten:MultiList<T>(multiset / bag) plus a completeMultiDictionary<TKey, TValue>(multimap) withAsLookup(), per-key value set operations and a configurable inner-collection factory.- XML documentation with
<example>and<exception>sections on the public API surface. - Symbol packages (
.snupkg) and SourceLink on every package. - Automated nuget.org publishing through GitHub Actions (
Releaseworkflow).
- Target frameworks expanded to 11 TFMs (
net451,net461,net47,net48,netstandard2.0,netstandard2.1,net6.0–net10.0), now including theMultipackage. Multitargetsnetstandard2.0,netstandard2.1andnet6.0upwards (previouslynetstandard2.0only).- Paginable EF Core integration:
net8.0consumes the EF Core 8.0.x assets andnet9.0the 9.0.x assets (both previously resolved to 9.0.x). PaginableSettingsvalues are validated on assignment;PaginableSettingsManagerswaps an immutable snapshot atomically.- Deterministic builds with locked dependencies (
packages.lock.json).
EnumerablePage: singleSkip/Takematerialization for non-IListsources, removing the O(skip²) cost of per-memberElementAtcalls.CurrentPageSizeinteger-division defect on exact-multiple last pages.MaxMemberItemsboundary is now an open interval (exactly 10,000,000 rows allowed).- Argument validation (
pageNumber >= 1,pageSize >= 1) enforced across the core and every ORM integration. - NHibernate
AllValuesand coreQueryEntryStatenow materialize lazily, once.
PublishToMyget.bat: the MyGet feed is no longer part of the release flow; local publishing is done withscripts/Publish.bat, CI publishing with theReleaseworkflow.
5.0.0 - 2023-11-03
- Target frameworks
net451,net461,netstandard2.1,net5.0. Multimodule introduced (MultiList,MultiDictionary,netstandard2.0).
3.2.0 - 2020-05-16
- Added
DotNetCore.Collections.Paginable.SqlKataintegration. - Split the FreeSql integration into
FreeSqlandFreeSql.DbContext.
2.1.4 - 2019-05-30
- Maintenance release of the 2.x line (Chloe, Dos.ORM, EF6, EF Core, FreeSql, NHibernate and SqlSugar integrations).
2.0.1 - 2019-02-16
- First stable 2.x release of the pagination extensions.
1.0.0-beta1 - 2017-08-03
- Initial preview of the pagination extensions.