4-layer Clean Architecture pattern with dependency rules. Domain, Application, Infrastructure, and Presentation layers with dependency inversion. Trigger: clean architecture, layers, dependency inversion, use cases.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
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) |