Domain Commands
Scaffold domain-driven design components: aggregates, entities, commands, queries, projections, and events.
peardrop add aggregate
Create a new domain aggregate with its folder structure.
peardrop add aggregate Equipment \
--properties "Name:string,IsAvailable:bool,Category:string"
Creates:
Infrastructure/Domain/EquipmentAggregate/
├── AggregateRoot/
│ └── Equipment Aggregate.cs # Your aggregate class
├── CommandHandlers/ # Command handlers go here
├── Commands/ # Commands go here
├── CommandValidators/ # FluentValidation validators
├── CommandAuthorizers/ # Authorization logic
├── DomainEvents/ # Domain events
├── EventHandlers/ # Domain event handlers
└── IntegrationEvents/ # Integration events
Generated aggregate:
public class EquipmentAggregate : AggregateRoot
{
public string Name { get; private set; }
public bool IsAvailable { get; private set; }
public string Category { get; private set; }
// Static factory method
public static EquipmentAggregate Create(string name, bool isAvailable, string category)
{
return new EquipmentAggregate
{
Id = Guid.NewGuid(),
Name = name,
IsAvailable = isAvailable,
Category = category
};
}
// Add business methods here
}
Options
| Option | Description | Example |
|---|---|---|
| Name | Aggregate name | Equipment, Order, User |
--properties | Properties with types | --properties "Name:string,Count:int" |
After Creating Aggregate
- Add business logic - Implement domain methods and invariants
- Create entity configurations - Configure EF Core mappings
- Run migrations -
dotnet ef migrations add CreateEquipment
peardrop add entity
Create an entity under an aggregate root.
# Single entity
peardrop add entity Address --aggregate Customer \
--properties "Street:string,City:string,PostalCode:string"
# Collection entity
peardrop add entity LineItem --aggregate Order \
--properties "ProductId:Guid,Quantity:int,Price:decimal" \
--collection
Creates:
Infrastructure/Domain/CustomerAggregate/
└── AggregateRoot/
├── CustomerAggregate.cs
└── Address.cs # New entity class
Generated entity:
public class Address
{
public string Street { get; private set; }
public string City { get; private set; }
public string PostalCode { get; private set; }
public static Address Create(string street, string city, string postalCode)
{
return new Address
{
Street = street,
City = city,
PostalCode = postalCode
};
}
}
Updates aggregate:
public class CustomerAggregate : AggregateRoot
{
// Existing properties...
// Single entity
public Address Address { get; private set; }
// Or collection (with --collection flag)
private readonly List<LineItem> _lineItems = new();
public IReadOnlyCollection<LineItem> LineItems => _lineItems.AsReadOnly();
}
Options
| Option | Description | Example |
|---|---|---|
| Name | Entity name | Address, LineItem |
--aggregate | Parent aggregate | --aggregate Customer |
--properties | Entity properties | --properties "Street:string" |
--collection | Make it a collection | --collection |
peardrop add command
Create a command with handler, validator, and authorizer.
# Simple command
peardrop add command CreateEquipment \
--aggregate Equipment \
--properties "Name:string,Category:string"
# Command with result type
peardrop add command ApproveOrder \
--aggregate Order \
--properties "OrderId:Guid,ApprovedBy:string" \
--result OrderApprovedResult
# Command without authorizer
peardrop add command UpdateEquipment \
--aggregate Equipment \
--properties "EquipmentId:Guid,NewName:string" \
--no-authorizer
Creates:
Infrastructure/Domain/EquipmentAggregate/
├── Commands/
│ └── CreateEquipmentCommand.cs
├── CommandHandlers/
│ └── CreateEquipmentCommandHandler.cs
├── CommandValidators/
│ └── CreateEquipmentCommandValidator.cs
└── CommandAuthorizers/
└── CreateEquipmentCommandAuthorizer.cs
Generated command (positional syntax):
public sealed record CreateEquipmentCommand(
string Name,
string Category) : ICommand;
Generated handler:
internal sealed class CreateEquipmentCommandHandler :
AuditableCommandHandler<CreateEquipmentCommand, Unit, EquipmentAggregate>
{
protected override async Task<CommandResult<Unit>> HandleInternalWithRepository(
CreateEquipmentCommand request,
CancellationToken cancellationToken)
{
// TODO: Implement business logic
var equipment = EquipmentAggregate.Create(request.Name, request.Category);
this.Repository.Add(equipment);
var result = await this.Repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return result.IsSuccess
? CommandResult<Unit>.Succeeded(Unit.Value)
: CommandResult<Unit>.Failed(result.Error!);
}
}
Options
| Option | Description | Example |
|---|---|---|
| Name | Command name | CreateEquipment, ApproveOrder |
--aggregate | Target aggregate | --aggregate Equipment |
--properties | Command parameters | --properties "Name:string,Count:int" |
--result | Return type | --result OrderApprovedResult |
--no-authorizer | Skip authorizer | --no-authorizer |
peardrop add query
Create a query with handler and authorizer.
# Query returning single item
peardrop add query GetEquipmentById \
--properties "EquipmentId:Guid" \
--returns EquipmentDetailDto
# Query returning list
peardrop add query ListEquipment \
--returns "List<EquipmentSummaryDto>"
# Query without authorizer
peardrop add query SearchEquipment \
--properties "SearchTerm:string" \
--returns "List<EquipmentDto>" \
--no-authorizer
Creates:
Queries/
├── GetEquipmentByIdQuery.cs
├── QueryHandlers/
│ └── GetEquipmentByIdQueryHandler.cs
└── QueryAuthorizers/
└── GetEquipmentByIdQueryAuthorizer.cs
Generated query:
public sealed record GetEquipmentByIdQuery(
Guid EquipmentId) : IQuery;
public sealed record GetEquipmentByIdQueryResult(
EquipmentDetailDto Equipment);
Generated handler:
internal sealed class GetEquipmentByIdQueryHandler :
IQueryProcessor<GetEquipmentByIdQuery, GetEquipmentByIdQueryResult>
{
private readonly IEquipmentReadModels readModels;
public async Task<QueryResult<GetEquipmentByIdQueryResult>> Handle(
GetEquipmentByIdQuery request,
CancellationToken cancellationToken)
{
// TODO: Implement query logic
var equipment = await readModels.Equipment
.Where(e => e.Id == request.EquipmentId)
.FirstOrDefaultAsync(cancellationToken);
if (equipment == null)
{
return QueryResult<GetEquipmentByIdQueryResult>.Failed();
}
return QueryResult<GetEquipmentByIdQueryResult>.Succeeded(
new GetEquipmentByIdQueryResult(new EquipmentDetailDto(...)));
}
}
Options
| Option | Description | Example |
|---|---|---|
| Name | Query name | GetEquipmentById, ListEquipment |
--properties | Query parameters | --properties "Id:Guid" |
--returns | Return type | --returns EquipmentDto |
--no-authorizer | Skip authorizer | --no-authorizer |
peardrop add projection
Create a read model projection (for query optimization).
peardrop add projection Equipment
Creates:
Data/ReadModel/
├── Projections/
│ └── EquipmentProjection.cs
└── EntityConfigs/
└── EquipmentProjectionTypeConfiguration.cs
Generated projection:
public sealed class EquipmentProjection : IProjection
{
public required Guid Id { get; init; }
public required string Name { get; init; }
public required bool IsAvailable { get; init; }
public required string Category { get; init; }
}
Generated configuration:
public class EquipmentProjectionTypeConfiguration :
ProjectionTypeConfigurationBase<EquipmentProjection>,
IProjectionTypeConfiguration
{
public override void Configure(EntityTypeBuilder<EquipmentProjection> builder)
{
builder.ToView("vw_equipment");
builder.HasKey(e => e.Id);
builder.Property(e => e.Name).IsRequired();
builder.Property(e => e.Category).IsRequired();
}
}
After Creating Projection
- Create database view - Add view migration for optimal queries
- Add to ReadModels service - Register the projection in module read models
- Use in queries - Query handler accesses via
IModuleReadModels
peardrop add domain-event
Create a domain event with optional handler.
# Event with handler
peardrop add domain-event EquipmentCreated \
--aggregate Equipment \
--properties "EquipmentId:Guid,Name:string,Category:string"
# Event without handler
peardrop add domain-event EquipmentDeleted \
--aggregate Equipment \
--properties "EquipmentId:Guid" \
--no-handler
Creates:
Infrastructure/Domain/EquipmentAggregate/
├── DomainEvents/
│ └── EquipmentCreatedDomainEvent.cs
└── EventHandlers/
└── EquipmentCreatedDomainEventHandler.cs
Generated event:
public sealed record EquipmentCreatedDomainEvent(
Guid EquipmentId,
string Name,
string Category) : IDomainEvent;
Use in aggregate:
public class EquipmentAggregate : AggregateRoot
{
public static EquipmentAggregate Create(string name, string category)
{
var equipment = new EquipmentAggregate
{
Id = Guid.NewGuid(),
Name = name,
Category = category
};
// Raise domain event
equipment.AddDomainEvent(
new EquipmentCreatedDomainEvent(equipment.Id, name, category));
return equipment;
}
}
Options
| Option | Description | Example |
|---|---|---|
| Name | Event name | EquipmentCreated, OrderPlaced |
--aggregate | Related aggregate | --aggregate Equipment |
--properties | Event data | --properties "Id:Guid,Name:string" |
--no-handler | Skip handler | --no-handler |
peardrop add integration-event
Create an integration event with CAP subscriber.
# Event with handler
peardrop add integration-event EquipmentCreated \
--aggregate Equipment \
--properties "EquipmentId:Guid,Name:string,Category:string"
# Cross-library handler
peardrop add integration-event UserCreated \
--aggregate User \
--handler-project ../Notification.Module
# Event without handler
peardrop add integration-event UserDeleted \
--aggregate User \
--no-handler
Creates:
Infrastructure/Domain/EquipmentAggregate/
└── IntegrationEvents/
└── EquipmentCreatedIntegrationEvent.cs
Infrastructure/EventHandlers/
└── EquipmentCreatedSubscriber.cs
Constants/
└── EquipmentCapTopics.cs # Auto-updated
Generated event:
public sealed record EquipmentCreatedIntegrationEvent(
Guid EquipmentId,
string Name,
string Category) : IIntegrationEvent
{
public string EventName => EquipmentCapTopics.EquipmentCreated;
}
Generated subscriber:
public sealed class EquipmentCreatedSubscriber : ICapSubscribe
{
[CapSubscribe(EquipmentCapTopics.EquipmentCreated)]
public async Task Handle(
EquipmentCreatedIntegrationEvent @event,
CancellationToken cancellationToken)
{
// TODO: Handle event
_logger.LogInformation(
"Equipment created: {EquipmentId}", @event.EquipmentId);
}
}
Options
| Option | Description | Example |
|---|---|---|
| Name | Event name | EquipmentCreated, OrderPlaced |
--aggregate | Related aggregate | --aggregate Equipment |
--properties | Event data | --properties "Id:Guid" |
--handler-project | Cross-module handler path | --handler-project ../Notifications |
--no-handler | Skip subscriber | --no-handler |
Complete Workflow Example
Build an Equipment Checkout feature step-by-step:
# 1. Create aggregates
peardrop add aggregate Equipment \
--properties "Name:string,Category:string,IsAvailable:bool"
peardrop add aggregate CheckoutRequest \
--properties "EquipmentId:Guid,UserId:Guid,Status:CheckoutStatus"
# 2. Create commands
peardrop add command CreateEquipment \
--aggregate Equipment \
--properties "Name:string,Category:string"
peardrop add command RequestCheckout \
--aggregate CheckoutRequest \
--properties "EquipmentId:Guid"
peardrop add command ApproveCheckout \
--aggregate CheckoutRequest \
--properties "CheckoutRequestId:Guid"
# 3. Create queries
peardrop add query ListAvailableEquipment \
--returns "List<EquipmentDto>"
peardrop add query GetMyCheckouts \
--returns "List<CheckoutDto>"
# 4. Create projections
peardrop add projection Equipment
peardrop add projection CheckoutRequest
# 5. Create integration events
peardrop add integration-event CheckoutApproved \
--aggregate CheckoutRequest \
--properties "CheckoutId:Guid,EquipmentId:Guid,UserId:Guid"
# 6. Implement business logic in generated files
# ... edit aggregates, handlers, validators ...
# 7. Create migrations
dotnet ef migrations add AddEquipmentCheckout
# 8. Apply to database
peardrop migrate
Next Steps
- Feature Commands - Add auth or modules
- Utility Commands - Migrations and maintenance
- CQRS Operations Guide - Best practices for commands/queries