ddd-dotnet
# Domain-Driven Design (DDD) in .NET - Basics & Patterns
What this skill does
# Domain-Driven Design (DDD) in .NET - Basics & Patterns
## Overview
This skill provides a comprehensive guide to implementing Domain-Driven Design patterns in .NET, based on real-world production code from the LMP project. These patterns promote clean architecture, maintainability, and testability.
### Key Benefits
- **Clear Separation of Concerns**: Domain logic isolated from infrastructure
- **CQRS Pattern**: Optimized read and write operations
- **Hexagonal Architecture**: Ports (domain) and Adapters (infrastructure)
- **Testability**: Domain logic testable without dependencies
- **Time Control**: Deterministic time handling for testing
---
## Core Building Blocks
### 1. Domain Object Hierarchy
All domain objects implement a marker interface at the root:
```csharp
namespace Pikot.LMP.Domain.Common;
public interface IDomainObject;
```
### 2. Aggregates
Aggregates are consistency boundaries and entry points for domain operations.
```csharp
namespace Pikot.LMP.Domain.Common;
public interface IAggregate : IDomainObject
{
int Id { get; }
DateTimeOffset CreatedAt { get; }
DateTimeOffset InsertedAt { get; }
}
```
**Versioned Aggregates** track changes over time:
```csharp
namespace Pikot.LMP.Domain.Common;
public interface IAggregateVersioned : IAggregate
{
int Version { get; }
}
```
**Key Principles:**
- Aggregates are the only objects that can be retrieved from repositories
- They maintain invariants and enforce business rules
- They control access to their entities
- Use `CreatedAt` for business time, `InsertedAt` for audit/database time
### 3. Entities
Entities are objects with identity that belong to aggregates:
```csharp
namespace Pikot.LMP.Domain.Common;
public interface IEntity : IDomainObject
{
int Id { get; }
DateTimeOffset CreatedAt { get; }
DateTimeOffset InsertedAt { get; }
}
```
**Example Entity:**
```csharp
using Pikot.LMP.Domain.Common;
namespace Pikot.LMP.Domain.Shipment.ShipperOrders;
public class Package : IEntity
{
protected Package() { } // For EF Core
public Package(decimal kg, DateTimeOffset createdAt, DateTimeOffset insertedAt, byte numeration)
{
Guid = Guid.NewGuid();
Kg = kg;
CreatedAt = createdAt;
InsertedAt = insertedAt;
Numeration = numeration;
Status = PackageStatus.Pending;
}
public int Id { get; protected init; }
public Guid Guid { get; protected init; }
public DateTimeOffset CreatedAt { get; }
public DateTimeOffset InsertedAt { get; }
public decimal Kg { get; private set; }
public byte Numeration { get; private set; }
public PackageStatus Status { get; private set; }
public int ShipperOrderId { get; protected init; }
// Business logic methods
public void UpdateWeight(decimal newKg)
{
Kg = newKg;
}
public void SetNumeration(byte numeration)
{
Numeration = numeration;
}
public void SetStatus(PackageStatus status)
{
Status = status;
}
}
```
---
## Aggregate Root Pattern
**Example: ShipperOrder Aggregate**
```csharp
using Pikot.LMP.Domain.Common;
using Pikot.LMP.Domain.Common.Exceptions;
namespace Pikot.LMP.Domain.Shipment.ShipperOrders;
public class ShipperOrder : IAggregateVersioned
{
protected ShipperOrder() { } // For EF Core
public ShipperOrder(DateTimeOffset createdAt, DateTimeOffset insertedAt,
int shipperId, string shipperCompanyName, /* ... other params */)
{
Guid = Guid.NewGuid();
CreatedAt = createdAt;
InsertedAt = insertedAt;
Version = 1;
// Initialize collections
_changeEvents.Add(new ShipperOrderChangeEvent(/* ... */));
_versions.Add(new ShipperOrderVersion(/* ... */));
}
// Properties
public int Id { get; protected init; }
public Guid Guid { get; protected init; }
public int Version { get; private set; }
public DateTimeOffset CreatedAt { get; }
public DateTimeOffset InsertedAt { get; }
public ShipperOrderStatusType Status { get; private set; }
// Collections - private backing fields with public readonly access
private readonly List<ShipperOrderChangeEvent> _changeEvents = [];
public IReadOnlyCollection<ShipperOrderChangeEvent> Changes
{
get => _changeEvents;
init => _changeEvents = value.ToList(); // For EF Core materialization
}
private readonly List<Package> _packages = [];
public IReadOnlyCollection<Package> Packages
{
get => _packages.OrderBy(p => p.Numeration).ToList();
init => _packages = value.ToList();
}
// Business methods with validation
public void Pickup(int dispatcherUserId, DateTimeOffset timestamp, IClock clock,
int dispatcherId, string dispatcherCompanyName)
{
if (Status is not (ShipperOrderStatusType.Created or ShipperOrderStatusType.Ready))
{
throw new InvalidTransitionException();
}
Status = ShipperOrderStatusType.PickedUp;
DispatcherId = dispatcherId;
DispatcherCompanyName = dispatcherCompanyName;
foreach (var package in _packages)
{
package.SetStatus(PackageStatus.OutForDelivery);
}
var change = new ShipperOrderChangeEvent(timestamp, clock.Now, dispatcherUserId,
ShipperOrderStatusType.PickedUp, ShipperOrderChangeEventType.StatusChanged,
ShipperOrderChangeEventSourceType.Dispatcher);
_changeEvents.Add(change);
}
public Package AddPackage(decimal kg, DateTimeOffset createdAt, DateTimeOffset insertedAt)
{
var numeration = (byte)(_packages.Count + 1);
var package = new Package(kg, createdAt, insertedAt, numeration);
_packages.Add(package);
return package;
}
public void RemovePackage(int packageId)
{
var package = _packages.FirstOrDefault(p => p.Id == packageId);
if (package == null)
throw new NotFoundException("Package not found");
_packages.Remove(package);
RenumberPackages();
}
private void RenumberPackages()
{
byte numeration = 1;
foreach (var package in _packages.OrderBy(p => p.Numeration))
{
package.SetNumeration(numeration);
numeration++;
}
}
}
```
**Key Patterns:**
- Protected parameterless constructor for EF Core
- Public constructor with all required data
- Private setters on properties
- Private backing fields for collections with `IReadOnlyCollection<T>` exposure
- Business logic encapsulated in methods
- Validation within business methods
- Track domain events in collections
---
## Repository Pattern (CQRS)
### Write Repository
```csharp
namespace Pikot.LMP.Domain.Common;
public interface IRepository<TAggregate, in TUniqueKey> where TAggregate : class, IAggregate
{
Task<TAggregate?> GetAsync(int id, CancellationToken cancellationToken);
Task<bool> ExistsAsync(TUniqueKey key, CancellationToken cancellationToken);
Task SaveChangesAsync(CancellationToken cancellationToken);
void Add(TAggregate entity);
}
```
### Query Repository
```csharp
namespace Pikot.LMP.Domain.Common;
public interface IQueryRepository<TAggregate> where TAggregate : class, IAggregate
{
Task<TAggregate?> GetAsync(int id, CancellationToken cancellationToken);
}
```
**Usage:**
- **Write operations**: Use `IRepository<TAggregate, TUniqueKey>` in command handlers
- **Read operations**: Use `IQueryRepository<TAggregate>` in query handlers
- Write repositories can check existence and add entities
- Query repositories are simpler and optimized for reads
- In command handlers where you need to load(read) then update the aggregate and save, use the write repository to load the aggregate. The query repository does not track the entity and the save on the write repository will do nothing if the aggregate is loaded with the query repository.
### Specific Repository InRelated in Design
contribute
IncludedLocal-only OSS contribution command center. Auto-refreshes the user's in-flight PR and issue state on invoke so conversations start with full context — no need to brief Claude on what's in flight. Helps the user find issues to contribute to on GitHub, builds per-repo dossiers of what each upstream expects (CLA, DCO, branch convention, AI policy, draft-first, review bots, issue templates), runs deterministic gates before any external action so AI-assisted contributions don't reach maintainers as slop. State is markdown-only: candidate files at ~/.contribute-system/candidates/, repo dossiers at ~/.contribute-system/research/, append-only event log at ~/.contribute-system/log.jsonl. No database, no cloud calls. Use when the user asks about their PRs / issues / contributions, wants to find new work to take on, claim an issue, build/refresh a repo's dossier, or draft a Design Issue or PR. Trigger with "/contribute", "what's my PR status", "find a contribution", "claim issue X", "draft a Design Issue for Y", "refresh dossier for Z".
architectural-analysis
IncludedUser-triggered deep architectural analysis of a codebase or scoped subtree across eight modes — information architecture, data flow, integration points, UI surfaces, interaction patterns, data model, control flow, and failure modes. This skill should be used when the user asks to "diagram this codebase," "map the architecture," "show the data flow," "give me an ERD," "trace control flow," "find the integration points," "verify the layout pattern," "audit the UX architecture," or any similar request whose primary deliverable is mermaid diagrams plus cited reports under docs/architecture/. Dispatches haiku/sonnet sub-agents in parallel for per-mode exploration, then verifies every citation mechanically before any node lands in a diagram. Not for one-off prose explanations of code (use code-explanation) or for high-level system design from scratch (use system-design).
mcp
IncludedModel Context Protocol (MCP) server development and tool management. Languages: Python, TypeScript. Capabilities: build MCP servers, integrate external APIs, discover/execute MCP tools, manage multi-server configs, design agent-centric tools. Actions: create, build, integrate, discover, execute, configure MCP servers/tools. Keywords: MCP, Model Context Protocol, MCP server, MCP tool, stdio transport, SSE transport, tool discovery, resource provider, prompt template, external API integration, Gemini CLI MCP, Claude MCP, agent tools, tool execution, server config. Use when: building MCP servers, integrating external APIs as MCP tools, discovering available MCP tools, executing MCP capabilities, configuring multi-server setups, designing tools for AI agents.
react-native-skia
IncludedDesign, build, debug, and optimise high-polish animated graphics in React Native or Expo using @shopify/react-native-skia, Reanimated, and Gesture Handler. Use when the user wants canvas-driven UI, shaders, paths, rich text, image filters, sprite fields, Skottie, video frames, snapshots, web CanvasKit setup, or performance tuning for custom motion-heavy elements such as loaders, hero art, cards, charts, progress indicators, particle systems, or gesture-driven surfaces. Also use when the user asks for fluid, glow, glass, blob, parallax, 60fps/120fps, or GPU-friendly animated effects in React Native, even if they do not explicitly say "Skia". Do not use for ordinary form/layout work with standard views.
plaid
IncludedProduct Led AI Development — guides founders from idea to launched product. Six capabilities: Idea (discover a product idea), Validate (pressure-test the idea against fatal flaws, problem reality, competition, and 2-week MVP feasibility), Plan (vision intake + document generation), Design (translate image references into a design.md spec), Launch (go-to-market strategy), and Build (roadmap execution). Use when someone says "PLAID", "plaid idea", "help me find an idea", "product idea", "idea from my business", "idea from my expertise", "plaid validate", "validate my idea", "pressure-test", "is this idea good", "find fatal flaws", "validate the problem", "plan a product", "define my vision", "generate a PRD", "product strategy", "plaid design", "design from image", "translate image to design", "create design.md", "extract design tokens", "plaid launch", "go-to-market", "launch plan", "GTM strategy", "launch playbook", "plaid build", "build the app", "start building", or "execute the roadmap".
nextjs-framer-motion-animations
IncludedAdds production-safe Motion for React or Framer Motion animations to Next.js apps, including reveal, hover and tap micro-interactions, whileInView, stagger, AnimatePresence, layout and layoutId transitions, reorder, scroll-linked UI, and lightweight route-content transitions. Use when the user asks to add, refactor, or debug Motion or Framer Motion in App Router or Pages Router codebases, especially around server/client boundaries, reduced motion, LazyMotion, bundle size, hydration, or route transitions. Avoid for GSAP-style timelines, WebGL or 3D scenes, heavy scroll storytelling, or CSS-only effects unless Motion is explicitly requested.