Konduit 0.6.0

dotnet add package Konduit --version 0.6.0
                    
NuGet\Install-Package Konduit -Version 0.6.0
                    
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="Konduit" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Konduit" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="Konduit" />
                    
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 Konduit --version 0.6.0
                    
#r "nuget: Konduit, 0.6.0"
                    
#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 Konduit@0.6.0
                    
#: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=Konduit&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=Konduit&version=0.6.0
                    
Install as a Cake Tool

Konduit

CI NuGet

ASP.NET Core style middleware pipelines for any dependency-injected service.

Register a service the way you already do, and chain WithMiddleware<T>() to wrap every call to it:

services.AddScoped<IOrderService, OrderService>()
        .WithMiddleware<LoggingMiddleware>()
        .WithMiddleware<RetryMiddleware>();

Resolving IOrderService now gives you a proxy. Every call travels Logging → Retry → OrderService, and the "after" half of each middleware unwinds on the way back out — exactly like app.Use(...), but for method calls instead of HTTP requests.

Proxies are source-generated, so there is no reflection on the call path and nothing to configure.

Writing middleware

Middleware takes the next step in the pipeline as a KonduitDelegate constructor parameter, just as ASP.NET Core middleware takes a RequestDelegate. Every other constructor parameter comes from DI.

public sealed class LoggingMiddleware(KonduitDelegate next, ILogger<LoggingMiddleware> logger)
    : IKonduitMiddleware
{
    public async ValueTask InvokeAsync(KonduitContext context)
    {
        logger.LogInformation("→ {Method}", context.Method.Name);

        var started = Stopwatch.GetTimestamp();
        await next(context);

        logger.LogInformation("← {Method} in {Elapsed}",
            context.Method.Name, Stopwatch.GetElapsedTime(started));
    }
}

The pipeline is built per service resolution, from the resolving scope, so middleware can inject scoped dependencies directly:

public sealed class AuditMiddleware(KonduitDelegate next, AppDbContext db) : IKonduitMiddleware

What's on the context

Member
Method Name, declaring type, return type, parameters. Method.MethodInfo is resolved lazily, only if you ask.
Arguments The call arguments. Writing to the array before next changes what the target receives.
Result The return value. For async methods this is the awaited value, not the task.
Target The implementation being wrapped.
Services The provider that resolved the service.
CancellationToken Lifted from the method's own token parameter when it has one.
Items A bag for passing state between middleware. Allocated on first use.

Because Result holds the awaited value, a middleware wrapped around Task<Order> PlaceAsync() sees the finished Order after await next(context) — not a Task that hasn't run yet.

Things middleware can do

// Rewrite an argument on the way in
context.Arguments[0] = Sanitize((string)context.Arguments[0]!);

// Rewrite the result on the way out
await next(context);
context.Result = Redact((Order)context.Result!);

// Short-circuit: skip the target entirely
if (cache.TryGetValue(key, out var hit))
{
    context.Result = hit;
    return;               // never calls next
}

// Handle failures
try { await next(context); }
catch (TimeoutException) { context.Result = Order.Unavailable; }

What gets intercepted

Every method declared on the service interface, including inherited ones.

Opt a single method out with [SkipKonduit] — it is forwarded straight to the implementation, allocating nothing and running no middleware:

public interface IOrderService
{
    Task<Order> PlaceAsync(Cart cart, CancellationToken cancellationToken);

    [SkipKonduit]
    string Describe(int id);      // hot path, no middleware
}

When the interface belongs to a package you can't edit, put the attribute on your implementation instead:

public sealed class OurStorage : IThirdPartyStorage   // interface you cannot annotate
{
    public Task<Stream> ReadAsync(string key) => ...;

    [SkipKonduit]
    public Task<bool> ExistsAsync(string key) => ...;   // no middleware for this one
}

services.AddScoped<IThirdPartyStorage, OurStorage>().WithMiddleware<LoggingMiddleware>();

That applies to OurStorage alone — another implementation behind the same interface keeps every method intercepted.

It works because the registration names the implementation, which is the only thing to check:

services.AddScoped<IFoo, Foo>()                         // ✅ named
services.AddScoped(typeof(IFoo), typeof(Foo))           // ✅ named
services.AddHttpClient<IFoo, Foo>()                     // ✅ named
services.AddScoped<IFoo, Foo>(sp => new Foo(...))       // ✅ named, even with a factory
services.AddScoped<IFoo>(sp => new Foo(...))            // ❌ not named

Only the last form hides it. If you build the instance yourself and want the attribute honoured, use the two-type-parameter factory overload — same lambda, one more type argument.

Properties, indexers and events are forwarded without interception. So are methods Konduit cannot proxy — each one produces a build warning naming the method and the reason, so nothing is silently uninstrumented.

Interception happens through the interface. A call from inside OrderService to its own method does not pass through the pipeline, and neither does a call through a concrete OrderService reference. The implementation stays resolvable unwrapped if you want it:

provider.GetRequiredService<IOrderService>();   // the proxy
provider.GetRequiredService<OrderService>();    // the real thing, no pipeline

Registration

WithMiddleware<T>() decorates the service registered immediately before it, so chain it directly onto the registration:

// Good
services.AddScoped<IOrderService, OrderService>().WithMiddleware<LoggingMiddleware>();

// Build error KDT001 — Konduit can't tell which service this belongs to
services.AddScoped<IOrderService, OrderService>();
services.WithMiddleware<LoggingMiddleware>();

AddScoped, AddSingleton and AddTransient all work, in their generic, factory, instance and typeof(...) forms. The registration keeps its original lifetime.

Konduit does not replace the built-in registration methods. An AddScoped<TService, TImpl>() of its own would have the same signature as the BCL's and make every registration in your project ambiguous, so WithMiddleware<T>() is the only extension added.

Not supported

Keyed services and open generics both throw at registration time with a message naming the type. Register each closed generic separately if you need middleware on it:

services.AddScoped<IRepository<Order>, OrderRepository>().WithMiddleware<LoggingMiddleware>();

Build diagnostics

ID
KDT001 error WithMiddleware<T>() is not chained onto a service registration
KDT002 error The service is not registered as an interface
KDT003 warning A method cannot be intercepted and will bypass the pipeline
KDT004 error The registration shape was not recognised
KDT006 warning An init-only interface property cannot be forwarded

Caveats

Synchronous methods and async middleware. Sync methods are supported: the proxy runs the same pipeline and, when every middleware completed synchronously — the usual case for logging, validation or metrics — takes the result with no blocking at all. It blocks only if a middleware genuinely went async inside a synchronous call, which carries the usual sync-over-async deadlock risk. If your middleware does real I/O, prefer async methods on the interface.

IAsyncEnumerable<T> is not streamed. A method returning IAsyncEnumerable<T> is treated as an ordinary value-returning method: middleware sees the enumerable object as Result, and the "after" half runs when the method returns, before anything is enumerated. Timing or logging middleware on such a method measures the call, not the iteration.

Init-only interface properties cannot be forwarded. No proxy can assign another object's init-only member. The generated accessor throws NotSupportedException and KDT006 warns at build time.

Default interface members are not intercepted directly. They still dispatch correctly, and calls they make back onto the interface do go through the pipeline.

Performance

Proxies are generated at compile time, so an intercepted call costs a direct virtual call plus the context and argument-array allocations — no reflection, no IL emit, no dynamic dispatch. Method descriptors and the terminal delegates are static and shared across calls. [SkipKonduit] methods compile down to a direct forward.

The library is trim- and AOT-compatible.

MediatR

MediatR discovers handlers by scanning assemblies, so there is no registration call to chain WithMiddleware<T>() onto. The companion package Konduit.MediatR adds one call that wraps everything MediatR found:

services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

services.AddKonduitToMediatRHandlers()
        .WithMiddleware<LoggingMiddleware>()
        .WithMiddleware<RetryMiddleware>();

Commands, queries, notifications and streams all run through the same middleware you write for ordinary services, with [SkipKonduit] opting a handler out.

Typed HttpClient services

AddHttpClient<TClient, TImplementation>() returns an IHttpClientBuilder, with no registration call to chain WithMiddleware<T>() onto. The companion package Konduit.Http adds AddMiddleware<T>():

services.AddHttpClient<IOrderApi, OrderApi>(c => c.BaseAddress = new Uri("https://api.example.com"))
        .AddMiddleware<LoggingMiddleware>()
        .AddMiddleware<RetryMiddleware>();

This wraps calls to the service, not the underlying HttpClient: middleware sees GetAsync(7) returning an Order, not a GET /orders/7 returning 200 OK. Reach for a DelegatingHandler when you want the HTTP exchange itself — the two compose.

Typed clients registered under a name work the same way. A bare named client (AddHttpClient("orders")) registers no interface, so there are no method calls to intercept; put the middleware on the service that consumes it instead.

Samples

Three runnable console apps under samples/, one per package, printing what the pipeline did at each step:

dotnet run --project samples/Konduit.Sample
dotnet run --project samples/Konduit.MediatR.Sample
dotnet run --project samples/Konduit.Http.Sample

Requirements

Konduit targets netstandard2.0, net8.0 and net10.0, so it runs on .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5 and later, Mono, Xamarin and Unity.

Building requires the .NET 6 SDK or later. That is a property of the compiler, not of what you target: the proxies come from an incremental source generator, which needs Roslyn 4.0. You can build with a current SDK and still target net472.

The source generator ships inside the Konduit package; no separate reference is needed.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Konduit:

Package Downloads
Konduit.Http

Adds Konduit middleware to typed HttpClient services. Chain AddMiddleware<T>() onto AddHttpClient<TClient, TImplementation>() to wrap calls to the client itself, rather than adding a DelegatingHandler to the underlying HttpClient.

Konduit.MediatR

Runs MediatR handlers through a Konduit middleware pipeline. One registration call wraps every handler discovered by MediatR's assembly scanning, so the same middleware you use for ordinary services also wraps commands, queries, notifications and streams.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.6.0 176 9/20/2026