cqrs-command-generator

CQRS Command Generator

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cqrs-command-generator" with this command: npx skills add ronnythedev/dotnet-clean-architecture-skills/ronnythedev-dotnet-clean-architecture-skills-cqrs-command-generator

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&#x3C;Result&#x3C;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&#x3C;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&#x3C;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&#x3C;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&#x3C;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&#x3C;Result&#x3C;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&#x3C;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 queries

  • domain-entity-generator

  • Generate domain entities with factory methods

  • result-pattern

  • Complete Result pattern implementation

  • pipeline-behaviors

  • Validation and logging behaviors

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

dotnet-clean-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

unit-testing

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

quartz-background-jobs

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

dapper-query-builder

No summary provided by upstream source.

Repository SourceNeeds Review