Kanject.Core.Packages 3.10.1

Prefix Reserved
There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Kanject.Core.Packages" Version="3.10.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Kanject.Core.Packages" Version="3.10.1" />
                    
Directory.Packages.props
<PackageReference Include="Kanject.Core.Packages" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Kanject.Core.Packages --version 3.10.1
                    
#r "nuget: Kanject.Core.Packages, 3.10.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Kanject.Core.Packages@3.10.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Kanject.Core.Packages&version=3.10.1
                    
Install as a Cake Addin
#tool nuget:?package=Kanject.Core.Packages&version=3.10.1
                    
Install as a Cake Tool

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:

  1. Hand-written switch statements scattered across services.
  2. A single giant factory that knows every provider in the application.
  3. Direct IServiceProvider lookups with string keys at business call sites.
  4. Mutable strategy objects where every method must remember to pick a provider first.
  5. 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.Annotations version that emits contributors. Consumers on an older generator get an empty (always- valid) report until they upgrade.

Gotchas

  1. IPackageScope is a routing context, not the provider registry. Concrete providers still live in keyed DI.
  2. Scope keys are resolver keys, not always provider ids. If [PackageResolverKey("stripe")] is used, the key is "stripe".
  3. The contract on IPackageScope is a single Type-based method (PackageScopeSelection? For(Type)). The typed For<T> / KeyFor<T> / VersionFor<T> forms are extension methods over it. Custom implementations only need to implement the one method.
  4. Custom resolvers used with IPackageScope must implement the sync key path. Pre-compute async selection at scope-construction time (see the Async Selection section above).
  5. Register IPackageScope as scoped for request/order/command-specific routing. For app-wide static overrides built via PackageScopeBuilder, singleton works too.
  6. Avoid transient IPackageScope; a new instance per resolution breaks the unit-of-work invariant.
  7. Package managers are best for explicit dispatch. Direct package injection is best for ambient workflow dispatch.
  8. Defaults should live in GetDefaultPackage(), not in every scope builder call.
  9. default and latest are different. Default = [DefaultPackageProvider]-marked provider; latest = highest registered version for a key. They diverge once you have multi-version providers.
  10. 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.
  11. Property members on port interfaces are emitted as properties on the manager (read-only as => Package.Foo, read-write as get/set delegation). Indexers are not supported and need a custom manager.
  12. PackageScopeBuilder is for static overrides known at scope-construction time. For per-context routing, implement IPackageScope directly — Order-style scopes with field-derived keys are the canonical custom-impl pattern.
  13. 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 injecting IXxxPackageManager for explicit dispatch. The diagnostic itself doesn't flip between the two cases; that's on the reader to confirm.
  14. The DI graph is validated at runtime, not compile time. What KANPKG002 and the other analyzers can't see — missing registration, unresolved manifest keys, a forgotten provider assembly — is exactly what IServiceProvider.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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