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.
Fluents.Requests.* marks a write, Fluents.Queries.*
marks a read — no attributes, no naming conventions to remember.FluentResults types, so “product not
found” is a value the caller inspects rather than a thrown exception to catch.SaveChangesAsync call. AddSlimBusEfCoreInterceptor<TDbContext>() saves the
DbContext for you after a successful write, and AddSlimBusEventPublisher<TDbContext>() forwards the domain
events that save raised onto the bus.AddSlimMessageBus(...) and pick the provider (memory, Azure Service Bus, Kafka) yourself; the Fluents contracts
and your handlers stay the same either way.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.
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.
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.
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.
Fluents.Requests.INoResponse / IWitResponse<T>
and implement their handlers.Fluents.Queries.IWitResponse<T> /
IWitPageResponse<T> and implement their handlers.Fluents.EventsConsumers.IHandler<TEvent> to react to a
message on the bus.AddSlimBusEfCoreInterceptor<TDbContext>()
so a successful write saves the DbContext for you.AddSlimBusEventPublisher<TDbContext>()
so events raised by DKNet.EfCore.Events reach IMessageBus.ILazyMap<T> and IMapper.ResultOf<T>.Fluents interfaces instead of hand-writing them.Fluents requests to
minimal-API endpoints and turn their results into HTTP responses.AddSlimBusEventPublisher<TDbContext>() forwards onto the bus.AddNewEntitiesFromNavigations and
SaveChangesWithConcurrencyHandlingAsync, the save primitives the auto-save interceptor calls.AddDbContextWithHook<TDbContext>(), the save-hook
pipeline that domain-event publishing depends on.X.PagedList.EF
dependency) to build the paged, specification-driven queries a Fluents.Queries.IPageHandler<,> returns.