nesbo/dotnet-claude-code-skills/ddd-dotnet

Claude Code and agent skill by nesbo/dotnet-claude-code-skills

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Install

npx vskill@latest install nesbo/dotnet-claude-code-skills/ddd-dotnet

SKILL.md

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 Interfaces

Define domain-specific repository interfaces in the Domain project:

csharp
namespace Pikot.LMP.Domain.Shipment.Repositories;
public interface IShipperOrderRepository : IRepository<ShipperOrder, ShipperOrderUniqueKey>
{
// Additional write-specific methods if needed
}
public interface IShipperOrderQueryRepository : IQueryRepository<ShipperOrder>
{
Task<ShipperOrder?> GetByGuidAsync(Guid guid, CancellationToken cancellationToken);
// Additional query methods
}

Implementations live in the Data project (adapter layer).


Time Control Pattern

NEVER use `DateTime.Now` or `DateTime.UtcNow` directly in domain code!

IClock Interface
csharp
namespace Pikot.LMP.Domain.Common;
public interface IClock
{
DateTimeOffset Now { get; }
}
Usage in Domain Code
csharp
public void Complete(int completedByUserId, DateTimeOffset timestamp, IClock clock)
{
Status = ShipperOrderStatusType.Delivered;
var change = new ShipperOrderChangeEvent(
timestamp, // Business time (from command/user action)
clock.Now, // Audit time (when it was processed)
completedByUserId,
ShipperOrderStatusType.Delivered,
ShipperOrderChangeEventType.StatusChanged,
ShipperOrderChangeEventSourceType.Dispatcher);
_changeEvents.Add(change);
}

Benefits:

  • Testable: Inject mock clock for deterministic tests
  • Auditable: Separate business time from processing time
  • Consistent: All time handling centralized

Unit of Work Pattern

For transactions spanning multiple repositories:

csharp
namespace Pikot.LMP.Domain.Common;
public interface IUnitOfWork
{
Task BeginTransactionAsync(CancellationToken cancellationToken);
Task CommitTransactionAsync(CancellationToken cancellationToken);
Task RollbackTransactionAsync(CancellationToken cancellationToken);
}

Usage in Command Handlers:

csharp
public override async Task<CreateShipperOrderCommand> HandleAsync(
CreateShipperOrderCommand command, CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);
try
{
// Multiple repository operations
shipperAddressRepository.Add(shipperAddress);
receiverAddressRepository.Add(receiverAddress);
shipperOrderRepository.Add(shipperOrder);
await shipperOrderRepository.SaveChangesAsync(cancellationToken);
await unitOfWork.CommitTransactionAsync(cancellationToken);
}
catch (Exception e)
{
logger.LogError(e, "Failed to create shipper order");
await unitOfWork.RollbackTransactionAsync(cancellationToken);
throw;
}
return await base.HandleAsync(command, cancellationToken);
}

Command Pattern (Paramore.Brighter)

Domain Command Base Class
csharp
using System.Diagnostics;
using Paramore.Brighter;
namespace Pikot.LMP.Domain.Common;
public abstract class DomainCommand : IRequest
{
protected DomainCommand() { }
protected DomainCommand(DateTimeOffset timestamp)
{
Timestamp = timestamp;
Span = new Activity(GetType().Name);
Span.SetStartTime(timestamp.UtcDateTime);
Span.Start();
}
public Guid Id { get; set; } = Guid.NewGuid();
public Activity Span { get; set; } = null!;
public DateTimeOffset Timestamp { get; set; }
}
Command Implementation

Commands and their handlers are in the same file:

csharp
using Paramore.Brighter;
using Pikot.LMP.Domain.Common;
namespace Pikot.LMP.Domain.Shipment.ShipperOrders.CreateShipperOrder;

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
46
Repository forks
2
7-day trend score
19

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 18 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/nesbo/dotnet-claude-code-skills/ddd-dotnet/badge)](https://verified-skill.com/skills/nesbo/dotnet-claude-code-skills/ddd-dotnet)

Something wrong with this skill or its page? Tell us.