Kanject.Core
3.14.0
Prefix Reserved
dotnet add package Kanject.Core --version 3.14.0
NuGet\Install-Package Kanject.Core -Version 3.14.0
<PackageReference Include="Kanject.Core" Version="3.14.0" />
<PackageVersion Include="Kanject.Core" Version="3.14.0" />
<PackageReference Include="Kanject.Core" />
paket add Kanject.Core --version 3.14.0
#r "nuget: Kanject.Core, 3.14.0"
#:package Kanject.Core@3.14.0
#addin nuget:?package=Kanject.Core&version=3.14.0
#tool nuget:?package=Kanject.Core&version=3.14.0
Kanject.Core
Kanject.Core is the foundation library of the Kanject .NET SDK. It bundles the building blocks most services end up writing for themselves: bounded parallel fan-out and drift-corrected recurring jobs driven by attributes, reflection-free enum metadata, timezone-safe date handling, a bounded concurrent dictionary, a JSON path resolver, a lightweight audit trail, and a set of everyday extension methods.
Install it in any application or library that wants those primitives. The attribute-driven features are implemented by Roslyn source generators in the companion package Kanject.Core.Annotations, which you add alongside this one.
Installation
dotnet add package Kanject.Core
Targets .NET 8, .NET 9 and .NET 10. The package references the ASP.NET Core shared framework (Microsoft.AspNetCore.App), because a few helpers extend ASP.NET Core hosting types.
Attribute-driven features run on the generator package. Kanject.Core depends on Kanject.Core.Annotations, so the attribute types and generators normally reach your project through it. Reference Kanject.Core.Annotations explicitly anyway, on the same version as Kanject.Core, so the generator version is pinned alongside the runtime:
<ItemGroup>
<PackageReference Include="Kanject.Core" />
<PackageReference Include="Kanject.Core.Annotations" PrivateAssets="all" />
</ItemGroup>
Generated by Kanject.Core.Annotations |
Plain runtime APIs |
|---|---|
[Parallel] generated overloads and I{Type}Parallel |
ParallelLoop called directly |
[Recurring] / [RecurringHosted] generated loops, services and registrations |
RecurringLoop called directly |
[QueryableEnum] on your own enums |
SystemTimezone lookups (its SystemTimezoneQuery ships precompiled) |
PrintInConsole interceptors and the MSBuild switches |
Plain PrintInConsole calls, Types, Json, Audit, Extensions, UtilityServices |
Compile-time diagnostics (KANPAR*, KANREC*, KAN001, ...) |
Native AOT / trimming compatible. The library is built with IsAotCompatible on every target framework. The one reflection-based member, CollectionExtensions.ToDataTable, is marked [RequiresUnreferencedCode]. Under Native AOT, PrintInConsole prints objects with ToString() instead of reflection-based JSON.
Quick start
// OrderStatus.cs
using Kanject.Core.Annotations.Attributes.QueryableEnums;
namespace Shop;
[QueryableEnum]
public enum OrderStatus { Pending, InTransit, Delivered }
// Program.cs
using Kanject.Core.Timezone.Enums;
using Kanject.Core.Timezone.Extensions;
using Shop;
var status = OrderStatus.InTransit;
Console.WriteLine(status.GetDisplayName()); // "In Transit": generated at compile time, no reflection
Console.WriteLine(OrderStatusQuery.Count); // 3
Console.WriteLine(SystemTimezone.AfricaLagos.TodayIn()); // today's date on the wall clock in Lagos
This example uses [QueryableEnum], so it assumes the two-package setup from Installation. Declare [QueryableEnum] enums inside a namespace; the generator places its output in the enum's namespace.
What's inside
| Feature | What it gives you | Key types |
|---|---|---|
| Parallel | [Parallel] generates partitioned, bounded, retrying fan-out overloads for a method; the engine can also be called directly |
ParallelLoop, ParallelLoopOptions, ParallelResult, ParallelItemResult<TInput, TResult> |
| Recurring | [Recurring] generates a drift-corrected, non-reentrant schedule loop for a method; [RecurringHosted] also generates a BackgroundService |
RecurringLoop, RecurringOptions, RecurringResult |
| QueryableEnum | [QueryableEnum] generates name/description lookups, parsing and compile-time constants for an enum |
generated {Enum}Query, {Enum}Info |
| SystemConsole | PrintInConsole() for strings and objects, with opt-in interceptors that route calls to ILogger |
ConsoleExtensions, ConsolePrintInterceptorProvider, ConsolePrintLogLevel |
| Timezone | DST-aware "now", period boundaries, conversions and formatting per zone | SystemTimezone, SystemTimezoneExtensions, TimezoneSnapshot |
| Types | A fixed-capacity, thread-safe dictionary that evicts the oldest insert | CircularBufferDictionary<TKey, TValue> |
| Json | Dot-path lookups into JsonElement with a bounded compiled-path cache |
JsonFieldResolver |
| Audit | A self-pruning "who did what, when" log on any entity | IAuditEntity, AuditDetailsModel, AuditAction |
| Extensions | String, collection, date, array and hosting-environment helpers | StringExtensions, CollectionExtensions, DateExtensions, EnvironmentExtensions |
| UtilityServices | Secure random strings, Base64 helpers, a static host accessor | RandomGenerator, StringEncoding, AppUtilService |
| Common | A shared status enum | ConfigurationStatus (InActive, Active, Deleted) |
Parallel fan-out
Put [Parallel] on a method that takes one item (T) or a chunk (for example ReadOnlyMemory<T>), plus an optional CancellationToken. The generator adds extension methods that split a source collection into partitions, run them concurrently up to MaxDegreeOfParallelism, and aggregate the outcome.
using Kanject.Core.Annotations.Attributes.Parallel;
using Kanject.Core.Annotations.Attributes.Parallel.Enums;
using Kanject.Core.Parallel.Models;
public sealed partial class OrderProcessor(IOrderClient client, IOrderStore store)
{
// Per-item: the body receives one Order at a time.
[Parallel(MaxDegreeOfParallelism = 8, RetryCount = 1, RetryBackoffMs = 250)]
public ValueTask ProcessAsync(Order order, CancellationToken ct) => client.ProcessAsync(order, ct);
// Chunk: the body receives a slice of the input, which suits batched writes.
[Parallel(ChunkingMode = ParallelChunkingMode.FixedChunkSize, ChunkSize = 25, StopOnException = false)]
public ValueTask<int> SaveAsync(ReadOnlyMemory<Order> chunk, CancellationToken ct) => store.InsertAsync(chunk, ct);
}
// Generated overloads. A trailing "Async" is dropped before the suffix is added.
ParallelResult processed = await processor.ProcessParallelAsync(orders, cancellationToken: ct);
ParallelResult<int> saved = await processor.SaveParallelAsync(orders, cancellationToken: ct);
Console.WriteLine($"{processed.ItemCount} items, {processed.FailedCount} failed partitions, {processed.StopReason}");
What gets generated for each annotated method:
{Method}ParallelAsync: the aggregated result, overReadOnlyMemory<T>,T[],IReadOnlyList<T>,IEnumerable<T>andIAsyncEnumerable<T>sources.{Method}ParallelStream: anIAsyncEnumerable<ParallelChunkOutcome<TResult>>that yields partitions as they finish (chunk methods that return a value).{Method}ParallelOutcomesAsync: aParallelItemResult<TInput, TResult>whoseOutcomeslist records each item'sValue,Exception,TimedOut,CancelledandRetryCountin input order (per-item methods that return a value). Use it when you need to acknowledge part of a batch.- An
I{Type}Parallelinterface that the containing type implements, so you can register and mock the parallel surface through DI.
Attribute values are defaults. Pass options: ParallelLoopOptions.Default with { ... } at the call site to override them. For instance methods, the containing type must be a public partial class: the generator adds a partial declaration that implements I{Type}Parallel.
The engine can also be used without the attribute:
using Kanject.Core.Parallel.Engine;
using Kanject.Core.Parallel.Enums;
using Kanject.Core.Parallel.Models;
var result = await ParallelLoop.ForEachChunkAsync<Order, int>(
orders,
static (chunk, partitionIndex, ct) => Db.InsertAsync(chunk, ct),
ParallelLoopOptions.Default with
{
ChunkingStrategy = ParallelChunkingStrategy.FixedChunkSize,
ChunkSize = 25,
MaxDegreeOfParallelism = 4,
ChunkTimeout = TimeSpan.FromSeconds(10),
ExceptionPolicy = ParallelExceptionPolicy.ContinueAndCollect,
},
ct);
ParallelHookProvider.OnPartition, OnException and OnStop are static hooks for process-wide telemetry. [ParallelHosted] is checked by the analyzer, but this release generates no hosting code for it; to run a fan-out in the background, call {Method}ParallelAsync from your own BackgroundService.
Recurring schedules
[Recurring(rateValue, rateUnit)] generates extension methods that call the method on a fixed cadence until a stop condition is met: max iterations, max duration, cancellation, or the exception policy. Ticks are scheduled from a single start timestamp, so a slow iteration does not push later ticks back.
using Kanject.Core.Annotations.Attributes.Recurring;
using Kanject.Core.Annotations.Attributes.Recurring.Enums;
using Kanject.Core.Recurring.Enums;
using Kanject.Core.Recurring.Models;
public sealed partial class PriceFeed(IExchangeClient exchange)
{
[Recurring(5, RecurringRateUnit.Seconds, MaxIterations = 60, MaxJitterMs = 250)]
public ValueTask<decimal> FetchPriceAsync(string symbol, CancellationToken ct) => exchange.GetPriceAsync(symbol, ct);
}
// Generated names use the method name as written: FetchPriceAsync becomes FetchPriceAsyncRecurringAsync.
RecurringResult<decimal> run = await feed.FetchPriceAsyncRecurringAsync("BTC", cancellationToken: ct);
Console.WriteLine($"{run.Iterations} ticks, last = {run.LastValue}, stopped: {run.StopReason}");
// Override attribute defaults per call. Fields you leave at their default fall back to the attribute.
await foreach (var price in feed.FetchPriceAsyncRecurringStream(
"BTC",
options: RecurringOptions.Default with { MaxDuration = TimeSpan.FromMinutes(5), OverrunPolicy = RecurringOverrunPolicy.CatchUp },
cancellationToken: ct))
{
Console.WriteLine(price);
}
A method that returns no value gets {Method}RecurringAsync, which returns a RecurringResult. A method that returns a value also gets {Method}RecurringStream and {Method}RecurringToLastAsync. Overrun policies are Drop (the default), CatchUp and Stop. Exception policies are StopOnException (the default) and ContinueOnException. Global hooks are on RecurringHookProvider. As with [Parallel], instance methods need a public partial class, because the generator also emits an I{Type}Recurring interface.
For long-running hosts (ASP.NET Core, Worker Services), add [RecurringHosted] to generate a BackgroundService and a registration method. The hosted method must take no parameters or a single CancellationToken. Hosted services are not suitable for AWS Lambda; call {Method}RecurringAsync from the handler instead.
public sealed partial class WeatherPoller(IWeatherClient client)
{
[Recurring(30, RecurringRateUnit.Seconds)]
[RecurringHosted(Name = "WeatherPolling", Lifetime = RecurringServiceLifetime.Scoped)]
public ValueTask PollAsync(CancellationToken ct) => client.FetchAsync(ct);
}
// Overloads: (), (Action<RecurringOptions>), (IConfiguration).
builder.Services.AddWeatherPollerPollAsyncRecurring(builder.Configuration.GetSection("Recurring:WeatherPolling"));
By default every replica runs its own loop. Setting LeaseKey makes each iteration acquire a lease first. The lease is released afterwards, unless the iteration outlived it. Setting CheckpointKey saves a checkpoint after each successful iteration. Each save continues the stored checkpoint: its iteration count survives restarts, and its OpaqueStateJson is kept. The checkpoint isn't passed to your method; to resume, read it through IRecurringDataProvider. Both keys need an IRecurringDataProvider registered in DI (see Related packages).
Queryable enums
[QueryableEnum] generates a static {Enum}Query class and a {Enum}Info record struct in the enum's namespace. Names, [Description] text and [EnumParameter] values are captured at compile time, so lookups use no runtime reflection.
using System.ComponentModel;
using Kanject.Core.Annotations.Attributes.QueryableEnums;
namespace Shop;
[QueryableEnum(ConstantParameters = ["Wire"])]
public enum DeploymentStatus
{
[EnumParameter("Wire", "stable"), Description("Healthy, promotable revision.")]
Stable = 0,
[EnumParameter("Wire", "broken"), Description("Known-bad revision.")]
Broken = 1,
}
string text = DeploymentStatus.Stable.GetDescription(); // "Healthy, promotable revision."
string wire = DeploymentStatus.Broken.GetWire(); // "broken"
DeploymentStatusInfo? info = DeploymentStatusQuery.FindByName("stable"); // case-insensitive
// ConstantParameters produces real consts (class name = pluralised parameter name):
string raw = "stable";
var status = raw switch
{
DeploymentStatusQuery.Wires.Stable => DeploymentStatus.Stable,
DeploymentStatusQuery.Wires.Broken => DeploymentStatus.Broken,
_ => throw new ArgumentException($"Unknown status '{raw}'."),
};
The generated surface also includes GetName, GetDisplayName ("InTransit" becomes "In Transit"), GetInfo, IsDefined, GetAllValues, GetEnumValues, GetAllNames, SearchByName, TryParseByName, Count, and TryConvertTo{Enum} for string and for the underlying integer type. [QueryableEnum("Name")] changes the generated class prefix to NameQuery.
Console output
using Kanject.Core.SystemConsole;
using Kanject.Core.SystemConsole.Extensions;
"Processing payment".PrintInConsole(tag: "Billing"); // wrapped in START/END tag markers
order.PrintInConsole(); // objects are printed as indented JSON
"Pool exhausted".PrintInConsole(ConsolePrintLogLevel.Critical);
To send these calls to ILogger without editing any call site, reference Kanject.Core.Annotations (see Installation), set <EnablePrintInConsoleInterceptor>true</EnablePrintInConsoleInterceptor> in your project, and call services.AddPrintInConsoleLogging() (it registers a hosted service that installs the handler). The generator then emits C# interceptors for every PrintInConsole call. Each interceptor carries a severity inferred at compile time: calls inside a catch block, calls on an Exception, and tags or literals containing words like "error", "warning" or "debug" get the matching level. Use an overload that takes a ConsolePrintLogLevel to set the level yourself. To use a custom handler instead, assign ConsolePrintInterceptorProvider.Handler and ActionHandler.
Timezones
SystemTimezone is an enum of common zones, each mapped to both its IANA and its Windows ID, so you don't need per-platform fallback code. The extension methods cache the resolved TimeZoneInfo.
using Kanject.Core.Timezone.Enums;
using Kanject.Core.Timezone.Extensions;
var ny = SystemTimezone.AmericaNewYork;
DateOnly today = ny.TodayIn();
DateTimeOffset start = ny.TodayStartIn(); // also TodayEndIn, StartOfWeekIn, StartOfMonthIn, StartOfYearIn
DateTimeOffset meet = ny.At(2026, 6, 15, 14, 30);
DateTimeOffset gap = ny.At(2026, 3, 8, 2, 30, 0, out bool wasAdjusted); // 03:30 -04:00; true: 02:30 falls in the DST gap
DateTime local = DateTime.UtcNow.FromUtcTo(ny); // also ToUtc, ConvertTo, AsDateTimeOffsetIn
TimezoneSnapshot snap = ny.Snapshot(); // Now/Today/TodayStart/TodayEnd from a single clock read
Every "now" method reads the clock through a TimeProvider. In tests, SystemTimezoneExtensions.SetTimeProvider(fakeTimeProvider) replaces it for the current async flow only, and ResetTimeProvider() restores the system clock. "Europe/London".ToSystemTimezoneOrDefault() maps an IANA or Windows ID back to the enum, and falls back to Utc when the ID is unknown.
Bounded concurrent dictionary
CircularBufferDictionary<TKey, TValue> keeps at most Capacity entries. A new key evicts the oldest-inserted key. Updating an existing key replaces the value without changing its eviction position. Reads go straight to a ConcurrentDictionary, so they take no lock and allocate nothing.
using Kanject.Core.Types;
var seen = new CircularBufferDictionary<string, long>(capacity: 65_536); // a power of two avoids a modulo
if (!seen.ContainsKey(messageId))
seen.AddOrUpdate(messageId, receivedAt);
foreach (var (key, value) in seen.Snapshot()) { /* consistent copy */ }
This is FIFO eviction, not LRU, and there is no TTL, Remove or eviction callback. Under concurrent inserts, Count can briefly exceed Capacity; it settles once the inserts finish. Enumerate() gives a lazy, best-effort view without allocating, and Clear() is meant for quiet periods only.
JSON field resolution
JsonFieldResolver resolves dot-notated paths such as payload.address.city or items.0.productId against a System.Text.Json.JsonElement. Paths resolved from configuration at runtime are compiled once and cached.
using System.Text.Json;
using Kanject.Core.Json;
using var doc = JsonDocument.Parse(body);
string? city = JsonFieldResolver.ResolveFieldValue(doc.RootElement, "payload.address.city");
if (JsonFieldResolver.TryResolveDouble(doc.RootElement, "items.0.price", out var price)) { /* ... */ }
ResolveFieldValueDeep also retries the path under each top-level child object, for payloads wrapped in an envelope. There are also typed TryResolveString/Int64/Boolean/Element variants, ResolveMany, and Preload. The compiled-path cache holds 2,048 paths by default; set the KANJECT_JSON_FIELD_RESOLVER_MAX_CACHED_PATHS environment variable before the process starts to change that. Paths beyond the cap still resolve correctly; they just aren't cached.
Audit trail
using Kanject.Core.Audit.Enums;
using Kanject.Core.Audit.Extensions;
using Kanject.Core.Audit.Interfaces;
using Kanject.Core.Audit.Models;
public sealed class Invoice : IAuditEntity
{
public List<AuditDetailsModel>? AuditLog { get; set; }
}
invoice.AuditUserUpdateAction(userId); // also Create/Read/Delete; stamps DateTime.UtcNow
AuditDetailsModel? last = invoice.GetMostRecentAudit(AuditAction.Update);
The log keeps the 20 most recent entries: each write evicts the oldest by Timestamp, so it can be embedded in a document-database item without growing past that limit. Call LogUserAction to append an entry you built yourself, for example one with a historical timestamp.
Extension methods
All in Kanject.Core.Extensions:
| Class | Members |
|---|---|
StringExtensions |
IsValidEmail, IsValidPhoneNumber, IsValidNumber, IsValidJson, IsValidDateTime, IsImageExtension, ToTitleCase, ToCamelCase, ToPascalCase, RemovePreFix, RemovePostFix, Left, Right, MaskDigit, OrUsePlaceholder, ToObjectDictionary, CalculateLevenshteinDistance (static) |
CollectionExtensions |
IsNullOrEmpty, ToPaginatedList (1-based page index), ToDataTable |
DateExtensions |
GetEndOfDay, GetHoursToEndOfDay, GetFirstDateOfMonth, GetLastDateOfMonth, ConvertToDate, ParseToDateTime, ToInvariantDateTime, ToDateString |
ArrayExtensions |
ToNullable for value-type arrays |
EnvironmentExtensions |
IsUat, IsQa, and AddSubEnvironment / ConfigureSubEnvironment |
The validators are lightweight format checks, not full RFC validation. AddSubEnvironment (on WebApplicationBuilder or IConfigurationBuilder) and ConfigureSubEnvironment (on IWebHostBuilder) apply only in the Development environment: when the ASPNETCORE_SUBENVIRONMENT variable is set, they layer appsettings.Development.{value}.json on top of the usual settings if that file exists. This lets each developer keep local settings without overwriting a shared file.
Utility services
using Kanject.Core.UtilityServices;
string token = RandomGenerator.GenerateRandom(32); // cryptographically secure, A-Z a-z 0-9
string otp = RandomGenerator.GenerateRandomNumber(6);
string pwd = RandomGenerator.GenerateRandomText(length: 16, shouldHaveSpecialCharacter: false);
string b64 = StringEncoding.EncodeString("hello"); // ASCII Base64; DecodeString reverses it
Passing a seed switches the random helpers to a seeded, non-cryptographic Random so tests are repeatable; use seeds only in tests. app.UseAppUtilityService(env) (in Kanject.Core.UtilityServices.Extensions) registers the IApplicationBuilder and optional IWebHostEnvironment behind the static AppUtilService accessors. Reading them before registration throws AppBuilderServiceNotRegisteredException. It is an escape hatch for static contexts that cannot use constructor injection.
MSBuild switches
These properties are defined by Kanject.Core.Annotations and take effect when that package is referenced.
| Property | Default | Effect |
|---|---|---|
EnablePrintInConsoleInterceptor |
off | Generates the PrintInConsole interceptors and enables the interceptor namespace |
EnableRecurringGenerator |
on | Set false to stop generating [Recurring] extensions |
EnableRecurringHostedGenerator |
on | Set false to stop generating [RecurringHosted] services and registrations |
Compile-time diagnostics from the generators (KANPAR*, KANREC*, KAN001/KAN002, KANJECT0021-KANJECT0024, KANPIC001) are documented in the Kanject.Core.Annotations package.
Related packages
| Package | Role | Availability |
|---|---|---|
Kanject.Core.Annotations |
Source generators and analyzers behind the attribute-driven features; reference it alongside this package | nuget.org |
Kanject.Core.Annotations.Attributes |
The [Parallel], [Recurring], [QueryableEnum] attribute types; a dependency of Kanject.Core.Annotations |
nuget.org |
Kanject.Core.Recurring.Abstractions |
IRecurringDataProvider, used by [RecurringHosted] leases and checkpoints |
nuget.org |
Kanject.Core.Recurring.Provider.CacheDb |
IRecurringDataProvider implementation backed by ICacheDb |
nuget.org |
Kanject.Core.Queue.Provider.AwsSqs.Abstractions |
AcknowledgeOrFailAsync: turns a ParallelItemResult into an SQS partial-batch response |
nuget.org |
License
Licensed under the Kanject Code Libraries License Agreement (KCLLA); the full text ships in this package as LICENSE.md. Organizations whose trailing-twelve-month gross revenue and total funding raised are each below US$250,000 may use it at no cost under the Free Tier. At or above either threshold a commercial license is required — contact commercial@kanjectbusiness.com.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Kanject.Core.Annotations (>= 3.14.0)
-
net8.0
- Kanject.Core.Annotations (>= 3.14.0)
-
net9.0
- Kanject.Core.Annotations (>= 3.14.0)
NuGet packages (7)
Showing the top 5 NuGet packages that depend on Kanject.Core:
| Package | Downloads |
|---|---|
|
Kanject.Core.Api.Abstractions
Kanject core api abstractions |
|
|
Kanject.Core.CacheDb.Abstractions
Kanject core cache db abstractions |
|
|
Kanject.Core.Queue.Abstractions
Kanject Core Queue Abstractions |
|
|
Kanject.Core.CloudFunction.Abstractions
Kanject core cloud function abstractions |
|
|
Kanject.Core.NoSqlDatabase.Abstractions
Kanject Core NoSql Database specification pattern |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.14.0 | 64 | 10/2/2026 |
| 3.13.1 | 411 | 9/27/2026 |
| 3.13.0 | 354 | 9/27/2026 |
| 3.12.7 | 355 | 9/26/2026 |
| 3.12.6 | 405 | 9/7/2026 |
| 3.12.5 | 366 | 8/27/2026 |
| 3.12.4 | 457 | 8/22/2026 |
| 3.12.3 | 389 | 8/10/2026 |
| 3.12.2 | 387 | 8/9/2026 |
| 3.12.1 | 419 | 8/5/2026 |
| 3.12.0 | 419 | 8/5/2026 |
| 3.11.0 | 449 | 8/3/2026 |
| 3.10.6 | 445 | 7/30/2026 |
| 3.10.5 | 475 | 7/18/2026 |
| 3.10.4 | 203 | 7/13/2026 |
| 3.10.3 | 242 | 7/11/2026 |
| 3.10.2 | 435 | 7/11/2026 |
| 3.10.1 | 1,111 | 7/9/2026 |
| 3.10.0 | 487 | 7/9/2026 |