Add Domain Entity
When the user asks to add a new entity, scaffold following the project's domain-driven design patterns.
If the entity involves non-trivial EF Core configuration (owned types, complex value conversions, table splitting), use context7 to look up the current EF Core docs for the specific pattern before writing the configuration.
Arguments
{Entity}-- Entity name in PascalCase (e.g.,Customer,Order){Service}-- Target microservice that owns this entity. Ask if ambiguous.
Configuration
Read cloudstack.json from the project root at the start of execution. Extract:
NAMESPACE=project.namespace(default: detect from*.slnname or first*.csprojroot namespace)DOMAIN_PATH=backend.sharedDomainPath(default: find directory matching**/Domain/containingEntities/)INFRA_PATH=backend.sharedInfraPath(default: find directory matching**/Shared.*Infrastructure/)SERVICES=backend.services[](default: discover fromsrc/*/directories containing.Application/subfolders)SOLUTION=backend.solutionPath(default: find*.slninsrc/)
If cloudstack.json does not exist, auto-detect by scanning the project structure.
Prerequisites
- The target microservice must exist under
src/with Infrastructure and Host projects - The shared Domain project must exist at
{DOMAIN_PATH} - EF Core tools must be installed (
dotnet tool list -g | grep ef)
1. Value Object ID ({DOMAIN_PATH}/ValueObjects/{Entity}Id.cs)
namespace {Namespace}.Domain.ValueObjects;
public class {Entity}Id : ValueObject
{
public Guid Value { get; init; }
public {Entity}Id(Guid value)
{
if (value == Guid.Empty)
throw new ArgumentException("{Entity}Id cannot be empty.", nameof(value));
Value = value;
}
public static {Entity}Id New() => new(Guid.NewGuid());
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Value;
}
}
2. Entity / Aggregate Root ({DOMAIN_PATH}/Entities/{Entity}.cs)
namespace {Namespace}.Domain.Entities;
public class {Entity} : AggregateRoot<{Entity}Id>
{
public TenantId TenantId { get; private set; }
// Add properties with private setters
// EF Core constructor
protected {Entity}() : base(default!) { }
// Private constructor called by factory method
private {Entity}({Entity}Id id, TenantId tenantId, ...) : base(id)
{
TenantId = tenantId;
// Set properties
}
// Factory method - the ONLY way to create instances
public static {Entity} Create(TenantId tenantId, ...)
{
var id = {Entity}Id.New();
var entity = new {Entity}(id, tenantId, ...);
entity.RaiseDomainEvent(new {Entity}CreatedEvent(id, tenantId, DateTimeOffset.UtcNow));
return entity;
}
// State-changing methods raise domain events
public void Update(...)
{
// Modify state
RaiseDomainEvent(new {Entity}UpdatedEvent(Id, ...));
}
}
3. Domain Event ({DOMAIN_PATH}/Events/{Entity}CreatedEvent.cs)
namespace {Namespace}.Domain.Events;
public class {Entity}CreatedEvent : DomainEvent
{
public {Entity}Id {Entity}Id { get; }
public TenantId TenantId { get; }
public DateTimeOffset OccurredAt { get; }
public {Entity}CreatedEvent({Entity}Id entityId, TenantId tenantId, DateTimeOffset occurredAt)
{
{Entity}Id = entityId;
TenantId = tenantId;
OccurredAt = occurredAt;
}
}
4. Repository Interface (Domain/ or Application/)
public interface I{Entity}Repository
{
Task<{Entity}?> GetByIdAsync({Entity}Id id, CancellationToken cancellationToken = default);
Task AddAsync({Entity} entity, CancellationToken cancellationToken = default);
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}
public interface I{Entity}QueryRepository
{
Task<{Entity}?> GetByIdAsync({Entity}Id id, CancellationToken cancellationToken = default);
Task<(IReadOnlyCollection<{Entity}> Items, int TotalCount)> ListAsync(TenantId tenantId, int page, int pageSize, CancellationToken cancellationToken = default);
}
5. EF Core Configuration (Infrastructure/Persistence/Configurations/{Entity}Configuration.cs)
public class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasConversion(id => id.Value, value => new {Entity}Id(value));
builder.Property(x => x.TenantId)
.HasConversion(id => id.Value, value => new TenantId(value));
// Configure other properties
}
}
6. Add DbSet to DbContext
public DbSet<{Entity}> {Entity}s => Set<{Entity}>();
7. EF Core Migration (MANDATORY)
After adding the DbSet and entity configuration, always create a migration immediately:
dotnet ef migrations add Add{Entity} \
--project src/{Service}/{Service}.Infrastructure \
--startup-project src/{Service}/{Service}.Host
Review the generated migration to verify only expected changes are included. This step is not optional -- skipping it causes PendingModelChangesWarning errors at startup.
Checklist
- Value object ID with validation and
GetEqualityComponents() - Entity inherits
AggregateRoot<TId>with protected parameterless constructor - Factory method is the only creation path
- Domain events raised in factory and state-change methods
- Past-tense event names:
{Entity}CreatedEvent,{Entity}UpdatedEvent - Separate command and query repository interfaces
- EF Core configuration with value object conversions
- DbSet added to DbContext
- EF Core migration created for the new table
- No framework dependencies in Domain layer
Output
After scaffolding, report:
## Scaffolded: {Entity}
Files created/modified:
- `{DOMAIN_PATH}/ValueObjects/{Entity}Id.cs`
- `{DOMAIN_PATH}/Entities/{Entity}.cs`
- `{DOMAIN_PATH}/Events/{Entity}CreatedEvent.cs`
- `src/{Service}/{Service}.Application/` -- Repository interfaces
- `src/{Service}/{Service}.Infrastructure/Persistence/Configurations/{Entity}Configuration.cs`
- `src/{Service}/{Service}.Infrastructure/Persistence/{Service}DbContext.cs` (modified)
- Migration: `src/{Service}/{Service}.Infrastructure/Persistence/Migrations/{timestamp}_Add{Entity}.cs`
Next: Run `/add-command` or `/add-query` to create endpoints for this entity.
Error Handling
- DbContext not found: Search for
*DbContext.csin the Infrastructure project. If none exists, the service may not have persistence set up yet -- warn the user. - EF Core tools not installed: Run
dotnet tool install --global dotnet-efand retry. - Migration fails: Check that the entity configuration is correct and the DbSet was added. Read the error output for hints.
- Value object ID already exists: The entity name may conflict with an existing one. Check
{DOMAIN_PATH}/ValueObjects/before creating.
Related Skills
/add-commandto create commands that operate on this entity/add-queryto create queries that read this entity/add-event-handlerto handle the domain events raised by this entity/add-migrationto create the EF Core migration for the new table