Kanject.Core.Packages
3.10.1
Prefix Reserved
See the version list below for details.
dotnet add package Kanject.Core.Packages --version 3.10.1
NuGet\Install-Package Kanject.Core.Packages -Version 3.10.1
<PackageReference Include="Kanject.Core.Packages" Version="3.10.1" />
<PackageVersion Include="Kanject.Core.Packages" Version="3.10.1" />
<PackageReference Include="Kanject.Core.Packages" />
paket add Kanject.Core.Packages --version 3.10.1
#r "nuget: Kanject.Core.Packages, 3.10.1"
#:package Kanject.Core.Packages@3.10.1
#addin nuget:?package=Kanject.Core.Packages&version=3.10.1
#tool nuget:?package=Kanject.Core.Packages&version=3.10.1
Kanject Package Manager
Attribute-driven provider dispatch for ports that can have multiple concrete implementations. Declare a package interface, tag one or more providers, and let the source generator emit resolver, manager, keyed DI, metadata, and optional ambient-scope dispatch.
The package manager feature is the Kanject answer to the strategy-pattern problem:
IPaymentGateway -> Stripe or Paystack
IOrderShipping -> Evri or Voila
ICloudPlatform -> AWS or Azure
IMigrationApplier -> DSQL or DynamoDB
The key principle is:
Package interface defines the port.
Package provider implements the port.
Resolver maps a key to a provider id/version.
Keyed DI stores concrete providers.
Package manager gives an explicit dispatch API.
IPackageScope gives an ambient dispatch API for a unit of work.
The Problem
Multi-provider code tends to degrade into one of these shapes:
- Hand-written
switchstatements scattered across services. - A single giant factory that knows every provider in the application.
- Direct
IServiceProviderlookups with string keys at business call sites. - Mutable strategy objects where every method must remember to pick a provider first.
- Host-specific plugin glue that cannot be reused by libraries.
Kanject packages keep the provider registry source-generated and AOT-friendly while leaving the business-specific provider choice in application code.
Core Terms
| Term | Meaning |
|---|---|
| Package interface | The port, marked with [Package(id: "...")]. Example: IPaymentGateway. |
| Package provider | A concrete implementation, marked with [Package<TPackage>] and [PackageProvider(id, version)]. |
| Resolver key | A short dispatch key such as "stripe", "paystack", "aws", or "dsql". |
| Resolver | The generated or custom IXxxResolver that maps a key to (packageId, version). |
| Package manager | The generated IXxxPackageManager wrapper that delegates port methods to the active provider. |
| Keyed DI | The underlying MEDI registration: AddKeyedScoped<TPackage, TProvider>(key). |
| Package scope | An optional ambient IPackageScope that supplies the active PackageScopeSelection (resolver key + optional version pin) per package interface in a MEDI scope. |
| Package scope selection | A readonly record struct returned by IPackageScope.For(Type) carrying the resolver Key and an optional Version pin. Allocation-free on the dispatch hot path. |
| Package scope builder | Fluent helper for building a static scope from typed Use<TPackage>(key) / Use<TPackage>(key, version) calls. Use it for app-wide overrides; implement IPackageScope directly for per-request scopes. |
MEDI means Microsoft.Extensions.DependencyInjection.
Basic Usage
1. Define the package interface
using Kanject.Core.Annotations.Attributes.Packages;
[Package(id: "payment.gateway")]
[PackageDescription("Process order payments through third-party gateways")]
public interface IPaymentGateway
{
Task<PurchaseOrderResponse> PurchaseOrderAsync(
PurchaseOrderRequest request,
Guid userId);
Task<HandleBuyerRefundResponse> HandleBuyerRefundAsync(
HandleBuyerRefundRequest request);
}
This tells the generator to create the package surface for payment.gateway.
2. Define providers
[Package<IPaymentGateway>]
[PackageProvider(id: "stripe.payment.gateway", version: 1)]
[PackageProviderOwner("Stripe")]
[PackageDescription("Stripe-backed payment gateway")]
[PackageResolverKey("stripe")]
[DefaultPackageProvider]
public sealed partial class StripePaymentGateway : IPaymentGateway
{
// implementation
}
[Package<IPaymentGateway>]
[PackageProvider(id: "paystack.payment.gateway", version: 1)]
[PackageProviderOwner("Paystack")]
[PackageDescription("Paystack-backed payment gateway")]
[PackageResolverKey("paystack")]
public sealed partial class PaystackPaymentGateway : IPaymentGateway
{
// implementation
}
Provider classes should be partial so generated metadata properties can be added.
[DefaultPackageProvider] marks the fallback provider for GetDefaultPackage(). There should be exactly one default when the package needs default dispatch.
3. Register generated services
services.AddPaymentGatewayPackages();
The provider-side extension registers:
keyed scoped providers
package manager
auto key resolver, when resolver keys/defaults are present
scope-aware bare-port dispatch
What Gets Generated
For a package interface such as IPaymentGateway, the generator emits:
| Generated artifact | Purpose |
|---|---|
IPaymentGatewayPackageManager |
Explicit dispatch wrapper that also exposes the package methods. |
PaymentGatewayPackageManager |
Runtime manager implementation. |
PaymentGatewayPackageManager<TPackage> |
Typed manager variant. |
IPaymentGatewayResolver |
Resolver contract. |
AbstractPaymentGatewayResolver |
Base class for custom resolvers. |
PaymentGatewayKeyResolver |
Auto resolver when [PackageResolverKey] or [DefaultPackageProvider] is present. |
AddPaymentGatewayResolver<TResolver>() |
DI hook for replacing the resolver. |
AddPaymentGatewayPackages() |
Main provider registration entry point. |
AddPaymentGatewayScopedDispatch() |
Direct IPaymentGateway registration driven by IPackageScope. |
GetStripePaymentGatewayV1Package() |
Explicit service-provider lookup for one provider/version. |
UseStripePaymentGatewayV1Package() |
Explicit manager switch to one provider/version. |
| metadata partials | Provider id, name, owner/version metadata. |
Keyed DI Shape
Concrete providers are registered as keyed scoped services. Conceptually:
services.AddKeyedScoped<IPaymentGateway, StripePaymentGateway>(
"payment.gateway::stripe.payment.gateway::1");
services.AddKeyedScoped<IPaymentGateway, PaystackPaymentGateway>(
"payment.gateway::paystack.payment.gateway::1");
The generated resolver returns:
(packageId: "stripe.payment.gateway", version: 1)
The generated lookup then composes the keyed DI key:
payment.gateway::stripe.payment.gateway::1
This means the concrete provider registry remains explicit, scoped, and reflection-free.
Resolver Strategy
Resolvers map a dispatch key to a package id/version.
The auto resolver is generated from [PackageResolverKey]. Both the single-key and key+version overloads are supported:
resolver.ResolvePackageWith("stripe") // null version → latest registered stripe
// -> ("stripe.payment.gateway", 2) // (whatever's highest)
resolver.ResolvePackageWith("stripe", 1) // explicit pin
// -> ("stripe.payment.gateway", 1)
resolver.ResolvePackageWith("stripe", 99)
// throws KeyNotFoundException with the available versions listed.
If the package needs application-specific selection, replace the resolver:
public sealed class PaymentGatewayResolver(
CountrySetupRepository countrySetupRepository)
: AbstractPaymentGatewayResolver
{
public override (string packageId, int version) GetDefaultPackage()
=> this.UseStripePaymentGatewayV1Package();
public override async Task<(string packageId, int version)> ResolvePackageWithAsync(
Dictionary<string, string> args)
{
// Example: country id -> checkout provider from configuration.
// Return GetDefaultPackage() when no specific provider is configured.
}
public override (string packageId, int version) ResolvePackageWith(
string key,
int? version)
=> key switch
{
"stripe" when version is null or 1 => this.UseStripePaymentGatewayV1Package(),
"paystack" when version is null or 1 => this.UsePaystackPaymentGatewayV1Package(),
_ => throw new KeyNotFoundException(
$"No provider matches key '{key}' for {nameof(IPaymentGateway)}.")
};
}
Then register:
services.AddPaymentGatewayResolver<PaymentGatewayResolver>();
services.AddPaymentGatewayPackages();
Allocation Profile on the Dispatch Hot Path
The bridge runs once per bare-port resolution. The keyed-service key it passes to MEDI's GetRequiredKeyedService<TPort>(object key) is interned at registration time into a ConcurrentDictionary<(string packageId, int version), object> on the package manager class. The dispatch closure calls {PackageManager}.GetKeyedServiceKey(packageId, version) to fetch the cached reference — one stack-allocated tuple for the lookup, zero heap allocations per resolution.
Same intern applies to the manager's ResolvePackageWith(...) path and the GetXxxPackage(serviceProvider, ...) extension: each shares the cached object reference. Registering a package allocates one key string per (packageId, version) pair, then never again.
Practical implication: bare-port dispatch is safe to use on per-request hot paths (webapi / aspnetcore middleware) without burning allocations on every call.
Async Selection: Do the I/O at Scope Construction Time
The scoped-dispatch bridge calls ResolvePackageWith(key, version) synchronously — fine for sync resolvers, but it blocks await from inside the dispatch closure. This is by design, not a missing feature. MEDI's IServiceProvider factory cannot be async, and threading an async story through every port resolution would cost more in complexity than it buys back.
The recommended pattern for async-dependent selection (DB lookup, feature-flag service, remote config, etc.):
1. Do the async work at scope-construction time
2. Store the resolved key/version on a synchronous IPackageScope
3. Bridge reads the pre-computed key/version when the port is resolved
This is exactly what OrderPackageScopes.CreateAsync (below) does — load the order async, then build a sync scope from the result. The unit-of-work boundary owns the async work; the inner code stays clean.
When provider selection genuinely cannot be pre-computed (rare — usually means the unit-of-work boundary is wrong), use the manager's ResolvePackageWithAsync(args) instead of bare-port injection. Custom resolvers that only implement the async path remain valid for explicit manager calls; they simply don't participate in bare-port injection.
Explicit Dispatch With Package Managers
Package managers are useful when the call site intentionally chooses or switches provider.
public sealed class PaymentWebhookHandler(
IPaymentGatewayPackageManager paymentGateways)
{
public Task HandleStripeAsync(HttpRequest request)
=> paymentGateways
.UseStripePaymentGatewayV1Package()
.ProcessWebhookAsync(request);
public Task HandlePaystackAsync(HttpRequest request)
=> paymentGateways
.UsePaystackPaymentGatewayV1Package()
.ProcessWebhookAsync(request);
}
Or with resolver keys:
paymentGateways.ResolvePackageWith("paystack");
await paymentGateways.PurchaseOrderAsync(request, userId);
Use this for webhooks, admin tools, diagnostics, tests, and other explicit one-off dispatch flows.
Ambient Dispatch With IPackageScope
IPackageScope is for workflow context. A single MEDI scope can hold package choices for multiple package interfaces:
IPaymentGateway -> paystack
IOrderShipping -> evri
IOrderAddressing -> default
Build a scope with PackageScopeBuilder:
var packageScope = PackageScopeBuilder.Create()
.Use<IPaymentGateway>("paystack")
.Use<IOrderShipping>("evri")
.Build();
Version pinning is optional:
var packageScope = PackageScopeBuilder.Create()
.Use<IPaymentGateway>("stripe", version: 1)
.Build();
Register the scope in the MEDI scope that represents the unit of work:
services.AddScoped<IPackageScope>(_ => packageScope);
After that, direct package injection can work:
public sealed class CheckoutWorkflow(
IPaymentGateway paymentGateway,
IOrderShipping shipping)
{
public async Task RunAsync(Guid orderId, Guid userId)
{
await paymentGateway.PurchaseOrderAsync(
new PurchaseOrderRequest { OrderId = orderId },
userId);
await shipping.CreateShippingLabelAsync(
new CreateShippingLabelRequest(orderId));
}
}
The generated direct registration performs:
IPackageScope.For(typeof(TPackage)) // single Type-based lookup, returns PackageScopeSelection?
-> resolver.ResolvePackageWith(selection.Key, selection.Version)
-> GetRequiredKeyedService<TPackage>(composed keyed DI key)
The contract is intentionally Type-based: generator emit and runtime tooling already know the package interface they're resolving for, so a typeof(...) call is the most direct API. Typed call-site sugar — scope.For<TPackage>(), scope.KeyFor<TPackage>(), scope.VersionFor<TPackage>() — lives in PackageScopeExtensions and forwards to the Type-based method. Use whichever reads better at the call site; the underlying lookup is the same.
PackageScopeSelection is a readonly record struct with two fields: Key (the resolver key) and Version (an optional int? pin). Allocations on the resolution hot path are zero — the bridge unwraps via is { } selection pattern matching.
If no IPackageScope is registered, or For(typeof(TPackage)) returns null, the generator falls back to resolver.GetDefaultPackage(). Note: default and latest are distinct — default returns whichever provider carries [DefaultPackageProvider]; latest (the path taken when scope supplies a key with no version) returns the highest registered version for that key. These can be different providers in multi-version setups.
Bare-Port and Manager Resolve Consistently
Inside the same MEDI scope, bare-port injection (IPaymentGateway) and manager injection (IPaymentGatewayPackageManager.Package) resolve to the same concrete provider:
public sealed class CheckoutWorkflow(
IPaymentGateway directGateway,
IPaymentGatewayPackageManager managerGateway)
{
public Task RunAsync(...)
{
// Both directGateway and managerGateway.Package point at the same
// provider chosen by IPackageScope.For(typeof(IPaymentGateway)).
// Explicit `managerGateway.ResolvePackageWith(...)` still overrides
// per call when needed.
...
}
}
The manager's constructor reads the ambient scope on activation and initializes its active Package to match the bridge's selection. This eliminates the footgun where a service taking both injection styles could otherwise get two different providers.
Where PackageScopeBuilder Should Live
The builder belongs at the business boundary where provider choice is known.
For an order service, keep it behind an application-owned policy object:
public sealed class OrderPackageScopes(
OrderRepository orders,
CountrySetupRepository countrySetup,
ShippingProviderRepository shippingProviders)
{
public async Task<IPackageScope> CreateAsync(Guid orderId, CancellationToken ct)
{
var order = await orders.GetItemCollectionAsync(orderId)
?? throw new InvalidOperationException("Order not found.");
var builder = PackageScopeBuilder.Create();
var paymentKey = await ResolvePaymentKeyAsync(order.Order!.CountryId, ct);
if (paymentKey is not null)
builder.Use<IPaymentGateway>(paymentKey);
var shippingKey = await ResolveShippingKeyAsync(
order.OrderShipping?.ShippingProviderId,
ct);
if (shippingKey is not null)
builder.Use<IOrderShipping>(shippingKey);
return builder.Build();
}
}
Then the workflow boundary says:
var packageScope = await orderPackageScopes.CreateAsync(orderId, ct);
// Register packageScope in the MEDI scope used to resolve the workflow.
// The exact scope-creation helper is host-specific.
The rule is:
Builder records explicit overrides.
Resolvers own defaults.
Generated DI composes both.
Business workflows use ordinary constructor injection.
Manager vs IPackageScope
Both APIs stay useful.
Use package managers when provider selection is the action:
paymentGateways.ResolvePackageWith("stripe");
await paymentGateways.ProcessWebhookAsync(request);
Use IPackageScope when provider selection is context:
This order uses Paystack for payment and Evri for shipping.
Everything resolved inside this order workflow should follow that.
In practice:
| Scenario | Prefer |
|---|---|
Payment webhook route /stripe |
Package manager explicit override |
| CLI command with manifest provider | Package scope around command execution |
| Order lifecycle using payment + shipping + addressing | Package scope |
| Admin tool comparing providers | Package manager manual switching |
| Tests forcing one provider | Package scope or manager, depending on test shape |
Multiple Package Interfaces in One Project
One project can declare and consume many packages. A single IPackageScope is a map from package type to resolver key:
var scope = PackageScopeBuilder.Create()
.Use<IPaymentGateway>("paystack")
.Use<IOrderShipping>("evri")
.Build();
Each generated bridge reads only its own port type:
IPaymentGateway bridge reads scope.For(typeof(IPaymentGateway))
IOrderShipping bridge reads scope.For(typeof(IOrderShipping))
IOrderAddressing bridge reads scope.For(typeof(IOrderAddressing))
Ports omitted from the scope use their resolver defaults.
Defaults and Fallbacks
Defaults belong in resolvers, not in PackageScopeBuilder.
Good:
var scope = PackageScopeBuilder.Create()
.Use<IPaymentGateway>("paystack")
.Build();
If IOrderShipping should default to Voila, let IOrderShippingResolver.GetDefaultPackage() say that. Do not put "voila" into every scope just because it is the current default.
Version Selection
Providers are versioned:
[PackageProvider(id: "stripe.payment.gateway", version: 1)]
When a scope supplies only a key:
.Use<IPaymentGateway>("stripe")
the resolver should choose the latest registered version for "stripe".
When a scope supplies a key and version:
.Use<IPaymentGateway>("stripe", version: 1)
the resolver should choose exactly that version or throw a clear error.
Use version pinning for staged rollouts, backwards compatibility, or cohort-specific provider migrations.
Testing Patterns
Force a provider through scope:
var packageScope = PackageScopeBuilder.Create()
.Use<IPaymentGateway>("paystack")
.Build();
services.AddScoped<IPackageScope>(_ => packageScope);
using var scope = services.BuildServiceProvider().CreateScope();
var paymentGateway = scope.ServiceProvider.GetRequiredService<IPaymentGateway>();
Mock IPackageScope directly — single Type-based method makes this trivial without a framework:
public sealed class TestPackageScope(IDictionary<Type, PackageScopeSelection?> overrides) : IPackageScope
{
public PackageScopeSelection? For(Type packageType)
=> overrides.TryGetValue(packageType, out var selection) ? selection : null;
}
// In a test:
var scope = new TestPackageScope(new Dictionary<Type, PackageScopeSelection?>
{
[typeof(IPaymentGateway)] = new("stripe", version: 1),
[typeof(IOrderShipping)] = new("evri"),
});
services.AddScoped<IPackageScope>(_ => scope);
Override direct package injection entirely:
services.AddScoped<IPaymentGateway>(_ => new FakePaymentGateway());
services.AddPaymentGatewayPackages(); // generated TryAddScoped bridge will not replace the fake
The generated direct package bridge uses TryAddScoped<TPackage>, so consumer registrations win.
Startup Graph Validation
Analyzers catch static miswiring at compile time (KANPKG001–014).
They cannot see the DI graph — whether AddXxxPackages() was actually
called, whether a manifest-driven scope key resolves, whether a provider
assembly was forgotten. Those faults only surface at first dispatch.
IServiceProvider.ValidatePackageGraph() closes that gap. Each
AddXxxPackages() registers a generated, per-package
IPackageGraphContributor; the validator collects every registered
contributor (so the union validates the union — including cross-assembly
providers) and checks, against the live container:
- the resolver is registered (KANPKG050)
- every declared provider resolves from keyed DI (KANPKG051)
- the [DefaultPackageProvider] resolves (KANPKG052)
- any IPackageScope-routed key resolves (KANPKG053)
Discovery is "what's registered as IPackageGraphContributor" — no
reflection scanning, so it stays AOT-clean.
Fail fast at host startup:
var app = services.BuildServiceProvider();
app.ValidatePackageGraphOrThrow(); // throws PackageGraphValidationException on any error
Or inspect without throwing (e.g. from a doctor command):
var report = app.ValidatePackageGraph();
if (report.HasErrors)
Console.Error.WriteLine(report.Describe());
This is the startup-time catch for the manifest scenario — e.g.
manifest.aws.provider = "azure" with no Azure provider installed routes
through IPackageScope, and KANPKG053 names it before the first command
runs instead of mid-deploy. The check is opt-in: nothing calls it
automatically, so existing consumers are unaffected until they wire it in.
Rollout note: the contributor is generator-emitted, so the validator only sees packages built with a
Kanject.Core.Annotationsversion that emits contributors. Consumers on an older generator get an empty (always- valid) report until they upgrade.
Gotchas
IPackageScopeis a routing context, not the provider registry. Concrete providers still live in keyed DI.- Scope keys are resolver keys, not always provider ids. If
[PackageResolverKey("stripe")]is used, the key is"stripe". - The contract on
IPackageScopeis a single Type-based method (PackageScopeSelection? For(Type)). The typedFor<T>/KeyFor<T>/VersionFor<T>forms are extension methods over it. Custom implementations only need to implement the one method. - Custom resolvers used with
IPackageScopemust implement the sync key path. Pre-compute async selection at scope-construction time (see the Async Selection section above). - Register
IPackageScopeas scoped for request/order/command-specific routing. For app-wide static overrides built viaPackageScopeBuilder, singleton works too. - Avoid transient
IPackageScope; a new instance per resolution breaks the unit-of-work invariant. - Package managers are best for explicit dispatch. Direct package injection is best for ambient workflow dispatch.
- Defaults should live in
GetDefaultPackage(), not in every scope builder call. defaultandlatestare different. Default =[DefaultPackageProvider]-marked provider; latest = highest registered version for a key. They diverge once you have multi-version providers.- Aggregators fan out to one provider per
[PackageProviderId](latest version wins). Two[PackageProvider]s sharing an id but with different versions are deduped — prevents accidental double-calls in payment-like ports. - Property members on port interfaces are emitted as properties on the manager (read-only as
=> Package.Foo, read-write asget/setdelegation). Indexers are not supported and need a custom manager. PackageScopeBuilderis for static overrides known at scope-construction time. For per-context routing, implementIPackageScopedirectly —Order-style scopes with field-derived keys are the canonical custom-impl pattern.- KANPKG002 fires on every bare-port injection site, by design. The analyzer can't see your DI registration graph at compile time, so it can't distinguish "wired up via
AddXxxScopedDispatch()" from "unwired and will throw at first resolution". Read the warning as a checklist prompt — either (a) the consumer already registers the scope-aware bridge in its composition root (suppress per-site or per-project), or (b) switch to injectingIXxxPackageManagerfor explicit dispatch. The diagnostic itself doesn't flip between the two cases; that's on the reader to confirm. - The DI graph is validated at runtime, not compile time. What
KANPKG002and the other analyzers can't see — missing registration, unresolved manifest keys, a forgotten provider assembly — is exactly whatIServiceProvider.ValidatePackageGraph()checks (see Startup Graph Validation). It's opt-in and runs against the live container; call it at startup (ValidatePackageGraphOrThrow()) or from a diagnostics command.
Mental Model
Explicit path (manager-driven):
manager.ResolvePackageWith("paystack", version: null)
-> resolver maps (key, version) to (packageId, version)
-> manager resolves keyed provider
-> manager delegates method calls
Ambient path (scope-driven bare-port injection):
IPackageScope.For(typeof(IPaymentGateway)) returns (Key: "paystack", Version: null)
-> generated direct bridge asks resolver for (packageId, version)
-> bridge resolves keyed provider
-> normal DI injects IPaymentGateway
Convergence:
inside the SAME MEDI scope, both paths read the same IPackageScope on activation,
so IPaymentGateway (bare port) and IPaymentGatewayPackageManager.Package both
point at the same provider instance. The manager's explicit ResolvePackageWith(...)
call still overrides per-call when needed.
The two paths share the same resolver, keyed-provider registry, and (when a scope is registered) the same selection rule. That's the point: no parallel dispatch system, just a better DX layer over the existing one.
| 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
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
-
net9.0
- Microsoft.Extensions.DependencyInjection (>= 9.0.10)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.13.1 | 117 | 9/27/2026 |
| 3.13.0 | 86 | 9/27/2026 |
| 3.12.7 | 89 | 9/26/2026 |
| 3.12.6 | 128 | 9/7/2026 |
| 3.12.5 | 101 | 8/27/2026 |
| 3.12.4 | 107 | 8/22/2026 |
| 3.12.3 | 119 | 8/10/2026 |
| 3.12.2 | 111 | 8/9/2026 |
| 3.12.1 | 119 | 8/5/2026 |
| 3.12.0 | 121 | 8/5/2026 |
| 3.11.0 | 116 | 8/3/2026 |
| 3.10.5 | 126 | 7/30/2026 |
| 3.10.4 | 118 | 7/18/2026 |
| 3.10.3 | 125 | 7/13/2026 |
| 3.10.2 | 119 | 7/11/2026 |
| 3.10.1 | 116 | 7/11/2026 |
| 3.10.0 | 145 | 7/9/2026 |
| 3.9.2 | 113 | 7/9/2026 |