Skip to main content

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

OptionDescriptionExample
NameAggregate nameEquipment, Order, User
--propertiesProperties with types--properties "Name:string,Count:int"

After Creating Aggregate

  1. Add business logic - Implement domain methods and invariants
  2. Create entity configurations - Configure EF Core mappings
  3. 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

OptionDescriptionExample
NameEntity nameAddress, LineItem
--aggregateParent aggregate--aggregate Customer
--propertiesEntity properties--properties "Street:string"
--collectionMake 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

OptionDescriptionExample
NameCommand nameCreateEquipment, ApproveOrder
--aggregateTarget aggregate--aggregate Equipment
--propertiesCommand parameters--properties "Name:string,Count:int"
--resultReturn type--result OrderApprovedResult
--no-authorizerSkip 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

OptionDescriptionExample
NameQuery nameGetEquipmentById, ListEquipment
--propertiesQuery parameters--properties "Id:Guid"
--returnsReturn type--returns EquipmentDto
--no-authorizerSkip 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

  1. Create database view - Add view migration for optimal queries
  2. Add to ReadModels service - Register the projection in module read models
  3. 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

OptionDescriptionExample
NameEvent nameEquipmentCreated, OrderPlaced
--aggregateRelated aggregate--aggregate Equipment
--propertiesEvent data--properties "Id:Guid,Name:string"
--no-handlerSkip 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

OptionDescriptionExample
NameEvent nameEquipmentCreated, OrderPlaced
--aggregateRelated aggregate--aggregate Equipment
--propertiesEvent data--properties "Id:Guid"
--handler-projectCross-module handler path--handler-project ../Notifications
--no-handlerSkip 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