| name | clean-architecture-conventions |
| description | Project-wide conventions for this .NET Clean Architecture solution — layering, Result<T>, errors, domain events, and what belongs in which project. Use when planning or implementing any change that touches src/Domain, src/Application, src/Infrastructure, or src/Web.Api. |
Projects and responsibilities
| Project | Depends on | Holds |
|---|
SharedKernel | — | Entity, Result, Result<T>, Error, ErrorType, ValidationError, IDomainEvent, IDomainEventHandler<T>, IDateTimeProvider |
Domain | SharedKernel | Entities, enums, domain events, <Feature>Errors static classes |
Application | Domain, SharedKernel | Commands, queries, handlers, validators, IApplicationDbContext, IUserContext, IPasswordHasher, ITokenProvider, behaviors (logging, validation) |
Infrastructure | Application, Domain, SharedKernel | ApplicationDbContext, EF configurations, migrations, JWT, password hashing, permission authorization, domain event dispatcher |
Web.Api | All of the above | IEndpoint implementations, DI composition root, middleware, global exception handler |
ArchitectureTests | Solution | NetArchTest-based layer rules |
Hard layering rules (enforced by tests/ArchitectureTests/Layers/LayerTests.cs)
Domain must not depend on Application, Infrastructure, or Web.Api.
Application must not depend on Infrastructure or Web.Api.
Infrastructure must not depend on Web.Api.
Cross-cutting adapters go behind an Application interface (e.g., IApplicationDbContext), with the concrete implementation in Infrastructure.
Results and errors
-
Handlers return Result or Result<T> from SharedKernel. Do not throw exceptions for expected failures.
-
Failures carry an Error from a static class named <Feature>Errors living in src/Domain/<Feature>/:
public static class TodoItemErrors
{
public static Error NotFound(Guid id) => Error.NotFound(
"TodoItems.NotFound", $"Todo item '{id}' was not found");
}
-
Error codes follow <PluralEntity>.<Case> (e.g., TodoItems.AlreadyCompleted).
-
Endpoints translate Result → HTTP via result.Match(Results.Ok, CustomResults.Problem).
Domain events
- Event types live in
Domain/<Feature>/<Event>DomainEvent.cs, implement IDomainEvent, and are typically sealed records.
- Raise them on an entity via
entity.Raise(new XDomainEvent(...)) (provided by Entity base).
- Handlers live in
Application/<Feature>/<Handler>.cs and implement IDomainEventHandler<TEvent>.
- Dispatch happens from
DomainEventsDispatcher inside ApplicationDbContext.SaveChangesAsync — no manual wiring per event.
Visibility conventions
- Commands/queries:
public sealed class.
- Command/query handlers:
internal sealed class with primary-constructor DI.
- Validators:
public class : AbstractValidator<T>.
- Endpoints:
internal sealed class : IEndpoint.
- Domain entities:
public sealed class : Entity (or Entity base as appropriate).
Where to put things — quick lookup
- New use case →
Application/<Feature>/<Action>/ (see cqrs-pattern skill).
- New entity →
Domain/<Feature>/ + EF config in Infrastructure/<Feature>/ (see domain-entity skill).
- New HTTP surface →
Web.Api/Endpoints/<Feature>/ (see add-endpoint skill).
- New infrastructure adapter → add interface in
Application/Abstractions/..., implementation in Infrastructure/..., register in Infrastructure/DependencyInjection.cs.
- New cross-cutting behavior →
Application/Abstractions/Behaviors/ (see LoggingDecorator.cs, ValidationDecorator.cs).
Non-negotiables
- Do not reference
Microsoft.EntityFrameworkCore from Domain or Application code (except via IApplicationDbContext, which exposes DbSet<T>).
- Do not use ASP.NET Core types (
HttpContext, IResult) outside Web.Api.
- Do not add MediatR — this template intentionally uses direct
ICommandHandler<,> / IQueryHandler<,> resolution from DI.
- Do not bypass the
Result<T> pipeline by throwing from handlers.