Install
$ agentstack add skill-cuongtl1992-vibe-skills-dotnet-ddd ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
.NET Core DDD + Hexagonal/Clean Architecture
This skill guides building .NET Core applications following Domain-Driven Design with Hexagonal (Ports & Adapters) and Clean Architecture principles.
When NOT to Use This Skill
- Simple CRUD operations without complex business rules
- Basic EF Core or Dapper query optimization (use
sqlserver-expert) - Generic C# coding tasks unrelated to domain architecture
- Frontend or API design without domain modeling aspects
Skill Interactions
- For SQL query optimization inside QueryHandlers, defer to
sqlserver-expertskill - This skill defines the QueryHandler structure;
sqlserver-expertoptimizes the SQL within
Architecture Overview
Dependency Flow (Inward)
Presentation (APIs / Workers)
│
▼
Application (UseCases, Queries, DTOs)
│
▼
Domain (Entities, Value Objects, Domain Events, Interfaces)
│
▲
Infrastructure (Repos, External Services, Adapters)
│
▼
Core (Shared kernel, Base abstractions)
The dependency rule: Dependencies always point inward. Domain has ZERO dependencies on Infrastructure or Application. Infrastructure implements Domain interfaces (Ports & Adapters).
Modular Solution Structure
Each bounded context is a module with three projects:
src/Modules/{ModuleName}/
├── {Namespace}.Application/ # Use Cases, Queries, DTOs, Handlers
│ ├── UseCases/ # Command-side (write operations)
│ │ └── {Name}UseCase.cs
│ ├── UseCaseHandlers/ # UseCase implementations
│ │ └── {Name}UseCaseHandler.cs
│ ├── Queries/ # Query-side (read operations)
│ │ └── Get{Name}Query.cs
│ ├── QueryHandlers/ # Query implementations (Dapper)
│ │ └── Get{Name}QueryHandler.cs
│ ├── DTOs/ # Data Transfer Objects
│ ├── Interfaces/ # Application-level service interfaces
│ └── Helpers/ # Application utilities
│
├── {Namespace}.Domain/ # Pure domain model
│ ├── Entities/ # Aggregates and Entities
│ ├── ValueObjects/ # Value Objects
│ ├── Enums/ # Domain enumerations
│ ├── Events/ # Domain Events
│ ├── Interfaces/ # Repository interfaces (Ports)
│ └── Exceptions/ # Domain-specific exceptions
│
└── {Namespace}.Infrastructure/ # Adapters
├── Repositories/ # Repository implementations (EF Core)
├── Services/ # External service adapters
└── Persistence/ # DB configurations, migrations
Shared layers sit outside modules:
src/Core/{Namespace}.Core/ # Base interfaces, shared kernel
src/Infrastructure/{Namespace}.Infrastructure/ # Cross-cutting (DB context, caching, messaging)
src/Presentation/ # API and Worker entry points
CQRS Separation
Strict separation between reads and writes. Read the reference file for complete interface definitions: references/core-interfaces.md
Write Side: UseCase + UseCaseHandler
UseCases represent business commands. They always use Repository Pattern with EF Core.
UseCase — a data carrier describing the intent:
public class CreateInvoiceUseCase : IUseCase
{
public CreateInvoiceRequest Request { get; }
public CreateInvoiceUseCase(CreateInvoiceRequest request)
{
Request = request ?? throw new ArgumentNullException(nameof(request));
}
}
UseCaseHandler — contains the business logic:
public class CreateInvoiceUseCaseHandler : IUseCaseHandlerWithInput
{
private readonly IInvoiceRepository _invoiceRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateInvoiceUseCaseHandler(
IInvoiceRepository invoiceRepository,
IUnitOfWork unitOfWork)
{
_invoiceRepository = invoiceRepository;
_unitOfWork = unitOfWork;
}
public async Task HandleAsync(CreateInvoiceUseCase useCase)
{
var request = useCase.Request;
// Domain logic — use factory method on Aggregate
var invoice = Invoice.Create(
buyerTaxCode: request.BuyerTaxCode,
sellerTaxCode: request.SellerTaxCode,
amount: request.Amount
);
await _invoiceRepository.AddAsync(invoice);
await _unitOfWork.CommitAsync();
}
}
UseCase variants (choose the right interface):
| Variant | Interface | When | |---------|-----------|------| | Input + Output | IUseCaseHandler | Command returns result | | Input only | IUseCaseHandlerWithInput | Fire-and-forget command | | Output only | IUseCaseHandlerWithOutput | Parameterless command |
Dispatching via IUseCaseService:
// In Controller or Worker
await _useCaseService.ExecuteUseCaseAsync(new CreateInvoiceUseCase(request));
Read Side: Query + QueryHandler
Queries are read-only and always use Dapper for performance. Never use EF Core for queries.
// Query definition
public class GetInvoiceByIdQuery : IQuery
{
public Guid InvoiceId { get; }
public GetInvoiceByIdQuery(Guid invoiceId) => InvoiceId = invoiceId;
}
// Query Handler — uses Dapper, NOT repositories
public class GetInvoiceByIdQueryHandler : IQueryHandler
{
private readonly IDbConnection _connection;
public GetInvoiceByIdQueryHandler(IDbConnection connection)
{
_connection = connection;
}
public async Task HandleAsync(GetInvoiceByIdQuery query)
{
const string sql = """
SELECT Id, BuyerTaxCode, SellerTaxCode, Amount, Status
FROM Invoices
WHERE Id = @InvoiceId
""";
return await _connection.QuerySingleOrDefaultAsync(
sql, new { query.InvoiceId });
}
}
Dispatching via IQueryService:
var invoice = await _queryService.ExecuteAsync(new GetInvoiceByIdQuery(id));
Domain Modeling
For complete code patterns (Aggregates, Value Objects, Domain Events, Repository interfaces), see: references/domain-patterns.md
Key Rules
Aggregates:
- Use factory methods (
Create(...)) — never expose public constructors for creation - Use private setters — state changes only through behavior methods
- Raise domain events inside factory methods and behavior methods
- Validate invariants at creation and on every state transition
- Keep aggregates small — reference other aggregates by ID, not by navigation property
Value Objects:
- Immutable, compared by value — use C#
recordtypes - Self-validating in constructor
Domain Events:
- Events raised by aggregates, saved to Outbox table in the same transaction
- Published via CDC (Debezium) → Kafka → Consumer workers
Outbox flow:
- Aggregate raises domain event via
AddDomainEvent() SaveChangesAsyncinterceptor serializes events toOutboxEventstable in the SAME transaction- CDC (Debezium) captures changes from
OutboxEventstable - Debezium publishes to Kafka topic
- Consumer workers process the events
Repository Interfaces (Ports):
- Repository interfaces live in
Domain/Interfaces/ - Repository implementations live in
Infrastructure/Repositories/ - One repository per Aggregate Root
- Repositories work with domain entities, NEVER with DTOs
Error Handling
Use Result pattern as default, with exceptions for truly exceptional cases.
public class Result
{
public bool IsSuccess { get; init; }
public T? Value { get; init; }
public string? Error { get; init; }
public static Result Success(T value) => new() { IsSuccess = true, Value = value };
public static Result Failure(string error) => new() { IsSuccess = false, Error = error };
}
When to use what:
- Result pattern: Validation failures, business rule violations, expected error paths
- Exceptions: Infrastructure failures, unexpected errors, programming bugs
- DomainException: Domain invariant violations inside aggregates
Strategic DDD
For guidance on Bounded Context identification, Context Mapping patterns, and Ubiquitous Language practices, read: references/strategic-ddd.md
Common Issues
User provides incomplete aggregate requirements
Ask for: aggregate name, key properties, business rules, related events. Do NOT generate code until invariants are clear.
Namespace mismatch
Always ask user for the module namespace before generating code. Default pattern: {CompanyNamespace}.{ModuleName}.{Layer}
Over-engineering simple features
If the feature is simple CRUD with no business rules, suggest a simpler approach without full DDD ceremony. Not everything needs an Aggregate.
Checklist: Creating a New Module
- Identify the Bounded Context and its Ubiquitous Language
- Create the three projects:
.Domain,.Application,.Infrastructure - Define Aggregates with factory methods and domain events
- Define Repository interfaces in Domain
- Create UseCases and UseCaseHandlers for write operations
- Create Queries and QueryHandlers (Dapper) for read operations
- Implement Repositories with EF Core in Infrastructure
- Register DI in the Presentation layer
- Wire up Outbox for domain event publishing
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: cuongtl1992
- Source: cuongtl1992/vibe-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.