DKNet

DKNet.SlimBus.Extensions

CQRS and messaging glue on top of SlimMessageBus — fluent command, query and event-consumer contracts, FluentResults-based handlers, lazy Mapster mapping, and an opt-in EF Core auto-save and domain-event publisher for hosts that use a DbContext.

✨ Why use it?

This package already depends on EF Core: it references X.PagedList.EF (paged query results) and DKNet.EfCore.Events (the domain-event publisher). If your handlers never touch a DbContext, you can still install it and simply not call the two EF Core registration methods.

🚀 Quick Start

Use a .NET 10 console project. Install the packages, then replace Program.cs with the example.

dotnet new console -f net10.0 -n SlimBusDemo
cd SlimBusDemo
dotnet add package DKNet.SlimBus.Extensions
dotnet add package SlimMessageBus.Host.Memory --version 3.5.0
using DKNet.SlimBus.Extensions;
using FluentResults;
using Microsoft.Extensions.DependencyInjection;
using SlimMessageBus;
using SlimMessageBus.Host;
using SlimMessageBus.Host.Memory;

var services = new ServiceCollection();
services.AddLogging();
services.AddSlimMessageBus(mbb => mbb
    .AddServicesFromAssembly(typeof(Program).Assembly)
    .AddChildBus("Memory", m => m
        .WithProviderMemory()
        .AutoDeclareFrom(typeof(Program).Assembly)));

await using var provider = services.BuildServiceProvider();
var bus = provider.GetRequiredService<IMessageBus>();

var result = await bus.Send(new Ping("hello"));
Console.WriteLine(result.IsSuccess ? result.Value : "failed");

public record Ping(string Text) : Fluents.Requests.IWitResponse<string>;

internal sealed class PingHandler : Fluents.Requests.IHandler<Ping, string>
{
    public Task<IResult<string>> OnHandle(Ping request, CancellationToken cancellationToken) =>
        Task.FromResult<IResult<string>>(Result.Ok($"pong: {request.Text}"));
}

Run with dotnet run. Prints pong: hello.

The package brings no transport of its own — SlimMessageBus.Host.Memory above is the in-process provider for local development and tests. For a real deployment, swap it for the Azure Service Bus, Kafka, or another SlimMessageBus provider package and pass it to the same AddChildBus(...) call; the Fluents contracts and your handlers never change. No serializer is needed for the memory provider. AutoDeclareFrom declares the message types on that child bus — leaving it out makes Send return null without ever reaching your handler. AddServicesFromAssembly registers your handler types in DI. Both come from SlimMessageBus’s own SlimMessageBus.Host / SlimMessageBus.Host.Memory packages, not from this one.

🏗️ Architecture

The host registers IMessageBus and its provider; SlimMessageBus dispatches to your handlers, which implement the Fluents contracts and may use LazyMapper. AddSlimBusEfCoreInterceptor and AddSlimBusEventPublisher are an optional second row that wraps your handlers to save a DbContext and publish its domain events.

The top row — IMessageBus, the Fluents contracts, your handlers, and LazyMapper — needs no DbContext. The bottom row is opt-in: nothing wires it up unless the host calls AddSlimBusEfCoreInterceptor<TDbContext>() and/or AddSlimBusEventPublisher<TDbContext>(). The diagram cannot show registration order or DI lifetimes; see Auto-save after a successful write and Publish domain events onto the bus for those.

🔄 How it works

One write request: the caller sends it through IMessageBus, your own interceptors run first, auto-save wraps the handler last (Order = int.MaxValue), the handler loads and changes entities without calling SaveChangesAsync, and on success auto-save saves the DbContext, which publishes any raised events through SlimBusEventPublisher back onto IMessageBus before the response returns to the caller.

This is the write path with both EF Core add-ons registered — the pipeline a plain Fluents.Requests write handler sees once the host calls AddSlimBusEfCoreInterceptor<TDbContext>() and AddSlimBusEventPublisher<TDbContext>(). Without those two calls, Send goes straight from your interceptors to the handler and back, with no save and no event publish. A query (Fluents.Queries.*) never enters the auto-save branch at all, whatever it leaves in the change tracker — see Run read queries.

🧩 Features