Add a domain entity/aggregate with EF configuration and a migration to an existing FSH module. Use when adding a new database-backed entity. Pairs with add-feature and create-migration.
69
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Rich domain model: sealed aggregate, private EF ctor, static factory, behavior via methods, domain
events. DB conventions: .agents/rules/database.md.
AggregateRoot<Guid> (or BaseEntity<Guid>)BaseEntity<TId> gives only Id + domain-event machinery. Audit/tenant/soft-delete are opt-in via
marker interfaces (the base does NOT carry those fields). New ids use Guid.CreateVersion7().
public sealed class {Entity} : AggregateRoot<Guid>, IHasTenant, IAuditableEntity, ISoftDeletable
{
public string Name { get; private set; } = default!;
public Money Price { get; private set; } = default!;
// IHasTenant
public string TenantId { get; private set; } = default!;
// IAuditableEntity
public DateTimeOffset CreatedOnUtc { get; set; }
public string? CreatedBy { get; set; }
public DateTimeOffset? LastModifiedOnUtc { get; set; }
public string? LastModifiedBy { get; set; }
// ISoftDeletable
public bool IsDeleted { get; set; }
public DateTimeOffset? DeletedOnUtc { get; set; }
public string? DeletedBy { get; set; }
private {Entity}() { } // EF
public static {Entity} Create(string name, Money price)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(price);
var entity = new {Entity} { Id = Guid.CreateVersion7(), Name = name.Trim(), Price = price };
entity.AddDomainEvent(DomainEvent.Create((id, ts) =>
new {Entity}CreatedDomainEvent(entity.Id, entity.Name, id, ts)));
return entity;
}
public void Rename(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
Name = name.Trim();
}
}Notes: setters are private set; TenantId/audit/soft-delete members are settable by the framework
(interceptor + Finbuckle) so they aren't private set. Use Guid.CreateVersion7(), never Guid.NewGuid().
DomainEvent (abstract record)public sealed record {Entity}CreatedDomainEvent(
Guid {Entity}Id, string Name, Guid EventId, DateTimeOffset OccurredOnUtc)
: DomainEvent(EventId, OccurredOnUtc);Raise with the DomainEvent.Create((id, ts) => …) helper + AddDomainEvent(...) (not QueueDomainEvent).
public sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.ToTable("{Entities}"); // schema is set once on the DbContext
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).IsRequired().HasMaxLength(200);
// soft-deletable unique field → filter on live rows only
builder.HasIndex(x => x.Name).IsUnique().HasFilter("\"IsDeleted\" = FALSE");
// owned value object
builder.OwnsOne(x => x.Price, m =>
{
m.Property(p => p.Amount).HasColumnName("PriceAmount").HasPrecision(18, 4);
m.Property(p => p.Currency).HasColumnName("PriceCurrency").HasMaxLength(3);
});
builder.Ignore(x => x.DomainEvents);
}
}HasQueryFilter for soft-delete or tenant — BaseDbContext applies both automatically.builder.Property(x => x.Id).ValueGeneratedNever() in its config, or EF inserts it as Modified → 0-row UPDATE. See database.md.Add a DbSet; configurations are picked up by ApplyConfigurationsFromAssembly:
public DbSet<{Entity}> {Entities} => Set<{Entity}>();The DbContext already extends BaseDbContext and calls base.OnModelCreating last — don't change that.
Use the create-migration skill (build first, correct --context):
dotnet ef migrations add Add{Entity} \
--project src/Host/FSH.Starter.Migrations.PostgreSQL \
--startup-project src/Host/FSH.Starter.Api \
--context {X}DbContextsealed, AggregateRoot<Guid> (+ IHasTenant/IAuditableEntity/ISoftDeletable as needed), private ctor, static Create using Guid.CreateVersion7()DomainEvent; raised via DomainEvent.Create + AddDomainEventValueGeneratedNever() on nav-collection childrenDbSet added; build green; migration created with --context {X}DbContext3f2959e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.