FlintSoft.CQRS
0.1.24
dotnet add package FlintSoft.CQRS --version 0.1.24
NuGet\Install-Package FlintSoft.CQRS -Version 0.1.24
<PackageReference Include="FlintSoft.CQRS" Version="0.1.24" />
<PackageVersion Include="FlintSoft.CQRS" Version="0.1.24" />
<PackageReference Include="FlintSoft.CQRS" />
paket add FlintSoft.CQRS --version 0.1.24
#r "nuget: FlintSoft.CQRS, 0.1.24"
#:package FlintSoft.CQRS@0.1.24
#addin nuget:?package=FlintSoft.CQRS&version=0.1.24
#tool nuget:?package=FlintSoft.CQRS&version=0.1.24
FlintSoft.CQRS
A lightweight, opinionated .NET 9 library that provides a clean implementation of the CQRS (Command Query Responsibility Segregation) pattern with Domain-Driven Design (DDD) support.
Features
- Commands & Queries — clear separation of write and read operations
- ErrorOr integration — railway-oriented error handling; no exceptions thrown by default
- Domain Events — raise and dispatch events from your entities with multiple handler support
- Automatic handler discovery — uses Scrutor to scan and register all handlers in a given assembly
- Cross-cutting decorators — built-in logging and exception-handling decorators applied transparently to every handler
Installation
dotnet add package FlintSoft.CQRS
Quick Start
1. Register the library
Call AddFlintSoftCQRS on your IHostApplicationBuilder, passing any type from the assembly that contains your handlers:
var host = Host.CreateApplicationBuilder(args);
host.AddFlintSoftCQRS(typeof(Program));
var app = host.Build();
This single call:
- Scans the assembly for all
ICommandHandler<>andIQueryHandler<,>implementations and registers them as scoped services - Applies the logging and exception-handling decorators automatically
- Registers
IDomainEventDispatcheras a transient service
2. Commands
A command represents an intent to change state. Implement ICommand<TResponse> for commands that return a value, or ICommand for fire-and-forget commands.
using FlintSoft.CQRS.Interfaces;
using FlintSoft.CQRS.Handlers;
using ErrorOr;
// Define the command
public record CreateUserCommand(string UserName, string Email) : ICommand<Guid>;
// Implement the handler
public sealed class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, Guid>
{
public async Task<ErrorOr<Guid>> Handle(CreateUserCommand command, CancellationToken cancellationToken)
{
// ... create the user and persist it ...
return await Task.FromResult(Guid.NewGuid());
}
}
Execute the command by resolving ICommandHandler<TCommand, TResponse> from the DI container:
var handler = serviceProvider.GetRequiredService<ICommandHandler<CreateUserCommand, Guid>>();
var result = await handler.Handle(new CreateUserCommand("john", "john@example.com"), cancellationToken);
if (result.IsError)
Console.WriteLine(result.Errors.First().Description);
else
Console.WriteLine($"Created user {result.Value}");
Fire-and-forget command (no return value)
public record DeleteUserCommand(Guid UserId) : ICommand;
public sealed class DeleteUserCommandHandler : ICommandHandler<DeleteUserCommand>
{
public async Task<ErrorOr<Success>> Handle(DeleteUserCommand command, CancellationToken cancellationToken)
{
// ... delete the user ...
return await Task.FromResult(Result.Success);
}
}
3. Queries
A query retrieves data without changing state. Implement IQuery<TResponse>.
using FlintSoft.CQRS.Interfaces;
using FlintSoft.CQRS.Handlers;
using ErrorOr;
// Define the query
public record GetUserNameQuery(Guid UserId) : IQuery<string>;
// Implement the handler
public sealed class GetUserNameQueryHandler : IQueryHandler<GetUserNameQuery, string>
{
public Task<ErrorOr<string>> Handle(GetUserNameQuery query, CancellationToken cancellationToken)
{
// ... fetch the user name ...
return Task.FromResult<ErrorOr<string>>("John Doe");
}
}
Execute the query by resolving IQueryHandler<TQuery, TResponse>:
var handler = serviceProvider.GetRequiredService<IQueryHandler<GetUserNameQuery, string>>();
var result = await handler.Handle(new GetUserNameQuery(userId), cancellationToken);
Console.WriteLine(result.IsError ? result.Errors.First().Description : result.Value);
4. Domain Events
Domain events allow entities to broadcast side effects in a loosely coupled way.
4.1 Make your entity inherit DomainEventEntityBase
using FlintSoft.CQRS;
public class User : DomainEventEntityBase
{
public Guid Id { get; }
public string UserName { get; }
public User(Guid id, string userName)
{
Id = id;
UserName = userName;
}
public static User Create(string userName)
{
var user = new User(Guid.NewGuid(), userName);
user.RaiseDomainEvent(new UserCreatedDomainEvent(user.Id));
return user;
}
}
4.2 Define a domain event
using FlintSoft.CQRS.Events;
public sealed record UserCreatedDomainEvent(Guid UserId) : IDomainEvent;
4.3 Implement one or more event handlers
Multiple handlers for the same event are all invoked by the dispatcher:
using FlintSoft.CQRS.Events;
public sealed class SendWelcomeEmailHandler : IDomainEventHandler<UserCreatedDomainEvent>
{
public Task Handle(UserCreatedDomainEvent domainEvent, CancellationToken cancellationToken)
{
Console.WriteLine($"Sending welcome email for user {domainEvent.UserId}");
return Task.CompletedTask;
}
}
public sealed class AuditUserCreatedHandler : IDomainEventHandler<UserCreatedDomainEvent>
{
public Task Handle(UserCreatedDomainEvent domainEvent, CancellationToken cancellationToken)
{
Console.WriteLine($"Auditing creation of user {domainEvent.UserId}");
return Task.CompletedTask;
}
}
4.4 Dispatch the events
After executing a command, retrieve the events collected on the entity, dispatch them, and then clear the list:
var dispatcher = serviceProvider.GetRequiredService<IDomainEventDispatcher>();
// dispatch all events raised during command execution
await dispatcher.DispatchAsync(user.DomainEvents, cancellationToken);
// clear the events once dispatched
user.ClearDomainEvents();
5. Cross-cutting decorators
The following decorators are applied automatically to every command and query handler when you call AddFlintSoftCQRS:
| Decorator | Behavior |
|---|---|
LoggingDecorator |
Logs start and completion (success or error) of every handler using ILogger |
ExceptionDecorator |
Catches any unhandled exception and converts it to an ErrorOr failure, so callers never receive an unhandled exception |
No additional configuration is required.
Sample Application
A fully-working demo project can be found in src/Demo. It demonstrates:
- Registering
FlintSoft.CQRSwithAddFlintSoftCQRS - A
CreateUsercommand with a result type - A
GetUserNamequery - A
UserCreatedDomainEventwith two handlers (send email + audit log) - Dispatching domain events and clearing them after the command
// src/Demo/Program.cs
var host = Host.CreateApplicationBuilder(args);
host.AddFlintSoftCQRS(typeof(Program));
var app = host.Build();
var sp = app.Services;
var queryHandler = sp.GetRequiredService<IQueryHandler<GetUserName.Query, string>>();
var commandHandler = sp.GetRequiredService<ICommandHandler<CreateUser.Command, User>>();
var dispatcher = sp.GetRequiredService<IDomainEventDispatcher>();
var queryResult = await queryHandler.Handle(new GetUserName.Query(), CancellationToken.None);
Console.WriteLine(queryResult.IsError ? queryResult.Errors.First().Description : queryResult.Value);
var commandResult = await commandHandler.Handle(
new CreateUser.Command("uname", "geheim", "u@u.com", "John", "Doe"),
CancellationToken.None);
Console.WriteLine(commandResult.IsError ? commandResult.Errors.First().Description : commandResult.Value);
await dispatcher.DispatchAsync(commandResult.Value.DomainEvents);
commandResult.Value.ClearDomainEvents();
await app.StartAsync();
Dependencies
| Package | Version | Purpose |
|---|---|---|
| ErrorOr | 2.0.1 | Railway-oriented error handling |
| Scrutor | 7.0.0 | Assembly scanning & decorator registration |
| Microsoft.Extensions.Hosting.Abstractions | 9.0.5 | IHostApplicationBuilder DI integration |
Target Framework
.NET 9.0
License
See LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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 was computed. 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. |
-
net9.0
- ErrorOr (>= 2.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.5)
- Scrutor (>= 7.0.0)
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 |
|---|---|---|
| 0.1.24 | 195 | 3/26/2026 |
| 0.1.23 | 115 | 3/26/2026 |
| 0.1.22 | 117 | 3/26/2026 |
| 0.1.21 | 640 | 9/6/2025 |
| 0.1.20 | 252 | 6/17/2025 |
| 0.1.19 | 250 | 5/22/2025 |
| 0.1.17 | 229 | 5/21/2025 |
| 0.1.16 | 242 | 5/16/2025 |
| 0.1.15 | 242 | 5/16/2025 |
| 0.1.14 | 260 | 4/20/2025 |
| 0.1.13 | 247 | 4/20/2025 |
| 0.1.12 | 245 | 4/20/2025 |
| 0.1.11 | 265 | 4/20/2025 |
| 0.1.6 | 273 | 4/20/2025 |
| 0.0.30 | 282 | 6/16/2026 |