Skip to main content

add-entity

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.

Source facts

Repository
fullstackhero/dotnet-starter-kit
Last source activity
November 3, 2026 at 16:59
Detected SKILL.md language
English
Stars
6,860
Forks
2,018

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
add-entity
description
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.
argument-hint
[ModuleName] [EntityName]
# Add Entity Rich domain model: `sealed` aggregate, private EF ctor, static factory, behavior via methods, domain events. DB conventions: `.agents/rules/database.md`. ## Entity — `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()`**. ```csharp 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()`. ## Business-rule violations — localized `CustomException` `ArgumentException` guards are for programming errors (they surface as a 500). A rule a client can break (an illegal state transition, a duplicate) throws a `CustomException` with the matching status, English text as the message (for logs) and the key attached with `.Localized`: ```csharp public void Archive() { if (Status == {Entity}Status.Archived) { throw new CustomException( $"{Entity} {Id} is already archived.", errors: null, HttpStatusCode.Conflict) .Localized<{X}Resources>("{X}.{Entity}AlreadyArchived", Id); } Status = {Entity}Status.Archived; } ``` Add the key in English to `src/Modules/{X}/Modules.{X}/Localization/{X}Resources.resx` (`{0}` for each argument, in order); the pt-BR entry is optional. Enum arguments are translated when the catalog has a `{EnumType}.{Member}` entry. `LocalizedKeyReferenceTests` fails the build on a missing key. Rules: `.agents/rules/localization.md`. ## Domain event — inherit `DomainEvent` (abstract record) ```csharp 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`). ## EF configuration ```csharp 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); } } ``` - **Do NOT add a manual `HasQueryFilter` for soft-delete or tenant** — `BaseDbContext` applies both automatically. - A child entity reached only via a parent nav-collection needs `builder.Property(x => x.Id).ValueGeneratedNever()` in **its** config, or EF inserts it as `Modified` → 0-row UPDATE. See `database.md`. ## Register in the module DbContext Add a `DbSet`; configurations are picked up by `ApplyConfigurationsFromAssembly`: ```csharp public DbSet<{Entity}> {Entities} => Set<{Entity}>(); ``` The DbContext already extends `BaseDbContext` and calls `base.OnModelCreating` **last** — don't change that. ## Migration Use the **create-migration** skill (build first, correct `--context`): ```bash dotnet ef migrations add Add{Entity} \ --project src/Host/FSH.Starter.Migrations.PostgreSQL \ --startup-project src/Host/FSH.Starter.Api \ --context {X}DbContext ``` ## Checklist - [ ] `sealed`, `AggregateRoot<Guid>` (+ `IHasTenant`/`IAuditableEntity`/`ISoftDeletable` as needed), private ctor, static `Create` using `Guid.CreateVersion7()` - [ ] Domain event inherits `DomainEvent`; raised via `DomainEvent.Create` + `AddDomainEvent` - [ ] Client-breakable rules throw `CustomException(...).Localized<{X}Resources>("…")`; keys added in English to `{X}Resources.resx` - [ ] EF config: no manual soft-delete/tenant filter; `ValueGeneratedNever()` on nav-collection children - [ ] `DbSet` added; build green; migration created with `--context {X}DbContext`
View on GitHub