Help us improve
Share bugs, ideas, or general feedback.
From dotnet-clean-architecture-skills
Generates CQRS command records, handlers, validators, and request DTOs for .NET applications following Clean Architecture patterns.
npx claudepluginhub ronnythedev/dotnet-clean-architecture-skillsHow this skill is triggered β by the user, by Claude, or both
Slash command
/dotnet-clean-architecture-skills:02-dotnet-cqrs-command-generatorThe summary Claude sees in its skill listing β used to decide when to auto-load this skill
This skill generates Commands following the CQRS (Command Query Responsibility Segregation) pattern. Commands represent intentions to change system state. Each command has:
Generates FluentValidation validators for commands and queries in .NET, with built-in rules, custom validators, async validation, and MediatR pipeline behavior integration.
Generates CQRS commands, handlers, and unit tests for PHP 8.4 apps. Creates immutable self-validating DTOs and handlers that modify aggregates via repositories and events.
Scaffolds .NET 10 features, entities, and tests with EF Core config, vertical slices, FluentValidation, Result pattern, and integration tests matching project architecture like VSA or DDD.
Share bugs, ideas, or general feedback.
This skill generates Commands following the CQRS (Command Query Responsibility Segregation) pattern. Commands represent intentions to change system state. Each command has:
| Command Type | Returns | Use Case |
|---|---|---|
ICommand | Result | Operations without return value (Update, Delete) |
ICommand<T> | Result<T> | Operations returning data (Create returns Id) |
/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
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;
}
}
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();
}
}
// 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();
}
}
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;
}
}
// 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;
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.
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;
}
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;
}
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();
}
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;
}
}
ICommand<Guid> for creation// β 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);
dotnet-cqrs-query-generator - Generate read-side queriesdotnet-domain-entity-generator - Generate domain entities with factory methodsdotnet-result-pattern - Complete Result pattern implementationdotnet-pipeline-behaviors - Validation and logging behaviors