| name | domain-entity |
| description | How to model a new Domain entity in this repo — the base class, errors, domain events, and the matching EF configuration. Use when creating or modifying anything under src/Domain. |
Minimum files for a new entity
src/Domain/<Feature>/
<Entity>.cs
<Entity>Errors.cs
<Entity>CreatedDomainEvent.cs # whenever the lifecycle matters to other bounded-context code
src/Infrastructure/<Feature>/
<Entity>Configuration.cs # IEntityTypeConfiguration<T>
Entity
using SharedKernel;
namespace Domain.Todos;
public sealed class TodoItem : Entity
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Description { get; set; }
public DateTime? DueDate { get; set; }
public List<string> Labels { get; set; } = [];
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public Priority Priority { get; set; }
}
- Inherit
Entity from SharedKernel — gives you Raise(IDomainEvent) and event tracking.
sealed by default.
- This template intentionally uses public setters and plain POCO construction in handlers (see
CreateTodoCommandHandler). Don't introduce private setters / factory methods unless the user explicitly asks for stricter DDD.
- Primary key is a
Guid Id. Initialize in the handler (default = Guid.NewGuid() via new TodoItem() + EF or Guid.NewGuid() directly if you need it before insert).
Errors
using SharedKernel;
namespace Domain.Todos;
public static class TodoItemErrors
{
public static Error NotFound(Guid id) => Error.NotFound(
"TodoItems.NotFound", $"Todo item '{id}' was not found");
public static Error AlreadyCompleted(Guid id) => Error.Problem(
"TodoItems.AlreadyCompleted", $"Todo item '{id}' is already completed");
}
- Static class in
Domain/<Feature>/.
- Error code format:
<PluralEntity>.<Case>.
- Factory methods take the identifying value so handlers can write
TodoItemErrors.NotFound(id) cleanly.
Domain events
using SharedKernel;
namespace Domain.Todos;
public sealed record TodoItemCreatedDomainEvent(Guid TodoItemId) : IDomainEvent;
sealed record, implements IDomainEvent.
- Named
<Entity><PastTenseVerb>DomainEvent.
- Raised from the handler via
entity.Raise(new TodoItemCreatedDomainEvent(entity.Id)) after the state change, before SaveChangesAsync.
- Handlers live in
Application/<Feature>/<Name>DomainEventHandler.cs and implement IDomainEventHandler<TEvent>. They are dispatched automatically by DomainEventsDispatcher when ApplicationDbContext.SaveChangesAsync runs.
EF configuration
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Infrastructure.Todos;
internal sealed class TodoItemConfiguration : IEntityTypeConfiguration<TodoItem>
{
public void Configure(EntityTypeBuilder<TodoItem> builder)
{
builder.HasKey(t => t.Id);
builder.Property(t => t.DueDate).HasConversion(
d => d != null ? DateTime.SpecifyKind(d.Value, DateTimeKind.Utc) : d,
v => v);
builder.HasOne<User>().WithMany().HasForeignKey(t => t.UserId);
}
}
internal sealed, implements IEntityTypeConfiguration<T>.
- Lives in
src/Infrastructure/<Feature>/.
- Configurations are picked up by
ApplicationDbContext.OnModelCreating via ApplyConfigurationsFromAssembly.
- After adding a configuration and/or changing the entity, add an EF migration (see the
ef-migration-helper agent).
Then
- Expose the entity in
IApplicationDbContext as a DbSet<T> if handlers need to query it directly.
- Add a migration: see the
ef-migration-helper agent.
- If this entity's layering or shape must be enforced programmatically, add a test via the
architecture-test-writer agent.