Kanject.Core.Channels
3.11.7
Prefix Reserved
See the version list below for details.
dotnet add package Kanject.Core.Channels --version 3.11.7
NuGet\Install-Package Kanject.Core.Channels -Version 3.11.7
<PackageReference Include="Kanject.Core.Channels" Version="3.11.7" />
<PackageVersion Include="Kanject.Core.Channels" Version="3.11.7" />
<PackageReference Include="Kanject.Core.Channels" />
paket add Kanject.Core.Channels --version 3.11.7
#r "nuget: Kanject.Core.Channels, 3.11.7"
#:package Kanject.Core.Channels@3.11.7
#addin nuget:?package=Kanject.Core.Channels&version=3.11.7
#tool nuget:?package=Kanject.Core.Channels&version=3.11.7
Kanject.Core.Channels
One handler signature, more than one hosting mode.
Kanject.Core.Channels is the runtime for Kanject channels: a bounded, batching drain loop over System.Threading.Channels with time-based flush, per-batch retry, restart-on-fault, deadline-aware cancellation, and projection of un-acknowledged message IDs for partial-batch-failure responses. Write a (ReadOnlyMemory<T> batch, CancellationToken ct) → ValueTask handler once and run it as a long-lived BackgroundService consumer or inside a single AWS Lambda invocation.
Pair it with the Kanject.Core.Channels.Annotations source generator: annotate a method with [Channel] and the typed writer, DI registration, and host-appropriate consumer are generated at compile time. The engine is also usable directly, without the generator.
Installation
dotnet add package Kanject.Core.Channels
To use the [Channel] attribute and the generated wiring, add the generator next to the runtime:
<ItemGroup>
<PackageReference Include="Kanject.Core.Channels" />
<PackageReference Include="Kanject.Core.Channels.Annotations" PrivateAssets="all" />
</ItemGroup>
dotnet add package Kanject.Core.Channels.Annotations writes an IncludeAssets line for that package that omits compile. Delete that line (keep PrivateAssets="all") — otherwise the attribute types from Kanject.Core.Channels.Annotations.Attributes can be stripped from compilation.
Kanject.Core.Channels already depends on Kanject.Core.Channels.Annotations.Attributes, so the [Channel] attribute and its enums are available as soon as the runtime is referenced; the generator package is what turns them into code.
Targets .NET 8, .NET 9 and .NET 10. The runtime is Native AOT / trimming compatible (no reflection in the drain loop).
Quick start: in-process consumer
using Kanject.Core.Channels.Annotations.Attributes;
namespace Shop.Orders;
public sealed record OrderPlaced(string OrderId, decimal Total);
public interface IOrderReadModel
{
Task UpsertAsync(IReadOnlyList<OrderPlaced> orders, CancellationToken ct);
}
// The containing type must be partial (KANCHN002). The handler is an ordinary method with a body.
public sealed partial class OrderProjector(IOrderReadModel readModel)
{
[Channel(Capacity = 5000, BatchSize = 25, FlushIntervalMs = 200)]
public async ValueTask ProjectAsync(ReadOnlyMemory<OrderPlaced> batch, CancellationToken ct)
{
await readModel.UpsertAsync(batch.ToArray(), ct);
}
}
Register it from the composition root with the generated extension:
using Shop.Orders;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IOrderReadModel, SqlOrderReadModel>();
builder.Services.AddOrderProjectorProjectAsyncChannel(); // generated
var app = builder.Build();
app.Run();
Produce into the channel through the generated writer interface:
public sealed class CheckoutService(IOrderProjectorProjectAsyncChannelWriter orders)
{
public ValueTask OnOrderPlacedAsync(OrderPlaced order, CancellationToken ct) =>
orders.WriteAsync(order, ct);
}
Items are dispatched to ProjectAsync in batches of up to 25; a partial batch is flushed after 200 ms. With the default Lifetime = ChannelLifetime.Scoped, each batch runs in a fresh DI scope with a freshly resolved OrderProjector.
The generated files fully qualify every type and DI extension call, so they compile without any using in your project.
What the generator emits
For a [Channel] method M on type T:
| Generated member | Shape | Emitted when |
|---|---|---|
| Writer interface | public interface IT{M}ChannelWriter : IChannelWriter<TItem> (internal when TItem is not public) |
Always |
| DI extension | IServiceCollection AddT{M}Channel(this IServiceCollection services, Action<ChannelOptions>? configure = null) and const string ServiceKey in T{M}ChannelServiceCollectionExtensions |
Always |
| Hosted consumer | internal sealed class T{M}ChannelService : ChannelHostedServiceBase<TItem> |
Resolved mode is InProcess |
| Drain helper | public static class T{M}ChannelDrain with DrainAsync(IServiceProvider serviceProvider, TimeSpan remainingTime, CancellationToken cancellationToken = default) |
Resolved mode is InvocationLocal |
All generated types go in the handler's namespace. The DI extension:
- Builds a
ChannelOptionsfrom the attribute values, then applies yourconfigurecallback.ChannelFullModemaps to theBoundedChannelFullModemember of the same name. - Creates the bounded channel with
ChannelDrainer.CreateChannel<TItem>(options). - Registers
ChannelOptions,Channel<TItem>,ChannelReader<TItem>andChannelWriter<TItem>as keyed singletons underT{M}ChannelServiceCollectionExtensions.ServiceKey(the handler's fully qualifiedNamespace.Type.Method), and the writer interface as a singleton bound to this channel. Nothing is registered unkeyed, so any number of channels — including several over the same item type — coexist in one container. - In
InProcessmode only, also registers the hosted consumer, built over this channel's reader and options, and the handler type (with the lifetime fromLifetime).
To use the raw channel types, resolve them by key:
var writer = provider.GetRequiredKeyedService<ChannelWriter<OrderPlaced>>(
OrderProjectorProjectAsyncChannelServiceCollectionExtensions.ServiceKey);
Each AddT{M}Channel may be called once per IServiceCollection; a second call throws InvalidOperationException.
Handler shape
- Return
TaskorValueTask. - The partition is the first parameter that isn't a
CancellationToken: the item (TItem) whenBatchSizeis 1, or a batch (ReadOnlyMemory<TItem>orIReadOnlyList<TItem>) whenBatchSize> 1. PreferReadOnlyMemory<TItem>: it is passed through without copying. Both generated consumers copy each batch into an array forIReadOnlyList<TItem>handlers. - A
CancellationTokenis optional and may appear in any position. The generated consumers bind arguments by name, so any other parameter must be optional (orparams) and keeps its default;ref/out/inparameters are not supported. - Use a non-generic,
publicorinternalinstance method on apartial, non-generic type. Static handlers are accepted (with warning KANCHN015) but are invoked directly, without DI. - Every shape the generator can't emit is a compile-time error (see Diagnostics); it never silently generates nothing.
Hosting modes
ChannelMode is resolved when the project compiles, not at runtime.
| Mode | Consumer | Use it for |
|---|---|---|
InProcess |
Generated BackgroundService that drains the channel for the life of the host |
Containers, VMs, worker services |
InvocationLocal |
Generated DrainAsync helper that your Lambda entry point awaits |
Re-batching the records a single Lambda invocation received, with a soft deadline |
Distributed |
None — only the writer interface and DI helper are emitted | A consumer that runs elsewhere; replace the writer registration with your own transport-backed IT{M}ChannelWriter |
Default |
InvocationLocal if the project references Kanject.Core.CloudFunction.Provider.AwsLambda, otherwise InProcess |
Letting the reference graph decide |
Note that the DI helper registers the in-process writer in every mode. In Distributed mode, register your own implementation of the writer interface after calling the helper so it takes precedence.
Lambda: invocation-local draining
InvocationLocalDrainer arms a linked cancellation token that fires at remainingTime − ChannelOptions.SafetyMargin, drains the channel, and returns a ChannelDrainResult whose UnAckedMessageIds you project into the trigger's partial-batch-failure response. On a soft deadline, the in-flight batch and everything still buffered in the channel are reported as un-acked.
Rules enforced by ChannelOptions.Validate(isInvocationLocal: true), which the drainer calls on entry (it throws ArgumentException):
FullModemust not beBoundedChannelFullMode.Wait— a producer blocked on a full channel would burn the invocation budget. SizeCapacityto the record count and use a drop mode.ConsumerConcurrencymust be1.
Because the attribute default is FullMode = Wait, set a drop mode for invocation-local channels, for example through the generated helper's configure callback.
Per-invocation channel (recommended)
The generated DI helper registers one process-wide channel, and completing its writer closes it for the lifetime of the process — a problem for warm Lambda invocations. For Lambda entry points, create a channel per invocation and call the engine directly:
using System.Text.Json;
using System.Threading.Channels;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;
using Kanject.Core.Channels.Engine;
using Kanject.Core.Channels.Engine.Models;
public readonly record struct QueuedOrder(string MessageId, OrderPlaced Order);
public sealed class OrderFunction(OrderBatchHandler handler)
{
public async Task<SQSBatchResponse> HandleAsync(SQSEvent sqsEvent, ILambdaContext context)
{
var options = new ChannelOptions
{
Capacity = Math.Max(1, sqsEvent.Records.Count),
BatchSize = 25,
FullMode = BoundedChannelFullMode.DropWrite, // Wait is rejected for invocation-local drains
SingleWriter = true,
};
var channel = ChannelDrainer.CreateChannel<QueuedOrder>(options);
foreach (var record in sqsEvent.Records)
{
// Deserialize with your own JsonSerializerContext for trimming / Native AOT.
var order = JsonSerializer.Deserialize(record.Body, AppJsonContext.Default.OrderPlaced)!;
channel.Writer.TryWrite(new QueuedOrder(record.MessageId, order));
}
channel.Writer.Complete();
ChannelDrainResult result = await InvocationLocalDrainer.DrainBatchedAsync(
channel.Reader,
(batch, ct) => handler.HandleAsync(batch, ct), // ValueTask HandleAsync(ReadOnlyMemory<QueuedOrder>, CancellationToken)
options,
context.RemainingTime,
CancellationToken.None,
messageIdSelector: item => item.MessageId);
return new SQSBatchResponse
{
BatchItemFailures = result.UnAckedMessageIds
.Select(id => new SQSBatchResponse.BatchItemFailure { ItemIdentifier = id })
.ToList(),
};
}
}
The commercial Kanject.Core.Channels.Provider.AwsSqs and Kanject.Core.Channels.Provider.AwsKinesis packages package this loop for SQS and Kinesis events (including the response projection) and ship generators that emit the Lambda entry point.
Generated DrainAsync helper
In InvocationLocal mode the generator emits T{M}ChannelDrain.DrainAsync(serviceProvider, remainingTime, cancellationToken). It resolves this channel's keyed ChannelReader<TItem> and ChannelOptions and the handler type from serviceProvider, then delegates to InvocationLocalDrainer. Things to know:
- The DI helper does not register the handler type in this mode — register it yourself.
- No typed message-ID selector is passed; set
ChannelOptions.MessageIdSelectorinconfigureif you needUnAckedMessageIds. - The channel is the process-wide singleton described above.
Lambda timeout behaviour
- Soft deadline. The drainer checks the token between batches (batched handlers) or between items (per-item handlers). A batch is either executed or reported un-acked as a whole — never torn.
- Partial-batch failures. Un-acked IDs go into the trigger's failure response so only those records are redelivered. This requires
ReportBatchItemFailureson the event source mapping. - Hard kill. If the margin was too small and the runtime kills the invocation, un-acked records are redelivered after the visibility timeout: at-least-once delivery, at the cost of duplicates.
Using the engine directly
| API | Purpose |
|---|---|
ChannelDrainer.CreateChannel<T>(ChannelOptions) |
Create a bounded channel from Capacity, FullMode, SingleReader, SingleWriter |
ChannelDrainer.RunPerItemAsync<T>(reader, handler, options, ct, messageIdSelector, drainBufferedOnCancel) |
Per-item drain loop |
ChannelDrainer.RunBatchedAsync<T>(…) |
Batched drain loop; a thrown exception fails the whole batch |
ChannelDrainer.RunBatchedWithResultAsync<T>(…) |
Batched drain loop whose handler returns a BatchResult |
InvocationLocalDrainer.DrainBatchedAsync<T> / DrainBatchedWithResultAsync<T> / DrainPerItemAsync<T> |
The same loops with a Lambda soft deadline (remainingTime − SafetyMargin) and buffered-item projection on cancel |
ChannelHostedServiceBase<T> |
Abstract BackgroundService that runs the drain loop with exponential-backoff restart; implement ExecuteBatchAsync, ExecuteItemAsync, and IsBatched |
InProcessChannelWriter<T> |
IChannelWriter<T> implementation over a ChannelWriter<T> |
drainBufferedOnCancel pulls items still buffered in the channel into the un-acked set when cancellation fires. Only enable it when the producer has finished writing (the invocation-local drainers set it); a hosted consumer running alongside a live writer must leave it false.
Partial batch success
A batched handler can report which items failed instead of failing the whole batch:
ChannelDrainResult result = await ChannelDrainer.RunBatchedWithResultAsync(
channel.Reader,
async (batch, ct) =>
{
var failed = new List<int>();
for (var i = 0; i < batch.Length; i++)
{
if (!await TryShipAsync(batch.Span[i], ct))
failed.Add(i);
}
return failed.Count == 0 ? BatchResult.Success : BatchResult.WithFailures(failed);
},
options,
cancellationToken,
messageIdSelector: item => item.MessageId);
Indices refer to positions in the batch the handler received. Returned failures are terminal: they are projected into UnAckedMessageIds but never retried. To have a batch retried, throw — RetryCount retries cover unhandled exceptions only, with RetryBackoff × 2^(attempt−1) backoff capped at MaxRestartBackoff. BatchResult.Success (and default) are allocation-free.
Contracts at a glance
Cancellation is all-or-nothing within a batch. If the handler observes the token and exits — by throwing OperationCanceledException or returning — the whole batch is treated as un-acked. There is no partial ack within a cancelled batch; handlers that need per-item ack semantics should use BatchSize = 1 or return a BatchResult.
Cancellation means the drain's own token. An OperationCanceledException ends the drain pass (un-acked batch, SoftDeadlineHit) only when the token the drainer was given is cancelled — the host stopping, or the Lambda soft deadline. An OperationCanceledException your handler throws for its own reasons, such as the TaskCanceledException from an HttpClient timeout, is an ordinary fault: it is retried per RetryCount, then reported through OnBatchFault and un-acked, and the drain carries on. The hosted consumer keeps running.
A HandlerTimeout expiry is a fault, not a soft deadline. The timed-out batch is counted as failed and un-acked, reported with a TimeoutException, and the drain moves on to the next batch. It is never retried: the abandoned invocation is still running, so a retry would process the batch twice at once. The abandoned invocation keeps its batch memory — the drainer never refills or re-pools it — but it does keep running, so handlers should still honour their token.
Scope boundary per mode.
| Mode | DI scope |
|---|---|
Hosted, Lifetime = Scoped |
One scope per batch |
Hosted, Singleton / Transient |
Handler resolved from the root provider; no per-batch scope |
| Invocation-local (generated helper) | Whatever IServiceProvider you pass to DrainAsync — typically one scope per invocation |
Ordering. Items written by one producer are dispatched in write order within a channel. There is no ordering between concurrent producers.
Faults. When a batch or item fails (after retries, or on a HandlerTimeout expiry), its IDs are added to the un-acked set and the loop continues while RestartOnFault is true. With RestartOnFault = false the exception propagates: the hosted consumer stops, and the invocation-local drainers throw.
Flushing. The first item of a batch is awaited indefinitely; after that the batch fills until BatchSize items, the channel completes, or FlushInterval elapses. With FlushInterval = TimeSpan.Zero (the default), a partial batch waits until it fills or the writer completes — set FlushIntervalMs for slow producers.
ChannelOptions reference
| Property | Default | Meaning |
|---|---|---|
Capacity |
1024 |
Bounded channel capacity; must be > 0 |
FullMode |
Wait |
BoundedChannelFullMode when the channel is full; Wait is invalid for invocation-local drains |
SingleReader / SingleWriter |
true / false |
Channel performance hints |
BatchSize |
1 |
Items per handler call |
FlushInterval |
Zero |
Maximum wait before a partial batch is flushed |
HandlerTimeout |
Zero (off) |
Per-batch timeout; a timed-out batch is a non-retried fault (failed and un-acked) and the drain continues |
ConsumerConcurrency |
1 |
Must be 1 for invocation-local drains; the current consumers run a single drain loop |
RestartOnFault |
true |
Keep draining after a fault (and restart the hosted loop) |
MaxRestartAttempts |
-1 (infinite) |
Hosted-loop restart limit |
InitialRestartBackoff / MaxRestartBackoff |
100 ms / 30 s | Exponential restart backoff bounds |
RetryCount / RetryBackoff |
0 / 50 ms |
Per-batch retries on unhandled exceptions, including a handler's own OperationCanceledException (batched loops; not on HandlerTimeout) |
SafetyMargin |
1000 ms | Subtracted from the Lambda remaining time for the soft deadline (the generated helper sets it from SafetyMarginMs, default 500 ms) |
MessageIdSelector |
null |
Func<object, string> that stamps un-acked items; a typed messageIdSelector argument takes precedence |
OnBatchFault |
null |
Action<int, Exception> called with the failed item count and exception |
Metrics |
null |
IChannelMetrics sink |
Observability
The drain loop logs soft deadlines, handler timeouts, and faults through Kanject's PrintInConsole pipeline under the tag Kanject.Channels. For metrics, implement IChannelMetrics and assign it to ChannelOptions.Metrics:
| Callback | Fired when |
|---|---|
OnBatchSucceeded(int itemCount, TimeSpan elapsed) |
A batch (or item) completes |
OnBatchFaulted(int itemCount, Exception exception) |
The handler throws (after any retries) or exceeds HandlerTimeout |
OnSoftDeadlineHit(int processedCount, int failedCount, int unAckedCount) |
The soft deadline cancels the drain |
OnHandlerTimeout(TimeSpan timeout) |
HandlerTimeout fires |
OnQueueDepthSampled(int depth) |
A queue-depth sample is taken |
Exceptions thrown by the sink are swallowed; observability never breaks the drain loop.
Current limitations
[ChannelProducer]is declared inKanject.Core.Channels.Annotations.Attributes, but no generator emits code for it; the analyzer reports every use as errorKANCHN017. For a producer-only port, declare a[Channel(Mode = ChannelMode.Distributed)]handler.
Channels or Parallel?
Kanject.Core ships [Parallel] and ParallelLoop.ForEachChunkAsync, which solve an adjacent problem. They compose well; pick the simpler one when only one is needed.
| Scenario | Reach for |
|---|---|
| Materialized collection, parallel batched I/O, one host | Parallel |
| One-shot batch processing inside a single Lambda invocation, no re-batching | Parallel |
| Asynchronous producer (pagination, stream, webhook fan-in) feeding a batch consumer | Channels |
| The same handler must run as a hosted consumer and inside a Lambda invocation | Channels |
| Time-based flush ("every 200 ms or every 25 items") | Channels (FlushIntervalMs) |
| Backpressure between a fast producer and a slower consumer | Channels (FullMode = Wait) |
| Per-item retry with backoff over a materialized list | Parallel (RetryCount / RetryBackoffMs) |
[Channel] and [Parallel] cannot be applied to the same method (KANCHN011).
Channels SQS adapter or Kanject.Core.Queue.Provider.AwsSqs?
| Package | Scope |
|---|---|
Kanject.Core.Channels.Provider.AwsSqs |
Receive-only and Lambda-first: drains an SQSEvent through the channel engine and returns an SQSBatchResponse; [SqsChannel] generates the entry point. No producer, no queue registry. |
Kanject.Core.Queue.Provider.AwsSqs |
Full messaging framework: producer (IQueueManager), consumers (AbstractQueueConsumer<T>, [QueueConsumer], [RouteQueueConsumer]), [QueueMessage], [SubscribeToQueue], queue provisioning, and schema generation. |
If you need to send, or want the framework to own queue lifecycle and routing, use Kanject.Core.Queue.Provider.AwsSqs. If you only need to receive a batch inside a Lambda, or want to move a handler between hosting modes, use the Channels adapter. The two can coexist in one service.
Diagnostics
Kanject.Core.Channels.Annotations reports these at compile time:
| ID | Severity | What it means |
|---|---|---|
KANCHN002 |
Error | The type containing an instance [Channel] method is not partial (code fix available) |
KANCHN003 |
Error | The method does not return Task or ValueTask |
KANCHN004 |
Error | No item/batch parameter (T, ReadOnlyMemory<T>, IReadOnlyList<T>) |
KANCHN005 |
Error | BatchSize > 1 but the parameter is a single item |
KANCHN006 |
Warning | Batch-shaped parameter with BatchSize = 1 |
KANCHN007 |
Error | Capacity is not > 0 |
KANCHN010 |
Warning | Mode = InvocationLocal without a Kanject.Core.CloudFunction.Provider.AwsLambda reference |
KANCHN011 |
Error | [Channel] and [Parallel] on the same method |
KANCHN013 |
Warning | FullMode = DropOldest, DropWrite or DropNewest silently drops items on overflow |
KANCHN014 |
Error | MaxRestartAttempts = 0; use -1, a positive value, or RestartOnFault = false |
KANCHN015 |
Warning | [Channel] on a static method; it is invoked directly, without DI |
KANCHN017 |
Error | [ChannelProducer] is not supported; no code is generated for it |
KANCHN018 |
Error | FullMode is not a defined ChannelFullMode member; no wiring is generated for the method |
KANCHN019 |
Error | The method is generic or is declared on a generic type |
KANCHN020 |
Error | A parameter is ref, out, in or ref readonly |
KANCHN021 |
Error | A required parameter other than the item/batch and the first CancellationToken |
KANCHN022 |
Error | The method or a containing type is private, protected, private protected or file-local |
KANCHN023 |
Error | The item type is a ref struct or pointer |
KANCHN024 |
Error | [Channel] on a local function, lambda, operator, accessor or explicit interface implementation |
Design notes
- Mode resolution happens at compile time.
Defaultis resolved from the project's referenced assemblies, so there is no runtime reflection and no behaviour change when a dependency is added at deploy time. - Batch boundaries are respected. Cancellation never tears a batch, which is what makes partial-batch-failure acknowledgement safe to rely on.
- Drop modes warn by default. Silent loss is rarely intended; KANCHN013 makes it explicit.
- Transport is chosen at the composition root. The attribute never names a transport; the domain assembly carries no AWS SDK reference.
Related packages
| Package | Role | Availability |
|---|---|---|
Kanject.Core.Channels.Annotations |
Source generator and analyzers for [Channel] |
nuget.org |
Kanject.Core.Channels.Annotations.Attributes |
[Channel], [ChannelProducer], and their enums |
nuget.org |
Kanject.Core |
Kernel; also provides [Parallel] / ParallelLoop |
nuget.org |
Kanject.Core.CloudFunction.Provider.AwsLambda |
Lambda hosting; its reference makes ChannelMode.Default resolve to InvocationLocal |
nuget.org |
Kanject.Core.Queue.Provider.AwsSqs |
Full SQS messaging framework (send and receive) | nuget.org |
Kanject.Core.Channels.Provider.AwsSqs |
SQS Lambda drainer and SQSBatchResponse projection |
Commercial license (not on nuget.org) |
Kanject.Core.Channels.Provider.AwsKinesis |
Kinesis Lambda drainer and StreamsEventResponse projection |
Commercial license (not on 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 (>= 3.12.7)
- Kanject.Core.Annotations (>= 3.12.7)
- Kanject.Core.Channels.Annotations.Attributes (>= 3.12.7)
-
net8.0
- Kanject.Core (>= 3.12.7)
- Kanject.Core.Annotations (>= 3.12.7)
- Kanject.Core.Channels.Annotations.Attributes (>= 3.12.7)
-
net9.0
- Kanject.Core (>= 3.12.7)
- Kanject.Core.Annotations (>= 3.12.7)
- Kanject.Core.Channels.Annotations.Attributes (>= 3.12.7)
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.14.0 | 48 | 10/5/2026 |
| 3.13.0 | 54 | 10/2/2026 |
| 3.12.1 | 120 | 9/27/2026 |
| 3.12.0 | 90 | 9/27/2026 |
| 3.11.7 | 90 | 9/26/2026 |
| 3.11.6 | 135 | 9/7/2026 |
| 3.11.5 | 102 | 8/27/2026 |
| 3.11.4 | 111 | 8/22/2026 |
| 3.11.3 | 120 | 8/10/2026 |
| 3.11.2 | 122 | 8/9/2026 |
| 3.11.1 | 122 | 8/5/2026 |
| 3.11.0 | 124 | 8/5/2026 |
| 3.10.0 | 118 | 8/3/2026 |
| 3.9.7 | 128 | 7/30/2026 |
| 3.9.6 | 125 | 7/18/2026 |
| 3.9.5 | 144 | 7/13/2026 |
| 3.9.4 | 123 | 7/11/2026 |
| 3.9.3 | 125 | 7/11/2026 |
| 3.9.2 | 132 | 7/9/2026 |