LoomKit.Requests 10.0.2

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

LoomKit.Requests

A lightweight mediator library for .NET: send requests (with or without a response) to handlers resolved through dependency injection, with an optional per-request-type middleware pipeline. If you've used MediatR, the shape will feel familiar.

Status: early stage. The public API may still change between versions — pin a commit/tag if you depend on it.

Features

  • Fire-and-forget requests (IRequest) and request/response requests (IRequest<TResponse>)
  • Handlers resolved from your DI container (Microsoft.Extensions.DependencyInjection)
  • An optional middleware pipeline per request type, configured at startup
  • Built-in tracing via System.Diagnostics.ActivitySource (OpenTelemetry-compatible)
  • CancellationToken propagated end-to-end, from SendAsync through every middleware down to the handler
  • Extensible: bring your own IRequestSender implementation if the default one doesn't fit

Requirements

  • .NET 10 or later
  • Depends on LoomKit.Requests.Abstractions (the interfaces and abstract base types, in their own package) and Microsoft.Extensions.DependencyInjection.Abstractions — no other runtime dependency

Architecture: split from LoomKit.Requests.Abstractions

The interfaces (IRequest, IRequestHandler<>, IRequestSender) and abstract base types (RequestMiddleware<>, RequestSender<>, RequestSenderOptions, RequestSenderOptionsBuilder<>) live in the separate, lighter LoomKit.Requests.Abstractions package, which this package references. LoomKit.Requests adds the concrete pieces on top: DefaultRequestSender, the DI registration helpers, assembly-scanning handler discovery, and tracing.

This means a project that only needs to define requests/handlers — typically a domain/DDD class library that shouldn't know how requests get dispatched — can depend on LoomKit.Requests.Abstractions alone, keeping the concrete mediator implementation confined to your application/composition-root layer:

dotnet add package LoomKit.Requests.Abstractions   # domain layer: define IRequest/IRequestHandler
dotnet add package LoomKit.Requests                # application layer: wire up the sender

Installation

dotnet add package LoomKit.Requests

Available on nuget.org — a package version is published automatically for every vX.Y.Z tag pushed to this repo.

If you'd rather build against the source directly instead (e.g. to track main, or to debug/modify the library alongside your app), two options:

As a git submodule

git submodule add https://github.com/andreafoorlan/LoomKit.Requests.git external/LoomKit.Requests
cd external/LoomKit.Requests
git checkout v1.0.0
cd ../..
git add external/LoomKit.Requests
git commit -m "Add LoomKit.Requests submodule pinned to v1.0.0"

Then reference the project from your solution/project:

<ProjectReference Include="..\external\LoomKit.Requests\src\LoomKit.Requests.csproj" />

When cloning a repository that already has this submodule:

git clone --recurse-submodules <your-repo-url>
# or, on an existing clone:
git submodule update --init --recursive

To move to a newer release later:

cd external/LoomKit.Requests
git fetch --tags
git checkout v1.1.0
cd ../..
git add external/LoomKit.Requests
git commit -m "Bump LoomKit.Requests submodule to v1.1.0"

(git submodule add -b <tag> doesn't pin reliably since submodules track branches, not tags — checkout inside the submodule plus committing the resulting gitlink in the parent repo is what actually pins the commit.)

Plain project reference

If you're vendoring the source directly instead of using a submodule:

<ProjectReference Include="..\path\to\LoomKit.Requests\src\LoomKit.Requests.csproj" />

Core concepts

Type Purpose
IRequest Marker interface for a fire-and-forget request (no response).
IRequest<TResponse> Marker interface for a request that returns a TResponse.
IRequestHandler<TRequest> / IRequestHandler<TRequest, TResponse> Implement one per request type — this is where the actual logic lives.
IRequestSender The entry point your application code calls: SendAsync(...).
RequestMiddleware<TRequest> / RequestMiddleware<TRequest, TResponse> Optional cross-cutting behavior wrapped around a handler (logging, validation, retries, ...).

Quick start

1. Define requests and handlers

// Fire-and-forget
public sealed class PingCommand : IRequest;

public sealed class PingCommandHandler : IRequestHandler<PingCommand>
{
    public Task HandleAsync(PingCommand request, CancellationToken cancellationToken = default)
    {
        Console.WriteLine("Pong");
        return Task.CompletedTask;
    }
}

// Request/response
public sealed class GetUserQuery : IRequest<UserDto>
{
    public required Guid UserId { get; init; }
}

public sealed class GetUserQueryHandler : IRequestHandler<GetUserQuery, UserDto>
{
    private readonly IUserRepository _repository;

    public GetUserQueryHandler(IUserRepository repository) => _repository = repository;

    public async Task<UserDto> HandleAsync(GetUserQuery request, CancellationToken cancellationToken = default)
    {
        var user = await _repository.GetAsync(request.UserId, cancellationToken);
        return new UserDto(user.Id, user.Name);
    }
}

2. Register handlers and the sender in DI

Handlers are plain DI services. Register them one by one:

services.AddScoped<IRequestHandler<PingCommand>, PingCommandHandler>();
services.AddScoped<IRequestHandler<GetUserQuery, UserDto>, GetUserQueryHandler>();

services.AddDefaultRequestSender(options => { });

...or scan one or more assemblies for every closed IRequestHandler<>/IRequestHandler<,> implementation and register them all at once — no extra package required, this is built in:

services.AddRequestHandlersFromAssemblies(ServiceLifetime.Scoped, typeof(Program).Assembly);

// or across multiple assemblies, with whatever lifetime you need:
services.AddRequestHandlersFromAssemblies(ServiceLifetime.Scoped, typeof(Program).Assembly, typeof(SomeOtherModule).Assembly);

services.AddDefaultRequestSender(options => { });

Calling it more than once, or passing overlapping assemblies, won't produce duplicate registrations. Note it only picks up closed, concrete handler classes — an open-generic handler (e.g. class FallbackHandler<TRequest> : IRequestHandler<TRequest>) isn't discovered and must still be registered by hand: services.AddScoped(typeof(IRequestHandler<>), typeof(FallbackHandler<>));.

AddDefaultRequestSender registers IRequestSender (as ServiceLifetime.Scoped by default) backed by DefaultRequestSender.

3. Send requests

public sealed class SomeApplicationService(IRequestSender requestSender)
{
    public async Task RunAsync(Guid userId, CancellationToken cancellationToken)
    {
        await requestSender.SendAsync(new PingCommand(), cancellationToken);

        UserDto user = await requestSender.SendAsync(new GetUserQuery { UserId = userId }, cancellationToken);
    }
}

Middleware pipeline

A middleware wraps the next handler in the chain and decides whether/when to call it — same idea as ASP.NET Core middleware, but per request type.

You don't register middleware classes in DI. The pipeline constructs them directly via ActivatorUtilities, passing the next handler explicitly and resolving any other constructor parameter (like ILogger<> below) from the container. All you register in DI are the middleware's own dependencies, if any — the middleware type itself is only ever passed to UseRequestMiddleware/UseRequestResponseMiddleware, never to services.Add....

public sealed class LoggingMiddleware<TRequest> : RequestMiddleware<TRequest>
    where TRequest : IRequest
{
    private readonly ILogger<LoggingMiddleware<TRequest>> _logger;

    public LoggingMiddleware(IRequestHandler<TRequest> nextHandler, ILogger<LoggingMiddleware<TRequest>> logger)
        : base(nextHandler)
    {
        _logger = logger;
    }

    public override async Task HandleAsync(TRequest request, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Handling {RequestType}", typeof(TRequest).Name);

        await _nextHandler.HandleAsync(request, cancellationToken);

        _logger.LogInformation("Handled {RequestType}", typeof(TRequest).Name);
    }
}

Register it as an open generic type:

services.AddDefaultRequestSender(options => options
    .UseRequestMiddleware(typeof(LoggingMiddleware<>)));

The request/response variant works the same way, just implementing RequestMiddleware<TRequest, TResponse> and registering with UseRequestResponseMiddleware(typeof(YourMiddleware<,>)).

Execution order: middlewares run in the order they're registered — the first one registered is the outermost, so it runs first on the way in and last on the way out (a normal "onion" pipeline):

options
    .UseRequestMiddleware(typeof(LoggingMiddleware<>))     // runs 1st, then last
    .UseRequestMiddleware(typeof(ValidationMiddleware<>)); // runs 2nd, then first

ClearRequestMiddlewares() / ClearRequestResponseMiddlewares() reset the pipeline built so far if you need to override it conditionally.

Extensibility: custom senders

DefaultRequestSender is just the built-in implementation. If you need different behavior at the sender level (rather than as a middleware), derive from RequestSender<TOptions> and register it with the generic overload:

services.AddRequestSender<MyRequestSender, MyRequestSenderOptionsBuilder, MyRequestSenderOptions>(options => { });

Cancellation

CancellationToken passed to SendAsync flows through every middleware and reaches the handler's HandleAsync. Make sure any custom middleware you write forwards the token it receives to _nextHandler.HandleAsync(request, cancellationToken) instead of dropping it.

Observability

Every SendAsync call starts an Activity (named Send {RequestTypeName}) on an ActivitySource named after the assembly (LoomKit.Requests), tagged with request.type and, for request/response calls, response.type. If a handler or middleware throws, the exception is recorded on the activity via Activity.AddException (standard OpenTelemetry semantic conventions), including its message and stack trace.

⚠️ If your tracing backend doesn't have the same access controls as your application logs, avoid throwing exceptions from handlers whose Message carries secrets or personal data — they will flow into your trace exporter as-is.

License

MIT

Product Compatible and additional computed target framework versions.
.NET 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
10.0.2 180 8/25/2026
10.0.1 103 8/24/2026
1.0.1 127 8/20/2026 1.0.1 is deprecated.
1.0.0 124 8/20/2026 1.0.0 is deprecated.