cqrs-command-generator
SKILL.md
CQRS Command Generator
Overview
This skill generates Commands following the CQRS (Command Query Responsibility Segregation) pattern. Commands represent intentions to change system state. Each command has:
- Command Record - Immutable data structure with request parameters
- Validator - FluentValidation rules for input validation
- Handler - Business logic implementation returning Result
- Request DTO (optional) - API layer request model
Quick Reference
| Command Type | Returns | Use Case |
|---|---|---|
ICommand |
Result |
Operations without return value (Update, Delete) |
ICommand<T> |
Result<T> |
Operations returning data (Create returns Id) |
Command Structure
/Application/{Feature}/
├── Create{Entity}/
│ ├── Create{Entity}Command.cs # Record + Validator + Handler
│ └── Create{Entity}Request.cs # Optional API DTO
├── Update{Entity}/
│ ├── Update{Entity}Command.cs
│ └── Update{Entity}Request.cs
└── Delete{Entity}/
└── Delete{Entity}Command.cs
Template: Command with Return Value (Create)
Use for operations that return data (typically entity ID after creation).
// src/{name}.application/{Feature}/Create{Entity}/Create{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.clock;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
using {name}.domain.{entities};
namespace {name}.application.{feature}.Create{Entity};
// ═══════════════════════════════════════════════════════════════
// COMMAND RECORD
// ═══════════════════════════════════════════════════════════════
public sealed record Create{Entity}Command(
string Name,
string? Description,
Guid? ParentId) : ICommand<Guid>;
// ═══════════════════════════════════════════════════════════════
// VALIDATOR
// ═══════════════════════════════════════════════════════════════
internal sealed class Create{Entity}CommandValidator : AbstractValidator<Create{Entity}Command>
{
public Create{Entity}CommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("{Entity} name is required")
.MaximumLength(100)
.WithMessage("{Entity} name must not exceed 100 characters");
RuleFor(x => x.Description)
.MaximumLength(500)
.When(x => x.Description is not null);
}
}
// ═══════════════════════════════════════════════════════════════
// HANDLER
// ═══════════════════════════════════════════════════════════════
internal sealed class Create{Entity}CommandHandler
: ICommandHandler<Create{Entity}Command, Guid>
{
private readonly I{Entity}Repository _{entity}Repository;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly IUnitOfWork _unitOfWork;
public Create{Entity}CommandHandler(
I{Entity}Repository {entity}Repository,
IDateTimeProvider dateTimeProvider,
IUnitOfWork unitOfWork)
{
_{entity}Repository = {entity}Repository;
_dateTimeProvider = dateTimeProvider;
_unitOfWork = unitOfWork;
}
public async Task<Result<Guid>> Handle(
Create{Entity}Command request,
CancellationToken cancellationToken)
{
// 1. Validate business rules
var existingEntity = await _{entity}Repository
.GetByNameAsync(request.Name, cancellationToken);
if (existingEntity is not null)
{
return Result.Failure<Guid>({Entity}Errors.AlreadyExists);
}
// 2. Create domain entity using factory method
var {entity}Result = {Entity}.Create(
request.Name,
request.Description,
_dateTimeProvider.UtcNow);
if ({entity}Result.IsFailure)
{
return Result.Failure<Guid>({entity}Result.Error);
}
// 3. Persist to repository
_{entity}Repository.Add({entity}Result.Value);
// 4. Save changes (via Unit of Work)
await _unitOfWork.SaveChangesAsync(cancellationToken);
// 5. Return created entity ID
return {entity}Result.Value.Id;
}
}
Template: Command without Return Value (Update)
Use for operations that don't return data.
// src/{name}.application/{Feature}/Update{Entity}/Update{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.clock;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
using {name}.domain.{entities};
namespace {name}.application.{feature}.Update{Entity};
// ═══════════════════════════════════════════════════════════════
// COMMAND RECORD
// ═══════════════════════════════════════════════════════════════
public sealed record Update{Entity}Command(
Guid Id,
string Name,
string? Description) : ICommand;
// ═══════════════════════════════════════════════════════════════
// VALIDATOR
// ═══════════════════════════════════════════════════════════════
internal sealed class Update{Entity}CommandValidator : AbstractValidator<Update{Entity}Command>
{
public Update{Entity}CommandValidator()
{
RuleFor(x => x.Id)
.NotEmpty()
.WithMessage("{Entity} ID is required");
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(100);
}
}
// ═══════════════════════════════════════════════════════════════
// HANDLER
// ═══════════════════════════════════════════════════════════════
internal sealed class Update{Entity}CommandHandler
: ICommandHandler<Update{Entity}Command>
{
private readonly I{Entity}Repository _{entity}Repository;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly IUnitOfWork _unitOfWork;
public Update{Entity}CommandHandler(
I{Entity}Repository {entity}Repository,
IDateTimeProvider dateTimeProvider,
IUnitOfWork unitOfWork)
{
_{entity}Repository = {entity}Repository;
_dateTimeProvider = dateTimeProvider;
_unitOfWork = unitOfWork;
}
public async Task<r> Handle(
Update{Entity}Command request,
CancellationToken cancellationToken)
{
// 1. Retrieve existing entity
var {entity} = await _{entity}Repository
.GetByIdAsync(request.Id, cancellationToken);
if ({entity} is null)
{
return Result.Failure({Entity}Errors.NotFound);
}
// 2. Call domain method to update
var updateResult = {entity}.Update(
request.Name,
request.Description,
_dateTimeProvider.UtcNow);
if (updateResult.IsFailure)
{
return Result.Failure(updateResult.Error);
}
// 3. Save changes
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success();
}
}
Template: Delete Command
// src/{name}.application/{Feature}/Delete{Entity}/Delete{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
using {name}.domain.{entities};
namespace {name}.application.{feature}.Delete{Entity};
public sealed record Delete{Entity}Command(Guid Id) : ICommand;
internal sealed class Delete{Entity}CommandValidator : AbstractValidator<Delete{Entity}Command>
{
public Delete{Entity}CommandValidator()
{
RuleFor(x => x.Id).NotEmpty();
}
}
internal sealed class Delete{Entity}CommandHandler
: ICommandHandler<Delete{Entity}Command>
{
private readonly I{Entity}Repository _{entity}Repository;
private readonly IUnitOfWork _unitOfWork;
public Delete{Entity}CommandHandler(
I{Entity}Repository {entity}Repository,
IUnitOfWork unitOfWork)
{
_{entity}Repository = {entity}Repository;
_unitOfWork = unitOfWork;
}
public async Task<r> Handle(
Delete{Entity}Command request,
CancellationToken cancellationToken)
{
var {entity} = await _{entity}Repository
.GetByIdAsync(request.Id, cancellationToken);
if ({entity} is null)
{
return Result.Failure({Entity}Errors.NotFound);
}
// Check business rules before deletion
if ({entity}.HasActiveRelationships())
{
return Result.Failure({Entity}Errors.CannotDeleteWithActiveRelationships);
}
_{entity}Repository.Remove({entity});
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success();
}
}
Template: Command with Complex Request Object
For commands with many parameters, use a nested request object.
// src/{name}.application/{Feature}/Create{Entity}/Create{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
namespace {name}.application.{feature}.Create{Entity};
// Request object for complex data
public sealed class Create{Entity}Request
{
public required string Name { get; init; }
public string? Description { get; init; }
public required Guid OrganizationId { get; init; }
public List<Create{Child}Request> Children { get; init; } = new();
}
public sealed class Create{Child}Request
{
public required string Name { get; init; }
public int SortOrder { get; init; }
}
// Command wraps the request
public sealed record Create{Entity}Command(
Create{Entity}Request Request) : ICommand<Guid>;
// Validator for nested structures
internal sealed class Create{Entity}CommandValidator
: AbstractValidator<Create{Entity}Command>
{
public Create{Entity}CommandValidator()
{
RuleFor(x => x.Request.Name)
.NotEmpty()
.MaximumLength(100);
RuleFor(x => x.Request.OrganizationId)
.NotEmpty();
RuleForEach(x => x.Request.Children)
.ChildRules(child =>
{
child.RuleFor(c => c.Name).NotEmpty();
child.RuleFor(c => c.SortOrder).GreaterThanOrEqualTo(0);
});
}
}
// Handler processes the complex request
internal sealed class Create{Entity}CommandHandler
: ICommandHandler<Create{Entity}Command, Guid>
{
// ... dependencies
public async Task<Result<Guid>> Handle(
Create{Entity}Command command,
CancellationToken cancellationToken)
{
var request = command.Request;
// Process parent entity
var {entity} = {Entity}.Create(
request.Name,
request.Description,
request.OrganizationId);
// Process children
foreach (var childRequest in request.Children)
{
var child = {Child}.Create(
childRequest.Name,
childRequest.SortOrder);
{entity}.AddChild(child);
}
_{entity}Repository.Add({entity});
await _unitOfWork.SaveChangesAsync(cancellationToken);
return {entity}.Id;
}
}
Validation Rules Reference
Common Validators
// String validations
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.NotNull().WithMessage("Name cannot be null")
.MaximumLength(100).WithMessage("Name too long")
.MinimumLength(3).WithMessage("Name too short")
.Matches("^[a-zA-Z]+$").WithMessage("Only letters allowed");
// Numeric validations
RuleFor(x => x.Amount)
.GreaterThan(0).WithMessage("Must be positive")
.LessThanOrEqualTo(1000).WithMessage("Max 1000")
.InclusiveBetween(1, 100).WithMessage("Must be 1-100");
// GUID validations
RuleFor(x => x.Id)
.NotEmpty().WithMessage("ID is required")
.NotEqual(Guid.Empty).WithMessage("Invalid ID");
// Email validation
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress().WithMessage("Invalid email format");
// Conditional validation
RuleFor(x => x.ParentId)
.NotEmpty()
.When(x => x.RequiresParent);
// Collection validation
RuleFor(x => x.Items)
.NotEmpty().WithMessage("At least one item required")
.Must(items => items.Count <= 10).WithMessage("Max 10 items");
// Custom validation
RuleFor(x => x.DateRange)
.Must(BeValidDateRange).WithMessage("End date must be after start date");
private bool BeValidDateRange(DateRange range) => range.End > range.Start;
Async Validation (Use Sparingly)
internal sealed class Create{Entity}CommandValidator
: AbstractValidator<Create{Entity}Command>
{
private readonly I{Entity}Repository _{entity}Repository;
public Create{Entity}CommandValidator(I{Entity}Repository {entity}Repository)
{
_{entity}Repository = {entity}Repository;
RuleFor(x => x.Name)
.MustAsync(BeUniqueName)
.WithMessage("Name already exists");
}
private async Task<bool> BeUniqueName(string name, CancellationToken ct)
{
var existing = await _{entity}Repository.GetByNameAsync(name, ct);
return existing is null;
}
}
Note: Prefer doing existence checks in the Handler rather than Validator for better separation of concerns.
Handler Patterns
Pattern 1: Single Entity Operation
public async Task<Result<Guid>> Handle(CreateCommand request, CancellationToken ct)
{
// Create
var entity = Entity.Create(request.Data);
_repository.Add(entity);
await _unitOfWork.SaveChangesAsync(ct);
return entity.Id;
}
Pattern 2: With Related Entities
public async Task<Result<Guid>> Handle(CreateCommand request, CancellationToken ct)
{
// Load related entity
var parent = await _parentRepository.GetByIdAsync(request.ParentId, ct);
if (parent is null)
return Result.Failure<Guid>(ParentErrors.NotFound);
// Create with relationship
var entity = Entity.Create(request.Data, parent);
_repository.Add(entity);
await _unitOfWork.SaveChangesAsync(ct);
return entity.Id;
}
Pattern 3: Batch Operations
public async Task<r> Handle(CreateBatchCommand request, CancellationToken ct)
{
var entities = new List<Entity>();
foreach (var item in request.Items)
{
var entityResult = Entity.Create(item);
if (entityResult.IsFailure)
return Result.Failure(entityResult.Error);
entities.Add(entityResult.Value);
}
_repository.AddRange(entities);
await _unitOfWork.SaveChangesAsync(ct);
return Result.Success();
}
Pattern 4: With Transaction
public async Task<r> Handle(ComplexCommand request, CancellationToken ct)
{
using var transaction = await _unitOfWork.BeginTransactionAsync(ct);
try
{
// Multiple operations
var entity1 = await CreateEntity1(request);
var entity2 = await CreateEntity2(request, entity1);
await _unitOfWork.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
return Result.Success();
}
catch
{
await transaction.RollbackAsync(ct);
throw;
}
}
Critical Rules
- Commands are records - Immutable, value equality
- One handler per command - No shared handlers
- Validators are internal - Not exposed outside Application layer
- Use Result pattern - Never throw exceptions for business errors
- Inject IUnitOfWork - Don't call SaveChanges in repository
- Always use CancellationToken - Pass through all async calls
- Domain logic in Domain - Handler orchestrates, doesn't contain business rules
- Return IDs from Create - Use
ICommand<Guid>for creation - Validate in order - Check existence before creating, then validate business rules
- Keep handlers focused - One responsibility per handler
Anti-Patterns to Avoid
// ❌ WRONG: Throwing exceptions for business errors
if (entity is null)
throw new NotFoundException("Entity not found");
// ✅ CORRECT: Return Result
if (entity is null)
return Result.Failure<Guid>(EntityErrors.NotFound);
// ❌ WRONG: Saving in repository
public void Add(Entity entity)
{
_dbContext.Add(entity);
_dbContext.SaveChanges(); // Don't do this!
}
// ✅ CORRECT: Handler calls SaveChanges via UnitOfWork
_repository.Add(entity);
await _unitOfWork.SaveChangesAsync(ct);
// ❌ WRONG: Business logic in handler
if (request.Amount > 1000 && user.Level < 5)
return Result.Failure(Error.InsufficientLevel);
// ✅ CORRECT: Business logic in domain
var result = entity.ProcessOrder(request.Amount, user);
if (result.IsFailure)
return Result.Failure(result.Error);
Related Skills
cqrs-query-generator- Generate read-side queriesdomain-entity-generator- Generate domain entities with factory methodsresult-pattern- Complete Result pattern implementationpipeline-behaviors- Validation and logging behaviors
Weekly Installs
4
Repository
ronnythedev/dot…e-skillsGitHub Stars
42
First Seen
Mar 1, 2026
Security Audits
Installed on
opencode4
gemini-cli4
github-copilot4
codex4
kimi-cli4
amp4