Kmd 0.4.0
dotnet add package Kmd --version 0.4.0
NuGet\Install-Package Kmd -Version 0.4.0
<PackageReference Include="Kmd" Version="0.4.0" />
<PackageVersion Include="Kmd" Version="0.4.0" />
<PackageReference Include="Kmd" />
paket add Kmd --version 0.4.0
#r "nuget: Kmd, 0.4.0"
#:package Kmd@0.4.0
#addin nuget:?package=Kmd&version=0.4.0
#tool nuget:?package=Kmd&version=0.4.0
Kmd
A lightweight, extensible CQRS library for .NET with built-in interceptor pipeline and OpenTelemetry support.
Packages
| Package | Description |
|---|---|
Kmd.Abstractions |
Core interfaces (commands, queries, interceptors) |
Kmd |
Default implementations (buses, pipeline, dispatcher) |
Kmd.AspNetCore |
ASP.NET Core dependency injection integration |
Getting Started
Installation
Install the packages you need via the .NET CLI:
dotnet add package Kmd.Abstractions
dotnet add package Kmd
dotnet add package Kmd.AspNetCore
Registration
Register Kmd in your Program.cs using AddKmd:
builder.Services.AddKmd(kmdBuilder => {
// Register global interceptors (applied to all commands and queries)
kmdBuilder
.AddInterceptor(typeof(LoggingInterceptor<,>))
.AddInterceptor(typeof(TimingInterceptor<,>));
// Auto-discover and register all handlers in the given assemblies
kmdBuilder
.AddCommandsFromAssemblies(typeof(Program).Assembly)
.AddQueriesFromAssemblies(typeof(Program).Assembly);
});
Commands
Commands represent operations that change state. Inherit from Command and implement ICommandHandler<TCommand, TResult>.
Defining a Command
sealed record CreateOrderCommand(string ProductName, int Quantity) : Command;
Implementing a Handler
sealed class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, string>
{
public Task<string> HandleAsync(ICommandContext<CreateOrderCommand> context,
CancellationToken cancellationToken = default)
{
var command = context.Command;
// Handle the command...
return Task.FromResult($"Order created for {command.ProductName}");
}
}
Dispatching a Command
Inject ICommandBus<TCommand, TResult> and call SendAsync:
app.MapPost("/orders", async ([FromServices] ICommandBus<CreateOrderCommand, string> commandBus) => {
return await commandBus.SendAsync(new CreateOrderCommand("Widget", 5));
});
Queries
Queries represent read operations that return data without modifying state. Inherit from Query and implement IQueryHandler<TQuery, TResult>.
Defining a Query
sealed record GetOrderQuery(string OrderId) : Query;
Implementing a Handler
sealed class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, string>
{
public Task<string> HandleAsync(IQueryContext<GetOrderQuery> context,
CancellationToken cancellationToken = default)
{
return Task.FromResult($"Order {context.Query.OrderId}");
}
}
Dispatching a Query
Inject IQueryBus<TQuery, TResult> and call SendAsync:
app.MapGet("/orders/{id}", async ([FromServices] IQueryBus<GetOrderQuery, string> queryBus, string id) => {
return await queryBus.SendAsync(new GetOrderQuery(id));
});
Interceptors
Interceptors implement cross-cutting concerns such as logging, timing, validation, and exception handling. They form a middleware pipeline that wraps every command or query execution.
Global Interceptors
A global interceptor is applied to all commands and queries. It must be an open generic type implementing IInterceptor<TRequest, TResult>:
sealed class LoggingInterceptor<TRequest, TResult>(ILogger<LoggingInterceptor<TRequest, TResult>>? logger = null)
: IInterceptor<TRequest, TResult> where TRequest : IRequest
{
public async Task<TResult> InterceptAsync(IRequestContext<TRequest> context,
Func<IRequestContext<TRequest>, Task<TResult>> next,
CancellationToken cancellationToken = default)
{
logger?.LogInformation("Handling {RequestId}", context.Request.RequestId);
var result = await next(context);
logger?.LogInformation("Handled {RequestId}", context.Request.RequestId);
return result;
}
}
Register global interceptors using the open generic type:
kmdBuilder.AddInterceptor(typeof(LoggingInterceptor<,>));
Type-Specific Interceptors
An interceptor can also target a specific command or query type:
sealed class ValidateOrderInterceptor : IInterceptor<CreateOrderCommand, string>
{
public async Task<string> InterceptAsync(IRequestContext<CreateOrderCommand> context,
Func<IRequestContext<CreateOrderCommand>, Task<string>> next,
CancellationToken cancellationToken = default)
{
if (context.Request.Quantity <= 0)
throw new ArgumentException("Quantity must be greater than zero.");
return await next(context);
}
}
Register using the typed overload:
kmdBuilder.AddInterceptor<CreateOrderCommand, string, ValidateOrderInterceptor>();
Interceptor Ordering
Interceptors are executed in the order they are registered. The first interceptor registered wraps the outermost layer of the pipeline.
Command-Only and Query-Only Interceptors
For finer-grained control, you can implement ICommandInterceptor<TCommand, TResult> or IQueryInterceptor<TQuery, TResult> and register them with:
kmdBuilder.AddCommandInterceptor(typeof(MyCommandInterceptor<,>));
kmdBuilder.AddQueryInterceptor(typeof(MyQueryInterceptor<,>));
OpenTelemetry Integration
Kmd exposes an ActivitySource and a Meter for distributed tracing and metrics. Both are identified by the name "Kmd", available via KmdConstants:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource(KmdConstants.ActivitySourceName)
// ...
)
.WithMetrics(metrics => metrics
.AddMeter(KmdConstants.MeterName)
// ...
);
You can start custom activities inside handlers or interceptors using the activity source:
using var activity = Observability.ActivitySource.StartActivity("MyOperation");
Shared Request Context
Each command or query handler receives a context object that exposes the request and a shared Items dictionary. Use Items to pass data between interceptors and handlers in the same pipeline execution:
public async Task<TResult> InterceptAsync(IRequestContext<TRequest> context, ...)
{
context.Items["startTime"] = DateTime.UtcNow;
return await next(context);
}
Supported Frameworks
- .NET 8.0
- .NET 9.0
- .NET 10.0
| 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
- Kmd.Abstractions (>= 0.4.0)
-
net8.0
- Kmd.Abstractions (>= 0.4.0)
-
net9.0
- Kmd.Abstractions (>= 0.4.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Kmd:
| Package | Downloads |
|---|---|
|
Kmd.AspNetCore
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.