skills/architecture/clean-architecture/SKILL.md
Use when implementing or enforcing Clean Architecture with 4-layer separation and dependency inversion.
npx skillsauth add faysilalshareef/dotnet-ai-kit clean-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
src/
{Company}.{Domain}.Domain/ # Entities, value objects, domain events, interfaces
{Company}.{Domain}.Application/ # Commands, queries, handlers, DTOs, validators
{Company}.{Domain}.Infrastructure/ # EF Core, external services, file system
{Company}.{Domain}.WebApi/ # Endpoints, middleware, DI composition root
WebApi → Infrastructure → Application → Domain
↑
WebApi → Application → Domain
Domain references nothingApplication references DomainInfrastructure references Application + DomainWebApi references all layers (composition root)// Domain/Entities/Order.cs
namespace {Company}.{Domain}.Domain.Entities;
public sealed class Order : BaseEntity, IAggregateRoot
{
public string CustomerName { get; private set; } = default!;
public OrderStatus Status { get; private set; } = OrderStatus.Draft;
public decimal Total { get; private set; }
private readonly List<OrderItem> _items = [];
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
private Order() { } // EF Core constructor
public static Order Create(string customerName)
{
var order = new Order { CustomerName = customerName };
order.AddDomainEvent(new OrderCreatedEvent(order.Id));
return order;
}
public void AddItem(Guid productId, int quantity, decimal price)
{
Guard.Against.NegativeOrZero(quantity);
_items.Add(new OrderItem(productId, quantity, price));
Total = _items.Sum(i => i.Quantity * i.Price);
}
}
// Domain/Interfaces/IOrderRepository.cs
public interface IOrderRepository
{
Task<Order?> FindAsync(Guid id, CancellationToken ct = default);
Task<List<Order>> ListAsync(CancellationToken ct = default);
void Add(Order order);
}
// Domain/Interfaces/IUnitOfWork.cs
public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken ct = default);
}
// Application/Orders/Commands/CreateOrder/CreateOrderCommand.cs
namespace {Company}.{Domain}.Application.Orders.Commands.CreateOrder;
public sealed record CreateOrderCommand(string CustomerName) : IRequest<Guid>;
// Application/Orders/Commands/CreateOrder/CreateOrderCommandHandler.cs
internal sealed class CreateOrderCommandHandler(
IOrderRepository repository,
IUnitOfWork unitOfWork) : IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(
CreateOrderCommand request, CancellationToken ct)
{
var order = Order.Create(request.CustomerName);
repository.Add(order);
await unitOfWork.SaveChangesAsync(ct);
return order.Id;
}
}
// Application/Orders/Commands/CreateOrder/CreateOrderCommandValidator.cs
public sealed class CreateOrderCommandValidator
: AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.CustomerName).NotEmpty().MaximumLength(200);
}
}
// Infrastructure/Persistence/AppDbContext.cs
namespace {Company}.{Domain}.Infrastructure.Persistence;
internal sealed class AppDbContext(
DbContextOptions<AppDbContext> options) : DbContext(options), IUnitOfWork
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(
typeof(AppDbContext).Assembly);
}
}
// Infrastructure/Persistence/Repositories/OrderRepository.cs
internal sealed class OrderRepository(AppDbContext db) : IOrderRepository
{
public async Task<Order?> FindAsync(Guid id, CancellationToken ct)
=> await db.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task<List<Order>> ListAsync(CancellationToken ct)
=> await db.Orders.ToListAsync(ct);
public void Add(Order order) => db.Orders.Add(order);
}
// Infrastructure/DependencyInjection.cs
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("Default")));
services.AddScoped<IUnitOfWork>(sp =>
sp.GetRequiredService<AppDbContext>());
services.AddScoped<IOrderRepository, OrderRepository>();
return services;
}
}
// WebApi/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddApplicationServices()
.AddInfrastructure(builder.Configuration);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.MapEndpointGroups();
app.Run();
new SqlConnection)Domain, Application, Infrastructure, WebApi/Api/PresentationIUnitOfWork interface in Domain or Application layerusing statements for EF Core or infrastructure{Company}.{Domain}.{Layer}| Question | Answer | |----------|--------| | Where do entities go? | Domain | | Where do repository interfaces go? | Domain (per aggregate root) | | Where do DTOs go? | Application | | Where do MediatR handlers go? | Application | | Where does DbContext go? | Infrastructure | | Where does DI registration go? | Each layer has its own + WebApi composes | | Where does validation go? | Application (FluentValidation) |
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.