Create DDD domain entities following this project's AggregateRoot/DomainEntity inheritance pattern. Use when adding a new domain entity or owned type to Minimal.Domains.
Create domain entities that integrate with this project's DDD infrastructure — AggregateRoot, DomainEntity, and owned value objects.
Before starting, gather:
Order, InvoiceOrders, InvoicesDomainSchemas"ord"AuditedEntity<Guid> ← from DKNet.EfCore.Abstractions.Entities
└── DomainEntity ← Minimal.Domains.Share (abstract, Guid Id + audit)
└── AggregateRoot ← Minimal.Domains.Share (abstract, Guid auto-gen)
AggregateRoot (or DomainEntity for non-root entities){ get; private set; } — mutation happens ONLY through named methodsUpdate(...) method handles mutable fieldsSetCreatedBy(userId) and SetUpdatedBy(userId) are inherited from AuditedEntityAddEvent(...) to publish domain events (inherited from base)Guid.Empty for Id means "let the database generate it"src/ApiEndpoints/Minimal.Domains/
├── Features/
│ └── {Feature}/
│ └── Entities/
│ ├── {Entity}.cs ← Aggregate root
│ ├── {OwnedType}.cs ← Owned value objects (optional)
│ └── {ChildEntity}.cs ← Non-root entities (optional)
├── Services/
│ ├── I{Service}.cs ← Domain service interfaces
│ └── IDomainService.cs ← Marker interface
└── Share/
├── AggregateRoot.cs ← DO NOT MODIFY
├── DomainEntity.cs ← DO NOT MODIFY
├── DomainSchemas.cs ← ADD your schema constant here
└── Sequences.cs ← ADD sequence name if needed
Edit src/ApiEndpoints/Minimal.Domains/Share/DomainSchemas.cs:
public static class DomainSchemas
{
public const string Migration = "migrate";
public const string Profile = "pro";
public const string {Feature} = "{prefix}"; // ← ADD THIS
}
Create src/ApiEndpoints/Minimal.Domains/Features/{Feature}/Entities/{Entity}.cs:
using Minimal.Domains.Share;
namespace Minimal.Domains.Features.{Feature}.Entities;
/// <summary>
/// {Description of the aggregate root}.
/// </summary>
public class {Entity} : AggregateRoot
{
#region Constructors
/// <summary>
/// Creates a new {Entity} with a system-assigned identity.
/// </summary>
public {Entity}(
{constructor params for immutable + mutable fields},
string byUser)
: this(Guid.Empty, {forward all params}, byUser)
{
}
/// <summary>
/// Rehydrates an existing {Entity} from persistence.
/// </summary>
internal {Entity}(
Guid id,
{all params},
string createdBy)
: base(id, createdBy)
{
// Set immutable properties
{ImmutableProp} = {value};
// Delegate mutable fields to Update
Update({mutable params}, createdBy);
}
#endregion
#region Properties
// Immutable properties (set only in constructor)
public string {ImmutableProp} { get; private set; }
// Mutable properties (changed via Update method)
public string? {MutableProp} { get; private set; }
#endregion
#region Methods
/// <summary>
/// Updates mutable fields. Null/empty values are ignored (preserves current).
/// </summary>
public void Update({mutable params}, string userId)
{
if (!string.IsNullOrEmpty({param}))
{
{MutableProp} = {param};
}
SetUpdatedBy(userId);
}
#endregion
}
For complex nested types that don't have their own identity:
namespace Minimal.Domains.Features.{Feature}.Entities;
/// <summary>
/// {Description} — owned value object, no independent identity.
/// </summary>
public class {OwnedType}
{
public string {Prop1} { get; set; } = default!;
public string? {Prop2} { get; set; }
}
If the entity needs external ID generation or cross-aggregate lookups:
namespace Minimal.Domains.Services;
public interface I{Service} : IDomainService
{
Task<string> NextValueAsync();
}
Edit src/ApiEndpoints/Minimal.Domains/Share/Sequences.cs to add sequence name:
public static class Sequences
{
public const string {Entity}Seq = "{entity}_seq";
}
public class CustomerProfile : AggregateRoot
{
// Constructor: new entity
public CustomerProfile(string name, string membershipNo, string email, string phone, string byUser)
: this(Guid.Empty, name, membershipNo, email, phone, byUser) { }
// Constructor: rehydration
internal CustomerProfile(Guid id, string name, string membershipNo, string email, string phone, string createdBy)
: base(id, createdBy)
{
Name = name;
Email = email;
MembershipNo = membershipNo;
Update(null, name, phone, null, createdBy);
}
// Immutable
public string Email { get; private set; }
public string MembershipNo { get; private set; }
// Mutable
public string Name { get; private set; }
public string? Phone { get; private set; }
public string? Avatar { get; private set; }
public DateTime? BirthDay { get; private set; }
public void Update(string? avatar, string? name, string? phoneNumber, DateTime? birthday, string userId)
{
Avatar = avatar;
BirthDay = birthday;
if (!string.IsNullOrEmpty(name)) Name = name;
if (!string.IsNullOrEmpty(phoneNumber)) Phone = phoneNumber;
SetUpdatedBy(userId);
}
}
AggregateRoot (not sealed, not using required keyword){ get; private set; } — no public settersstring byUser as last param; passes to base(id, createdBy)Guid.Empty for new entitiesUpdate(...) method calls SetUpdatedBy(userId) at the endDomainSchemas.csMinimal.Domains.Features.{Feature}.Entitiessrc/ApiEndpoints/Minimal.Domains/Features/{Feature}/Entities/IDomainService (if applicable)dotnet build src/DKNet.Templates.sln -c Release passes with zero warnings| Mistake | Fix |
|---|---|
Making entity sealed | Remove sealed — entities inherit from AggregateRoot |
Using required keyword on properties | Use { get; private set; } — values set in constructor |
| Public setters on properties | Make setters private set — mutate via methods only |
Missing SetUpdatedBy() in Update | Always call at end of mutation methods |
Using DateTime.UtcNow directly | Audit timestamps handled by AuditedEntity base class |
Forgetting internal on rehydration constructor | Mark it internal — only infra should call it |
After creating the domain entity, proceed to: → dknet-efcore-config skill to create the EF Core mapper configuration