| name | drn-domain-design |
| description | Domain-Driven Design implementation patterns - Entity design with Source-Known IDs (SourceKnownEntity, AggregateRoot), repository contracts (ISourceKnownRepository), domain events, EF Core configuration, DTO mapping rules, and implementation templates. Essential for domain modeling and data access. Keywords: ddd, entity-design, aggregate-root, source-known-id, repository, domain-events, ef-core-configuration, dto-mapping, entity-type, identity |
| last-updated | "2026-06-12T00:00:00.000Z" |
| difficulty | advanced |
| tokens | ~2.9K |
Domain Design Patterns
Entity design, repository implementation, and DDD patterns for DRN applications.
When to Apply
- Creating domain entities & aggregate roots
- Implementing repositories
- Working with Source-Known IDs
- Configuring EF Core entity types
- Mapping entities to DTOs
Entity Design
All entities inherit from SourceKnownEntity. See drn-sharedkernel for base class details and identity struct definitions.
Creating an Entity
[EntityType((byte)1)]
public class User : SourceKnownEntity
{
private User() { }
public User(string username, string email) : base()
{
Username = username;
Email = email;
}
public string Username { get; private set; }
public string Email { get; private set; }
public void UpdateEmail(string newEmail)
{
Email = newEmail;
}
}
Key Rules
[EntityType(byte)] is required — DrnContext validates at startup
- Private parameterless constructor for EF Core materialization
- Domain logic lives on the entity, not in services
- Never expose setters publicly — use methods with domain validation
JSON Model Entities
For entities with rich document models, use AggregateRoot<TModel>:
[EntityType((byte)3)]
public class SystemSettings : AggregateRoot<SettingsModel> { }
public class SettingsModel
{
public string Theme { get; set; }
public int MaxRetries { get; set; }
}
EF Core Configuration
[!TIP]
Design Preference: Prefer Attribute-based configuration over Fluent API when available. Use Fluent API only for complex definitions that cannot be elegantly expressed with attributes (e.g., composite keys, complex many-to-many relationships, or conditional mapping).
DrnContext auto-discovers IEntityTypeConfiguration<T> from the context's namespace:
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasKey(u => u.Id);
builder.Property(u => u.Username).HasMaxLength(100);
}
}
[!CAUTION]
Navigation Configuration Ordering: Navigation-level configurations (e.g., AutoInclude()) must be placed after relationship definitions in Fluent API. Placing them before silently fails. See dotnet/efcore#31380.
builder.Navigation(x => x.Books).AutoInclude();
builder.HasMany(x => x.Books).WithMany();
builder.HasMany(x => x.Books).WithMany();
builder.Navigation(x => x.Books).AutoInclude();
Conventions Applied by DrnContext
- Auto ID generation via
IDrnSaveChangesInterceptor
EntityIdSource initialization via IDrnMaterializationInterceptor
EntityId (Guid) and EntityIdSource are not mapped to DB columns
IEntityWithModel<T> auto-maps .Model to jsonb
[!TIP]
Prototype Mode: Set UsePrototypeMode = true on configuration attribute for ephemeral testcontainer-based development — auto-handles DB creation and migrations. See drn-entityframework.
Repository Implementation
Canonical contract in drn-sharedkernel. Implementation in drn-entityframework.
IEntityUtils
IEntityUtils provides scoped utilities available in repositories via base class:
| Property | Type | Purpose |
|---|
Id | ISourceKnownIdUtils | Generate internal long IDs |
EntityId | ISourceKnownEntityIdUtils | Generate/parse/validate external GUIDs |
Cancellation | ICancellationUtils | Token management and merging |
Pagination | IPaginationUtils | Pagination logic helpers |
DateTime | IDateTimeUtils | Time-aware operations |
ScopedLog | IScopedLog | Integrated logging |
Custom Repository with Behavior Override
public interface IUserRepository : ISourceKnownRepository<User> { }
[Scoped<IUserRepository>]
public class UserRepository : SourceKnownRepository<QAContext, User>, IUserRepository
{
protected override IQueryable<User> EntitiesWithAppliedSettings =>
base.EntitiesWithAppliedSettings
.Include(u => u.Profile)
.ThenInclude(p => p.Address);
}
[!TIP]
Always use EntitiesWithAppliedSettings() when building custom queries (joins, complex filters) to ensure repository settings are respected.
ID Validation at Repository Boundary
var entityId = userRepository.GetEntityId(externalGuid);
var user = await userRepository.GetAsync(entityId);
Contract Layer
[!IMPORTANT]
The *.Contract project is the shared boundary between layers. It holds DTOs, shared enums, and value models — anything consumed across Domain, Application, Infrastructure, or Presentation.
Dependency Rule
- Contract depends only on
DRN.Framework.SharedKernel
- Any project (including
*.Domain) may reference *.Contract
What Belongs in Contract
| Place Here | Never Place Here |
|---|
DTOs (Dto subclasses) | Entities |
| Shared enums (source of truth for DTOs & entities) | Repository interfaces |
Value models (e.g., TagValueModel) | Domain services |
| Shared constants / display types | Business logic |
Enum Placement Rule
Enums that are shared between DTOs and entities (e.g., TagType) must be defined in the Contract project as the single source of truth — unless there is a strong objection (security exposure, validation coupling, or maintainability risk).
Enums internal to the domain (e.g., SampleEntityTypes used only for [EntityType] attributes) remain in *.Domain.
DTO Mapping Rules
[!IMPORTANT]
- All DTOs must derive from
Dto base class and live in *.Contract project
- Primary constructor accepts
SourceKnownEntity? (base type, not concrete entity)
- Entity-specific properties use
required + init with default values
- Public APIs must never return/accept Entities — always DTOs
- Expose only
Guid IDs, never long Id or SourceKnownEntityId
- Keep DTOs concise — no pagination results, no unnecessary properties
DTO Definition (in *.Contract)
using DRN.Framework.SharedKernel.Domain;
namespace Sample.Contract.QA.Categories;
public class CategoryDto(SourceKnownEntity? entity = null) : Dto(entity)
{
public required string Name { get; init; } = string.Empty;
}
Constructor takes SourceKnownEntity? (not concrete entity) to keep Contract independent of Domain. Entity-specific mapping uses ToDto() in Domain layer.
Entity-to-DTO Mapping (in *.Domain)
Map entity properties via ToDto() instance method on the entity using object initializers:
using Sample.Contract.QA.Categories;
[EntityType((byte)SampleEntityTypes.Category)]
public class Category : AggregateRoot
{
public CategoryDto ToDto() => new(this)
{
Name = Name
};
}
new(this) passes entity → base Dto constructor (maps Id, CreatedAt, ModifiedAt). Object initializer sets entity-specific required properties.
DTO-to-Entity Mapping (in *.Domain)
Since Contract cannot reference Domain, DTO-to-Entity mapping uses extension methods defined in the Domain project:
using Sample.Contract.QA.Categories;
namespace Sample.Domain.QA.Categories;
public static class CategoryMappingExtensions
{
public static Category ToEntity(this CategoryDto dto) => new(dto.Name);
}
[!TIP]
Extension methods must call the entity's domain constructor — never bypass invariant enforcement.
Mapper Convention Summary
| Direction | Default Pattern | When Entity is Crowded |
|---|
| Entity → DTO | Instance method ToDto() on entity | Extract to extension method |
| DTO → Entity | Extension method dto.ToEntity() in Domain | Same |
Extract ToDto() to the extension class (e.g., CategoryMappingExtensions) when the entity grows crowded with mapping logic.
API Integration
[!WARNING]
Never enumerate all entities unless explicitly requested. Always use pagination to protect system performance.
Controller Pagination Triad
Implement these three endpoints for comprehensive pagination support:
[HttpGet]
public async Task<PaginationResultModel<UserDto>> GetAsync([FromQuery] PaginationRequest request)
{
var result = await _userRepository.PaginateAsync(request);
return result.ToModel(user => user.ToDto());
}
[HttpGet("{id:guid}")]
public async Task<UserDto> GetByIdAsync(Guid id)
{
var entity = await _userRepository.GetAsync(id);
return entity.ToDto();
}
Mapping Rule: PaginationResultModel must always contain DTOs when returned from API. Use .ToModel(entity => entity.ToDto()) to map.
EntityCreatedFilter
Date-based filtering for pagination and queries:
EntityCreatedFilter.After(DateTimeOffset date)
EntityCreatedFilter.Before(DateTimeOffset date)
EntityCreatedFilter.Between(DateTimeOffset begin, DateTimeOffset end)
EntityCreatedFilter.Outside(DateTimeOffset begin, DateTimeOffset end)
var filter = EntityCreatedFilter.After(DateTimeOffset.UtcNow.AddDays(-7));
var result = await repository.PaginateAsync(request, filter);
Implementation Template
New entity checklist:
- Create entity class with
[EntityType((byte)N)] or a byte-backed enum cast and private parameterless constructor
- Create DTO in
*.Contract project with Dto base class and required + init properties
- Add
ToDto() instance method on entity with object initializer
- Add
IEntityTypeConfiguration<T> in infrastructure layer
- Add
DbSet<T> to DrnContext
- Create repository interface (
IXxxRepository : ISourceKnownRepository<T>)
- Implement repository with
[Scoped<IXxxRepository>]
- Add migration:
dotnet ef migrations add AddXxx --context MyContext
Related Skills