Kanject.Core.Channels
3.9.6
Prefix Reserved
dotnet add package Kanject.Core.Channels --version 3.9.6
NuGet\Install-Package Kanject.Core.Channels -Version 3.9.6
<PackageReference Include="Kanject.Core.Channels" Version="3.9.6" />
<PackageVersion Include="Kanject.Core.Channels" Version="3.9.6" />
<PackageReference Include="Kanject.Core.Channels" />
paket add Kanject.Core.Channels --version 3.9.6
#r "nuget: Kanject.Core.Channels, 3.9.6"
#:package Kanject.Core.Channels@3.9.6
#addin nuget:?package=Kanject.Core.Channels&version=3.9.6
#tool nuget:?package=Kanject.Core.Channels&version=3.9.6
Kanject Channels
One handler signature. Every hosting mode.
Write (ReadOnlyMemory<T> batch, CancellationToken ct) → ValueTask once, and run it unchanged under a long-lived BackgroundService, a Lambda invocation-local drainer, an SQS-triggered Lambda, a Kinesis stream, or an in-process smoke test. The transport is a composition-root concern — your domain code never references an adapter.
// Domain — transport-agnostic, hosting-agnostic
[Channel(Capacity = 5000, BatchSize = 25)]
public partial ValueTask HandleAsync(
ReadOnlyMemory<OrderEvent> batch, CancellationToken ct);
// Composition root — pick the transport
services
.AddOrderHandlerHandleChannel() // domain wiring
.AddSqsChannelTransport("orders-queue"); // adapter wiring (separate package)
The same handler moves between hosts as your deployment model evolves — no rewrites, no framework lock-in, no Lambda-SDK dependency in your domain assembly.
Status: Phase 1.3 shipped. Cancellation, ordering, scope-boundary and FullMode contracts are locked (see
ChannelOptions). SQS adapter + generator shipped (Phase 2.1 / 2.2). Remaining phases: partial-batch atomicity, Kinesis adapter, samples, integration tests.
What you get
| Hosting-mode portability | One handler shape runs under BackgroundService, Lambda invocation-local, or SQS-triggered Lambda without edits. |
| Hexagonal seam | Generated IXChannelWriter port lives in your domain assembly — zero adapter dependencies. |
| Adapter-agnostic | SQS, Kinesis, EventBridge, in-memory all plug at the composition root; each lives in its own package with its own generator. |
| Lambda-first deadline handling | 3-layer defence (soft CTS → un-acked-ID projection → SQS visibility redelivery) baked in. |
| Conformance-gated parity | Every adapter passes the shared ChannelConformanceTests suite before release. |
| Observability | PrintInConsole log records + pluggable IChannelMetrics sink (CloudWatch / OTel / Meter). |
| Source-generated | Typed writer, consumer, DI helper, and adapter-specific entry-points — no reflection, AOT-clean. |
| Internal re-batching | SQS hands you 100 records? Handle in batches of 25 without leaving the invocation. |
🏛️ Hexagonal positioning
- Port: The generated
IXChannelWriterinterface is the driven port. It lives in the domain assembly, with no dependency on any adapter or SDK. - Adapters: Each transport (SQS, EventBridge, Kinesis, in-memory, etc.) is a separate package, providing a provider-specific DI extension and generator. No domain code references any adapter directly.
- Composition root: The adapter is selected at startup via DI wiring, not via attribute arguments.
Conformance contract: Every adapter must pass the universal ChannelConformanceTests suite (see below) to guarantee observable behavior is uniform across transports.
🧭 Parallel vs Channels — which do I reach for?
Parallel and Channels solve adjacent problems and are routinely composed. If you only need one, pick the simplest that fits.
| Scenario | Reach for |
|---|---|
| Materialized collection, parallel batched I/O, one host | Parallel — ForEachChunkAsync is simpler and cheaper. |
| One-shot SQS batch processing inside a single Lambda invocation | Parallel — no channel needed. |
| Async producer (pagination, stream, webhook fan-in) → batch consumer | Channels (optionally × Parallel inside the handler). |
The same handler must run unchanged as BackgroundService, Lambda inline, and SQS-triggered Lambda |
Channels — handler portability is the unique value. |
| Time-based flush of slow-arriving items (e.g. "flush every 200 ms or every 25 items") | Channels — FlushInterval is built-in. |
| Backpressure between a fast producer and a bounded, slower consumer | Channels — FullMode.Wait gives you that for free. |
| You want per-item retry with backoff on a materialized list | Parallel — RetryCount/RetryBackoffMs. |
Rule of thumb: if the production side is synchronous and bounded, Parallel wins on ceremony; if the production side is asynchronous or unbounded, or if you need the handler to be host-portable, Channels earns its keep.
🧭 SQS Channels adapter vs native SQS QueueConsumer — which do I reach for?
Two SQS packages exist; they solve overlapping-but-distinct problems. Pick by the shape of the thing you're building.
| Package | Scope | Shape |
|---|---|---|
Kanject.Core.Channels.Provider.AwsSqs |
Receive-only, Lambda-first. A bounded-channel drainer + SQSBatchResponse projection. No producer, no registry, no schema. |
One-function Lambda entry-point; the same handler also runs under BackgroundService or invocation-local. |
Kanject.Core.Queue.Provider.AwsSqs |
Full messaging framework. Producer (IQueueManager), receiver (AbstractQueueConsumer<T>, [QueueConsumer], [RouteQueueConsumer]), queue registry, schema/doc generation, publish/subscribe wiring. |
Long-running service that both sends and receives; batch DLQ; routed consumers. |
| Scenario | Reach for |
|---|---|
| SQS-triggered Lambda, partial-batch failures, deadline-aware drain, minimal ceremony | Channels SQS adapter — [SqsChannel] emits the entry-point; no DI registry required. |
Same handler must run under BackgroundService, invocation-local, and SQS-triggered Lambda |
Channels SQS adapter — handler portability is the unique value. |
| You need retry-with-backoff inside a Lambda invocation before surfacing failures to SQS | Channels SQS adapter — RetryCount / RetryBackoff in ChannelOptions. |
| Long-running worker that polls queues, publishes to other queues, subscribes to SNS topics, routes by payload schema | Native QueueConsumer — IQueueManager, [RouteQueueConsumer], SubscribeToQueue are built for this. |
| You rely on the queue registry (auto-create, namespace prefixing, declarative provisioning) | Native QueueConsumer — the Channels adapter is agnostic to how the queue was provisioned. |
You publish typed messages with [QueueMessage] / [EventQueueMessage] and want the generated producer |
Native QueueConsumer — Channels SQS adapter is receive-only. |
You want both: a legacy BackgroundService and a new Lambda entry-point on the same handler |
Channels SQS adapter — the native one is coupled to BackgroundService; Channels is mode-portable. |
Rule of thumb: if you need to send, or you want the framework to own queue lifecycle and routing, use the native provider. If you only need to receive a batch inside a Lambda (or move an existing handler between hosting modes), use the Channels adapter. The two can coexist in one service — e.g. native IQueueManager on the send side, Channels [SqsChannel] on the Lambda ingress.
📜 Contracts at a glance
These are load-bearing semantics. Authoritative versions live in the ChannelOptions XML docs; the summary here is for orientation.
Cancellation is all-or-nothing within a batch. The handler receives a CancellationToken for cooperative shutdown. If the handler observes the token and returns early — whether by throwing OperationCanceledException or returning normally — the entire batch is treated as un-acked and its message IDs flow into BatchItemFailures. The drainer does not support partial-ack within a batch. Handlers that need per-item ack semantics must set BatchSize = 1. Cancellation is checked between reads (per-item) or between batches (batched) — never torn across a dispatch.
Scope boundary per mode.
| Mode | DI scope lifetime |
|---|---|
Hosted (BackgroundService) |
one scope per batch dispatch |
| Invocation-local (Lambda inline) | one scope per invocation |
| Distributed (SQS-triggered Lambda) | one scope per invocation — adapter-defined |
Ordering guarantees.
| Mode | Within a batch | Across batches |
|---|---|---|
Hosted, ConsumerConcurrency = 1 |
FIFO | FIFO |
Hosted, ConsumerConcurrency > 1 |
FIFO | No guarantee |
| Invocation-local | FIFO | FIFO |
| Distributed + SQS Standard | FIFO (consumer-local) | No guarantee |
| Distributed + SQS FIFO | FIFO | FIFO per MessageGroupId |
| Distributed + Kinesis | FIFO per shard | FIFO per shard |
| Distributed + EventBridge | No guarantee | No guarantee |
FullMode × mode validity. BoundedChannelFullMode.Wait is rejected at startup for invocation-local drains (would block past the Lambda deadline). Use DropOldest, DropNewest, or DropWrite for invocation-local; the SQS adapter forces DropWrite since capacity matches record count and the channel cannot overflow.
⚠️ The Problem
You have a stream of items and a batched, async per-batch handler. The naïve options:
- Hand-rolled
Channel<T>+BackgroundService— every team rewrites the same drainer, batching, restart-on-fault, graceful-shutdown, and DI-scope code, slightly differently, slightly buggily. ~80 lines of plumbing per channel. - SQS poll loop in a hosted service — couples your handler to AWS SDK; no in-proc fast path; no batching layer between the SDK's per-poll batch and your business batch size; no graceful-shutdown story.
- Generic
IBackgroundJobQueueabstractions — usuallyTask-typed, lose item-type fidelity, can't batch, can't surface backpressure. - Lambda + SQS event source — works, but you get whatever batch SQS hands you (max 10 by default; up to 10000 with
MaximumBatchingWindowInSeconds), no internal re-batching, no in-invocation fan-out, no DI-scope-per-batch.
What's missing is the same trick Parallel pulled off: declare the channel intent on the method, get a typed producer + a host-appropriate consumer for free.
🛠️ The Solution
Five pieces, mirroring the Parallel quintet:
| Piece | Status | Purpose |
|---|---|---|
Engine (ChannelDispatcher, InvocationLocalDrainer) |
⏳ Phase 1.0 | Bounded-channel drainer with batching, backoff-restart, graceful drain, deadline-aware cancellation |
Attributes ([Channel], [ChannelProducer]) |
⏳ Phase 1.0 | Compile-time defaults the generator surfaces as method-local config |
Source generator (ChannelGenerator) |
⏳ Phase 1.0 | Emits typed writer interface, consumer, DI helper |
Analyzer (ChannelMethodAnalyzer) |
⏳ Phase 1.0 | KANCHN001–015 compile-time validation |
Transport seam (IChannelProducerTransport + IChannelConsumerTransport) |
⏳ Phase 2.0 | Pluggable distributed mode — AwsSqs, EventBridge, Kinesis providers |
The engine is fully usable on its own — call ChannelDispatcher.RunAsync(...) directly with your handler. The generator is what removes the call-site boilerplate.
📖 Attribute Surface
[AttributeUsage(AttributeTargets.Method)]
public sealed class ChannelAttribute : Attribute
{
// ── Channel sizing ────────────────────────────────────────────
public int Capacity { get; init; } = 1024;
public BoundedChannelFullMode FullMode { get; init; } = BoundedChannelFullMode.Wait;
// ── Batching ──────────────────────────────────────────────────
public int BatchSize { get; init; } = 1; // 1 = no batching
public int FlushIntervalMs { get; init; } = 0; // 0 = no time-flush
// ── Hosting ───────────────────────────────────────────────────
public ChannelMode Mode { get; init; } = ChannelMode.Default;
public ServiceLifetime Lifetime { get; init; } = ServiceLifetime.Scoped;
// ── Resilience ────────────────────────────────────────────────
public bool RestartOnFault { get; init; } = true;
public int MaxRestartAttempts { get; init; } = -1; // -1 = infinite
public int SafetyMarginMs { get; init; } = 500; // InvocationLocal only
// ── Naming ────────────────────────────────────────────────────
public string? ChannelName { get; init; }
}
public enum ChannelMode
{
Default, // generator picks: InvocationLocal if Lambda referenced, else InProcess
InProcess, // System.Threading.Channels + BackgroundService consumer
InvocationLocal, // System.Threading.Channels + inline drainer (Lambda)
Distributed // Adapter-provided; consumer is a separate process/Lambda
}
Producer-only declaration (when the producer doesn't itself host a [Channel] method):
[ChannelProducer(typeof(OrderEvent))]
public partial class OrderProducer { }
// → generates IOrderEventChannelWriter + DI helper
🧱 Hosting Modes & Adapter Selection
| Mode | Producer | Consumer | When to pick |
|---|---|---|---|
InProcess |
IXChannelWriter (DI) |
Generated BackgroundService |
Long-running container / EC2 / hosted worker |
InvocationLocal |
IXChannelWriter (DI) |
Generated inline drainer the entry-point awaits | Lambda — in-invocation fan-out (e.g. SQS hands you 100 records, you internally batch to 25) |
Distributed |
IXChannelWriter (DI) |
Adapter-provided consumer (e.g. SQS Lambda, EventBridge rule) | Cross-process / cross-invocation |
Default |
(one of the above) | (one of the above) | Generator picks InvocationLocal if the project references Kanject.Core.CloudFunction.Provider.AwsLambda, else InProcess. Compile-time only — no reflection. |
Adapter selection is at the composition root:
// Domain (annotated method) — transport-agnostic
[Channel(Capacity = 5000, BatchSize = 25)]
public partial ValueTask HandleBatchAsync(ReadOnlyMemory<OrderEvent> batch, CancellationToken ct);
// Composition root — adapter chosen here
services
.AddOrderHandlerHandleBatchChannel() // domain wiring
.AddSqsChannelTransport("orders-queue"); // adapter wiring (separate package)
🚀 Generator Topology
Two generators, two packages:
| Generator | Lives in | Outputs |
|---|---|---|
ChannelDomainGenerator |
Kanject.Core.Annotations |
Port interface, handler interface fragment, in-process drainer/hosted service, DI helper (transport-agnostic) |
ChannelTransportBindingGenerator |
Adapter package (e.g. Kanject.Core.Channel.Provider.AwsSqs.Annotations) |
Adapter-specific DI extension, provider-specific analyzers, conformance test hooks |
For:
public sealed partial class OrderHandler
{
[Channel(Capacity = 5000, BatchSize = 25, FlushIntervalMs = 200)]
public partial ValueTask HandleBatchAsync(
ReadOnlyMemory<OrderEvent> batch, CancellationToken ct);
}
…the generator emits five outputs (mirrors the Parallel generator's split):
// 1. Producer interface — what callers inject
public interface IOrderHandlerHandleBatchChannelWriter
{
ValueTask WriteAsync(OrderEvent item, CancellationToken ct = default);
ValueTask WriteAsync(ReadOnlyMemory<OrderEvent> items, CancellationToken ct = default);
bool TryWrite(OrderEvent item);
ValueTask CompleteAsync(); // graceful drain signal
}
// 2. Auto-attached interface fragment on the consumer type
// (the same partial-class trick KANPAR011 enforces)
partial class OrderHandler : IOrderHandlerHandleBatchChannelHandler { }
// 3. DI helper
public static IServiceCollection AddOrderHandlerHandleBatchChannel(
this IServiceCollection services,
Action<ChannelOptions>? configure = null);
// 4. Hosted consumer (Mode = InProcess)
internal sealed class OrderHandlerHandleBatchChannelService : BackgroundService { ... }
// 5. Invocation-local drainer (Mode = InvocationLocal)
public static class OrderHandlerHandleBatchChannelDrain
{
public static Task<ChannelDrainResult> DrainAsync(
IServiceProvider sp, CancellationToken ct);
}
ChannelDrainResult exposes:
int ProcessedCountint FailedCountIReadOnlyList<string> UnAckedMessageIds— wire straight intoSQSBatchResponse.BatchItemFailures
🔌 Lambda Integration (Mode = InvocationLocal)
The kanject-specific value-add: internal re-batching across the records SQS hands a single Lambda invocation, deadline-aware, with partial-batch-failure semantics.
// [CloudFunction] handler — kanject's existing Lambda primitive
public async Task<SQSBatchResponse> HandleAsync(SQSEvent evt, ILambdaContext ctx)
{
using var scope = _sp.CreateAsyncScope();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(default);
cts.CancelAfter(ctx.RemainingTime - TimeSpan.FromMilliseconds(500));
var writer = scope.ServiceProvider
.GetRequiredService<IOrderHandlerHandleBatchChannelWriter>();
foreach (var record in evt.Records)
await writer.WriteAsync(
JsonSerializer.Deserialize<OrderEvent>(record.Body)!, cts.Token);
await writer.CompleteAsync();
var drain = await OrderHandlerHandleBatchChannelDrain.DrainAsync(
scope.ServiceProvider, cts.Token);
return new SQSBatchResponse
{
BatchItemFailures = drain.UnAckedMessageIds
.Select(id => new SQSBatchResponse.BatchItemFailure { ItemIdentifier = id })
.ToList(),
};
}
Lambda timeout behaviour
Three layers of defence, all compile-time wired:
Layer 1 — Soft deadline. Generated glue creates a linked CTS firing RemainingTime − SafetyMarginMs before the hard kill. Drainer observes the token at:
WaitToReadAsync(no new batch starts)- the
handler(batch, ct)boundary (handler gets a cooperative-cancel chance) - between batches, never mid-batch — a batch is either fully executed or fully un-acked.
Layer 2 — Partial-batch failures. Un-handled items go into BatchItemFailures so SQS re-delivers only the tail. Successfully handled messages are gone for good. Requires ReportBatchItemFailures on the event source — KANCHN016 (project-level analyzer) warns if missing.
Layer 3 — Hard kill. If the safety margin was wrong, Lambda kills the process; un-acked items are redelivered after the visibility timeout. At-least-once preserved at the cost of duplicates — this is the seam where future [Idempotent] plugs in.
| Scenario | Outcome | Lost | Duplicates |
|---|---|---|---|
| Soft-deadline, drainer cooperates | Partial ack | None | None |
| Handler throws on a batch | That batch's IDs in BatchItemFailures |
None | The failed batch |
| Handler ignores CT, hard-kill | Lambda returns failure | None | All un-acked |
Mode = InvocationLocal + DropOldest overflow |
Producer drops silently | Yes | None — see KANCHN013 |
🔍 Diagnostics — KANCHN001…KANCHN016
| ID | Severity | Trigger |
|---|---|---|
KANCHN001 |
Error | [Channel] method must be partial |
KANCHN002 |
Error | Containing type must be partial |
KANCHN003 |
Error | Return type must be Task / ValueTask |
KANCHN004 |
Error | First parameter must be T, ReadOnlyMemory<T>, or IReadOnlyList<T> |
KANCHN005 |
Error | BatchSize > 1 requires ReadOnlyMemory<T> / IReadOnlyList<T> |
KANCHN006 |
Warning | BatchSize == 1 with ReadOnlyMemory<T> arg (probably unintended) |
KANCHN007 |
Error | Capacity must be > 0 |
KANCHN008 |
Error | Mode = Distributed requires non-null Transport |
KANCHN009 |
Error | Transport name does not resolve to a registered IChannelProducerTransport / IChannelConsumerTransport |
KANCHN010 |
Warning | Mode = InvocationLocal on a project with no Lambda reference |
KANCHN011 |
Error | [Channel] and [Parallel] both applied — pick one (lifts in Phase 3.5) |
KANCHN012 |
Error | Item type T must be JSON-serializable for Distributed |
KANCHN013 |
Warning | FullMode = DropOldest / DropWrite without ack semantics — silent loss |
KANCHN014 |
Error | MaxRestartAttempts must be -1 or > 0 |
KANCHN015 |
Warning | Method is static — no IXChannelHandler interface fragment generated |
KANCHN016 |
Warning | Mode = InvocationLocal requires ReportBatchItemFailures on the SQS event source (project-level heuristic via serverless.yml / template.yaml if discoverable) |
🔌 Adapter seam: port/adapters split
Producer/consumer transport interfaces are split:
public interface IChannelProducerTransport
{
string Name { get; }
ValueTask SendAsync<T>(string resource, T item, CancellationToken ct);
// Provider-specific batch methods may be exposed via type-test (see README notes)
}
public interface IChannelConsumerTransport
{
string Name { get; }
ChannelReader<T> Subscribe<T>(string resource, ChannelSubscriptionOptions options);
}
Each adapter package provides its own generator and analyzer.
Conformance contract: Every adapter must pass the universal ChannelConformanceTests suite:
| Test | What it asserts |
|---|---|
Single_message_round_trips |
Producer write → consumer reads exactly once |
Batch_of_N_round_trips_in_order |
FIFO providers preserve order; non-FIFO documents the relaxation |
Cancellation_during_drain_yields_unacked_ids |
The Lambda-timeout contract is universal |
Producer_overflow_honors_FullMode |
Wait blocks; DropOldest drops the oldest; etc. — provider-uniform |
Handler_throw_yields_partial_batch_failure |
Same ChannelDrainResult shape from every transport |
Item_size_at_provider_limit_succeeds |
Provider-specific cap (SQS 256KB) is enforced via analyzer + runtime guard |
Item_size_above_limit_fails_at_send |
Loud failure, not silent truncation |
Every provider project must subclass the conformance suite and pass all tests before publishing.
🗺️ Roadmap
⏳ Phase 1.0 — InProcess mode end-to-end
ChannelAttribute,ChannelProducerAttribute,ChannelModeenumChannelDispatcherengineChannelGenerator(5 outputs)ChannelMethodAnalyzer(KANCHN001–007, 011, 014, 015)- Sample console fan-out app
⏳ Phase 1.5 — InvocationLocal drainer
InvocationLocalDrainerengine- KANCHN010 wired
- Default-mode generator-time heuristic via
Compilation.ReferencedAssemblyNames - Lambda + SQS sample
⏳ Phase 2.0 — Distributed transport seam + AwsSqs adapter package
IChannelProducerTransport/IChannelConsumerTransportinterfacesKanject.Core.Channel.Provider.AwsSqs(adapter package)ChannelTransportBindingGeneratoremits DI extension, provider-specific analyzers, conformance suite
⏳ Phase 2.5 — EventBridge adapter package
Kanject.Core.Channel.Provider.EventBridge(adapter package)- EventBridge-rule → SQS-pipe → consumer-Lambda end-to-end sample
⏳ Phase 3.0 — [CircuitBreaker] integration
- Open breaker pauses drain; surfaces via
OnOpencallback
⏳ Phase 3.5 — [Channel] × [Parallel] composition (lifts KANCHN011)
- Each batch dispatched via
ParallelLoop.ForEachChunkAsync - Recurring × Parallel × Channels triple-composition sample
⏳ Phase 4.0 — Kinesis provider
- Ordered, sharded distributed transport
✏️ Design decisions worth knowing
Defaultmode is generator-time, not runtime. The generator inspects the project's referenced assemblies (Compilation.ReferencedAssemblyNames) — no reflection at runtime, no startup cost, no surprise behaviour swaps when a dependency is added at deploy time.- Producer/consumer transports are split. SQS-receive does its work via host wiring (event source mapping), not application code; a unified
IChannelTransportwould force half-emptySubscribeimplementations. The split mirrors how AWS actually works, and matches the port/adapter boundary in hexagonal. - Adapter selection is at the composition root. The domain never names its adapter; the handler is annotated transport-agnostically, and the adapter is chosen in DI wiring. This keeps the domain assembly free of SDK references and enables true swap-ability.
- Conformance contract is enforced. Every adapter must subclass and pass the universal conformance suite before publishing, ensuring observable behavior is uniform.
- Generator topology is two-tier. The domain generator emits only transport-agnostic code; each adapter package ships its own generator and analyzer for provider-specific wiring and diagnostics.
- Adapter scorecard is published. Each adapter's cold-start, steady-state allocation, and throughput are benchmarked and published in the README; regressions block release.
- Soft-deadline cancellation respects batch boundaries. A batch is either fully executed or fully un-acked; cancellation never tears mid-batch. This is what makes partial-batch-failure ack semantics safe to rely on.
FullMode = DropOldest/DropWritewarns by default. Silent message loss is rarely what callers actually want; KANCHN013 surfaces it loudly.- Predicate parity from day one.
ChannelMethodAnalyzerandChannelGeneratorshare a singleChannelTransformHelperforForAttributeWithMetadataNameextraction — no repeat of the schema-vs-code-generator divergence the repo memory documents for the Dynamo pipeline. - Partial enforcement ships in Phase 1.0. No KANPAR011-style after-the-fact retrofit; KANCHN001 + KANCHN002 are in the v1 diagnostic set.
- Test fixtures
partialfrom the first commit. Every[Channel]-annotated test type ispublic sealed partial class— the Parallel test-suite migration is not repeated here. EmitCompilerGeneratedFilestrap pre-documented in the sample csproj. Same MSBuild comment block as the Parallel sample.
| 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.10.5)
- Kanject.Core.Annotations (>= 3.10.5)
- Kanject.Core.Channels.Annotations.Attributes (>= 3.10.4)
-
net8.0
- Kanject.Core (>= 3.10.5)
- Kanject.Core.Annotations (>= 3.10.5)
- Kanject.Core.Channels.Annotations.Attributes (>= 3.10.4)
-
net9.0
- Kanject.Core (>= 3.10.5)
- Kanject.Core.Annotations (>= 3.10.5)
- Kanject.Core.Channels.Annotations.Attributes (>= 3.10.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.