Minimal abstractions for building CQS-compliant applications. Contains ICommand, IQuery, ICriterion and builder interfaces for declarative handler invocation.
$ dotnet add package NPv.CQS.AbstractionsMinimal, dependency-free contracts for applications following the Command-Query Separation (CQS) pattern.
net10.0 (dropped net9.0 support).This is a personal library that focuses on the latest .NET runtime to keep maintenance simple and enjoyable.
NPv.CQS.Abstractions is a foundational library containing interfaces and contracts for building systems based on the Command‑Query Separation (CQS) principle.
It does not include any infrastructure or implementation — only the minimal building blocks that let infrastructure resolve and invoke handlers automatically.
Use this package in your domain, application, or core layers to define behaviors without pulling in any dependencies.
| Interface | Purpose |
|---|---|
ICommand<TContext> | Represents an action that changes system state and returns a Result |
ICommand<TContext, TResult> | Command that changes system state and returns a Result<TResult> |
ICommandContext | Marker type for command input/context |
ICommandContext<TResult> | Marker type for command input that implies a typed result |
ICommandBuilder | Resolves and executes a command for a given context (container-agnostic contract) |
ICommandExecutor | Higher-level orchestrator for executing commands (e.g., can be wrapped with Unit of Work in infrastructure), returns a Result |
CommandExecutorExtensions | Convenience extension methods to execute commands with or without results in a concise way |
| Interface | Purpose |
|---|---|
IQuery<TCriterion, TResult> | Read-only operation returning data for a specific criterion |
ICriterion | Input object used to filter or parameterize queries |
IQueryBuilder | Fluent entrypoint: ForAsync<TResult>() ... With<TCriterion>(criterion) |
IQueryFor<TResult> | Continuation interface returned by IQueryBuilder for a specific TResult |
Result or Result<TResult> instead of being void tasks.ICommandContext split into two flavors: ICommandContext (no result) and ICommandContext<TResult> (with typed result).public class CreateOrderCommand : ICommand<CreateOrderCommandContext>
{
public Task ExecuteAsync(CreateOrderCommandContext context)
{
// Domain logic
return Task.CompletedTask;
}
}public class CreateOrderCommand : ICommand<CreateOrderCommandContext>
{
public Task<Result> ExecuteAsync(CreateOrderCommandContext context)
{
if (context.Amount <= 0)
return Task.FromResult(Result.Failure(new Error("Validation.Invalid", "Amount")));
return Task.FromResult(Result.Success());
}
}Commands with results:
public record LoginUserCommandContext(string Email, string Password) : ICommandContext<LoginUserCommandResult>;
public record LoginUserCommandResult(string Email, string Token);
public class LoginUserCommand : ICommand<LoginUserCommandContext, LoginUserCommandResult>
{
public Task<Result<LoginUserCommandResult>> ExecuteAsync(LoginUserCommandContext context, CancellationToken ct = default)
{
if (context.Password != "123")
return Task.FromResult(Result<LoginUserCommandResult>.Failure(new Error("Auth.Invalid", "Bad password")));
return Task.FromResult(new LoginUserCommandResult(context.Email, "jwt-token"));
}
}dotnet add package NPv.CQS.Abstractionspublic record BanUserCommandContext(Guid UserId) : ICommandContext;
public class BanUserCommand : ICommand<BanUserCommandContext>
{
public Task<Result> ExecuteAsync(BanUserCommandContext context, CancellationToken ct = default)
{
// Domain logic (ban user)
return Task.FromResult(Result.Success());
}
}public record CreateUserContext(string Email, string Password) : ICommandContext<UserDto>;
public record UserDto(Guid Id, string Email);
public class CreateUserCommand : ICommand<CreateUserContext, UserDto>
{
public Task<Result<UserDto>> ExecuteAsync(CreateUserContext context, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(context.Email))
return Task.FromResult(Result<UserDto>.Failure(new Error("Validation.Invalid", "Email required")));
return Task.FromResult(new UserDto(Guid.NewGuid(), context.Email));
}
}public record FindById(Guid Id) : ICriterion;
public class FindOrderByIdQuery(IOrderService orders) : IQuery<FindById, Order>
{
public Task<Order> AskAsync(FindById criterion) =>
orders.GetAsync(criterion.Id);
}public class OrdersController(ICommandExecutor exec, IQueryBuilder queries)
{
public async Task<IActionResult> Create(Guid id, decimal amount)
{
var result = await exec.ExecuteAsync(new CreateOrderCommandContext(id, amount));
return result.IsSuccess ? Ok() : BadRequest(result.Errors);
}
public async Task<IActionResult> Login(string email, string password)
{
var result = await exec.ExecuteAsync<LoginUserCommandResult>(
new LoginUserCommandContext(email, password));
return result.IsSuccess ? Ok(result.Value) : Unauthorized(result.Errors);
}
public Task<Order> Get(Guid id) =>
queries.ForAsync<Order>().With(new FindById(id));
}Note:
ICommandExecutoris just a contract; infrastructure can provide an implementation that wraps execution in a Unit of Work (seeNPv.Uow.Abstractions) or adds logging/telemetry/retries.
NPv.CQS.Infrastructure – runtime execution (dispatch via DI, Autofac/MS.DI modules)NPv.Uow.Abstractions – optional Unit of Work contract for transactional executionMIT — you are free to use this in commercial and open-source software.