
Dotnet Clean Architecture
- 1 installs
- Updated July 5, 2026
- adrianava9/cero-base
dotnet-clean-architecture is a Claude Code skill that scaffolds a complete .NET solution following Clean Architecture with Domain, Application, Infrastructure and API layers.
About
dotnet-clean-architecture is a Claude Code skill that scaffolds a complete .NET solution following Clean Architecture, with distinct Domain, Application, Infrastructure and API layers and unidirectional inward dependencies. It generates the project structure, dependency injection setup, CQRS messaging abstractions, and cross-cutting concerns. It targets .NET 8+ and uses MediatR, FluentValidation, Entity Framework Core and Dapper.
- Scaffolds a complete .NET solution following Clean Architecture with layer separation
- Creates Domain, Application, Infrastructure and API projects with the dependency rule inward
- Sets up MediatR CQRS, FluentValidation, EF Core and DI wiring
Dotnet Clean Architecture by the numbers
- 1 all-time installs (skills.sh)
- Ranked #121 of 153 .NET & C# skills by installs in the Skillselion catalog
- Data as of Jul 7, 2026 (Skillselion catalog sync)
dotnet-clean-architecture capabilities & compatibility
- Capabilities
- backend · api development · refactoring · database
- Use cases
- api development · database
What dotnet-clean-architecture says it does
Scaffolds a complete .NET solution following Clean Architecture principles with proper layer separation (API, Application, Domain, Infrastructure).
**Dependency Rule**: Dependencies point inward. Domain has no dependencies. Application depends only on Domain.
npx skills add https://github.com/adrianava9/cero-base --skill dotnet-clean-architectureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | July 5, 2026 |
| Repository | adrianava9/cero-base ↗ |
What it does
Scaffold a .NET 8+ solution with Clean Architecture layers, CQRS, DI and EF Core wiring.
Who is it for?
Bootstrapping a layered .NET backend with Clean Architecture, CQRS and EF Core
Skip if: Non-.NET stacks or projects that do not want a layered/onion architecture
When should I use this skill?
You need to scaffold a new .NET solution with Clean Architecture layers, DI, CQRS handlers and repositories
What you get
A .NET solution with Domain, Application, Infrastructure and API projects wired under the inward dependency rule
- .NET solution with Domain, Application, Infrastructure and API projects
- DI setup
- CQRS command/query abstractions
By the numbers
- 4 architecture layers: API, Infrastructure, Application, Domain
Files
.NET Clean Architecture Project Scaffolder
Overview
This skill generates a complete .NET solution following Clean Architecture (also known as Onion Architecture or Hexagonal Architecture). The architecture enforces separation of concerns through distinct layers with unidirectional dependencies pointing inward.
Architecture Layers
┌─────────────────────────────────────────────────────────────┐
│ API Layer │
│ Controllers, Middleware, Request/Response DTOs │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ EF Core, Repositories, External Services, Authentication │
├─────────────────────────────────────────────────────────────┤
│ Application Layer │
│ Commands, Queries, Handlers, Validators, DTOs │
├─────────────────────────────────────────────────────────────┤
│ Domain Layer │
│ Entities, Value Objects, Domain Events, Interfaces │
└─────────────────────────────────────────────────────────────┘Dependency Rule: Dependencies point inward. Domain has no dependencies. Application depends only on Domain. Infrastructure implements interfaces from Domain/Application.
Where I{Entity}Repository lives (Domain vs Application)
The folder tree above places I{Entity}Repository.cs under Domain/{Aggregate}/. That is the default for the aggregate persistence port: methods work with domain entities and primitives only (GetById, Add, Update, Remove, etc.). Those interfaces must not reference application DTOs or other Application-layer types, or Domain would stop being dependency-free (Critical Rule 1).
The same Dependency Rule line says Infrastructure implements interfaces from Domain or Application. Use `Application/Abstractions` for a contract when:
- Methods return query or API response DTOs (read models), or
- The operation spans multiple aggregates or tables for one screen/report.
Infrastructure still holds the implementations (e.g. one EF-backed class can implement both a Domain IBudgetRepository for Budget persistence and an Application IBudgetListQueries for DTO projections). Naming is flexible; what matters is Domain never references Application types.
So: persistence-only repository → Domain. DTO-returning or cross-aggregate reads → Application abstractions (unless you map to DTOs entirely inside the handler from entity-only repository methods).
Application {Feature} folder layout (mandatory)
Every CQRS use case must live in its own folder named after that command or query. CeroBase groups them under read/write parents:
| Layout | Path pattern | Namespace (example) |
|---|---|---|
| Grouped (CeroBase) | Application/{Feature}/Queries/{UseCase}/ | ...Application.Budgets.Queries.GetBudgets |
| Grouped (CeroBase) | Application/{Feature}/Commands/{UseCase}/ | ...Application.Budgets.Commands.UpsertBudgets |
| Flat (alternative) | Application/{Feature}/{UseCase}/ | ...Application.Budgets.GetBudgets |
Do not put multiple unrelated use cases in one leaf folder (no single folder containing every command’s files).
| Kind | Typical contents |
|---|---|
| Command (write) | {Name}Command.cs (record + handler), {Name}CommandValidator.cs, optional request types |
| Query (read) | {Name}Query.cs (record + handler + response DTOs as needed), {Name}QueryValidator.cs |
- Tests mirror the same folder shape under
{Feature}/(seeunit-testing,integration-testingskills). - Reference (this repo):
Fintrack.Server/Application/Budgets/Queries/GetBudgets/,.../Commands/UpsertBudgets/, etc.
Quick Reference
| Task | Command/Action |
|---|---|
| Create solution | dotnet new sln -n {SolutionName} |
| Create Domain project | dotnet new classlib -n {name}.domain |
| Create Application project | dotnet new classlib -n {name}.application |
| Create Infrastructure project | dotnet new classlib -n {name}.infrastructure |
| Create API project | dotnet new webapi -n {name}.api |
| Add project to solution | dotnet sln add src/{project}/{project}.csproj |
| Add project reference | dotnet add reference ../other/other.csproj |
---
Project Structure
{SolutionName}/
├── src/
│ ├── {name}.domain/
│ │ ├── Abstractions/
│ │ │ ├── Entity.cs
│ │ │ ├── IDomainEvent.cs
│ │ │ ├── IUnitOfWork.cs
│ │ │ └── Result.cs
│ │ ├── {Aggregate}/
│ │ │ ├── {Entity}.cs
│ │ │ ├── {Entity}Errors.cs
│ │ │ ├── I{Entity}Repository.cs
│ │ │ ├── ValueObjects/
│ │ │ └── Events/
│ │ └── {name}.domain.csproj
│ │
│ ├── {name}.application/
│ │ ├── Abstractions/
│ │ │ ├── Behaviors/
│ │ │ │ ├── LoggingBehavior.cs
│ │ │ │ └── ValidationBehavior.cs
│ │ │ ├── Messaging/
│ │ │ │ ├── ICommand.cs
│ │ │ │ ├── ICommandHandler.cs
│ │ │ │ ├── IQuery.cs
│ │ │ │ └── IQueryHandler.cs
│ │ │ ├── Authentication/
│ │ │ ├── Clock/
│ │ │ └── Data/
│ │ ├── {Feature}/
│ │ │ ├── Commands/ # optional parent for writes (CeroBase)
│ │ │ │ ├── Create{Entity}/ # one folder per command
│ │ │ │ └── Delete{Entity}/
│ │ │ └── Queries/ # optional parent for reads (CeroBase)
│ │ │ ├── Get{Entity}ById/
│ │ │ └── GetAll{Entities}/
│ │ ├── DependencyInjection.cs
│ │ └── {name}.application.csproj
│ │
│ ├── {name}.infrastructure/
│ │ ├── Authentication/
│ │ ├── Authorization/
│ │ ├── Clock/
│ │ ├── Configurations/
│ │ ├── Repositories/
│ │ ├── Outbox/
│ │ ├── ApplicationDbContext.cs
│ │ ├── DependencyInjection.cs
│ │ └── {name}.infrastructure.csproj
│ │
│ └── {name}.api/
│ ├── Controllers/
│ ├── Middleware/
│ ├── Extensions/
│ ├── Program.cs
│ ├── appsettings.json
│ └── {name}.api.csproj
│
├── tests/
│ ├── {name}.domain.tests/
│ ├── {name}.application.tests/
│ └── {name}.api.tests/
│
└── {SolutionName}.sln---
Step 1: Create Solution and Projects
# Create solution
dotnet new sln -n {SolutionName}
# Create projects
dotnet new classlib -n {name}.domain -o src/{name}.domain
dotnet new classlib -n {name}.application -o src/{name}.application
dotnet new classlib -n {name}.infrastructure -o src/{name}.infrastructure
dotnet new webapi -n {name}.api -o src/{name}.api
# Add projects to solution
dotnet sln add src/{name}.domain/{name}.domain.csproj
dotnet sln add src/{name}.application/{name}.application.csproj
dotnet sln add src/{name}.infrastructure/{name}.infrastructure.csproj
dotnet sln add src/{name}.api/{name}.api.csproj
# Add project references
cd src/{name}.application
dotnet add reference ../{name}.domain/{name}.domain.csproj
cd ../{name}.infrastructure
dotnet add reference ../{name}.domain/{name}.domain.csproj
dotnet add reference ../{name}.application/{name}.application.csproj
cd ../{name}.api
dotnet add reference ../{name}.application/{name}.application.csproj
dotnet add reference ../{name}.infrastructure/{name}.infrastructure.csproj---
Step 2: Domain Layer Setup
Entity Base Class
// src/{name}.domain/Abstractions/Entity.cs
namespace {name}.domain.abstractions;
public abstract class Entity
{
private readonly List<IDomainEvent> _domainEvents = new();
protected Entity(Guid id)
{
Id = id;
}
protected Entity() { } // EF Core
public Guid Id { get; init; }
public IReadOnlyList<IDomainEvent> GetDomainEvents() => _domainEvents.ToList();
public void ClearDomainEvents() => _domainEvents.Clear();
protected void RaiseDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);
}Domain Event Interface
// src/{name}.domain/Abstractions/IDomainEvent.cs
using MediatR;
namespace {name}.domain.abstractions;
public interface IDomainEvent : INotification
{
}Unit of Work Interface
// src/{name}.domain/Abstractions/IUnitOfWork.cs
namespace {name}.domain.abstractions;
public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}Result Pattern (see result-pattern skill for full implementation)
// src/{name}.domain/Abstractions/Result.cs
namespace {name}.domain.abstractions;
public class Result
{
protected Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None)
throw new InvalidOperationException();
if (!isSuccess && error == Error.None)
throw new InvalidOperationException();
IsSuccess = isSuccess;
Error = error;
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
public static Result<TValue> Success<TValue>(TValue value) => new(value, true, Error.None);
public static Result<TValue> Failure<TValue>(Error error) => new(default, false, error);
}
public class Result<TValue> : Result
{
private readonly TValue? _value;
protected internal Result(TValue? value, bool isSuccess, Error error)
: base(isSuccess, error)
{
_value = value;
}
public TValue Value => IsSuccess
? _value!
: throw new InvalidOperationException("Cannot access value of a failed result");
public static implicit operator Result<TValue>(TValue? value) =>
value is not null ? Success(value) : Failure<TValue>(Error.NullValue);
}
public record Error(string Code, string Description)
{
public static readonly Error None = new(string.Empty, string.Empty);
public static readonly Error NullValue = new("Error.NullValue", "A null value was provided");
}---
Step 3: Application Layer Setup
Package References
<!-- {name}.application.csproj -->
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.*" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.*" />
<PackageReference Include="MediatR" Version="12.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.*" />
</ItemGroup>CQRS Abstractions
// src/{name}.application/Abstractions/Messaging/ICommand.cs
using MediatR;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.messaging;
public interface ICommand : IRequest<Result> { }
public interface ICommand<TResponse> : IRequest<Result<TResponse>> { }// src/{name}.application/Abstractions/Messaging/ICommandHandler.cs
using MediatR;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.messaging;
public interface ICommandHandler<TCommand> : IRequestHandler<TCommand, Result>
where TCommand : ICommand { }
public interface ICommandHandler<TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>>
where TCommand : ICommand<TResponse> { }// src/{name}.application/Abstractions/Messaging/IQuery.cs
using MediatR;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.messaging;
public interface IQuery<TResponse> : IRequest<Result<TResponse>> { }// src/{name}.application/Abstractions/Messaging/IQueryHandler.cs
using MediatR;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.messaging;
public interface IQueryHandler<TQuery, TResponse> : IRequestHandler<TQuery, Result<TResponse>>
where TQuery : IQuery<TResponse> { }Dependency Injection
// src/{name}.application/DependencyInjection.cs
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using {name}.application.abstractions.behaviors;
namespace {name}.application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddMediatR(configuration =>
{
configuration.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
configuration.AddOpenBehavior(typeof(LoggingBehavior<,>));
configuration.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly);
return services;
}
}---
Step 4: Infrastructure Layer Setup
Package References
<!-- {name}.infrastructure.csproj -->
<ItemGroup>
<PackageReference Include="Dapper" Version="2.*" />
<PackageReference Include="EFCore.NamingConventions" Version="8.*" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.*" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.*" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.*" />
</ItemGroup>Application DbContext
// src/{name}.infrastructure/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using {name}.application.abstractions.clock;
using {name}.domain.abstractions;
namespace {name}.infrastructure;
public sealed class ApplicationDbContext : DbContext, IUnitOfWork
{
private readonly IDateTimeProvider _dateTimeProvider;
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options,
IDateTimeProvider dateTimeProvider)
: base(options)
{
_dateTimeProvider = dateTimeProvider;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// Add domain events to outbox before saving
AddDomainEventsAsOutboxMessages();
return await base.SaveChangesAsync(cancellationToken);
}
private void AddDomainEventsAsOutboxMessages()
{
// See outbox-pattern skill for implementation
}
}Dependency Injection
// src/{name}.infrastructure/DependencyInjection.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using {name}.application.abstractions.clock;
using {name}.application.abstractions.data;
using {name}.domain.abstractions;
using {name}.infrastructure.clock;
namespace {name}.infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddTransient<IDateTimeProvider, DateTimeProvider>();
AddPersistence(services, configuration);
AddAuthentication(services, configuration);
AddAuthorization(services);
AddHealthChecks(services, configuration);
return services;
}
private static void AddPersistence(IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("Database")
?? throw new ArgumentNullException(nameof(configuration));
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention();
});
services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<ApplicationDbContext>());
services.AddSingleton<ISqlConnectionFactory>(_ => new SqlConnectionFactory(connectionString));
// Register repositories here
// services.AddScoped<I{Entity}Repository, {Entity}Repository>();
}
private static void AddAuthentication(IServiceCollection services, IConfiguration configuration)
{
// See jwt-authentication skill
}
private static void AddAuthorization(IServiceCollection services)
{
// See permission-authorization skill
}
private static void AddHealthChecks(IServiceCollection services, IConfiguration configuration)
{
services.AddHealthChecks()
.AddNpgSql(configuration.GetConnectionString("Database")!);
}
}---
Step 5: API Layer Setup
Program.cs
// src/{name}.api/Program.cs
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Serilog;
using {name}.application;
using {name}.infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, configuration) =>
configuration.ReadFrom.Configuration(context.Configuration));
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseSerilogRequestLogging();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();appsettings.json
{
"ConnectionStrings": {
"Database": "Host=localhost;Port=5432;Database={name}-db;Username=postgres;Password=postgres"
},
"Serilog": {
"Using": ["Serilog.Sinks.Console"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"WriteTo": [{ "Name": "Console" }],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
},
"Authentication": {
"Audience": "{name}",
"Issuer": "{name}-auth",
"SecretKey": "your-secret-key-at-least-32-characters-long"
}
}---
Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Solution | PascalCase | HumanPwr |
| Projects | lowercase with dots | humanpwr.domain |
| Namespaces | lowercase | humanpwr.domain.users |
| Classes | PascalCase | UserRepository |
| Interfaces | IPascalCase | IUserRepository |
| Commands | {Action}{Entity}Command | CreateUserCommand |
| Queries | Get{Entity}Query | GetUserByIdQuery |
| Handlers | {Command/Query}Handler | CreateUserCommandHandler |
| Responses | {Entity}Response | UserResponse |
| Domain Events | {Entity}{Action}DomainEvent | UserCreatedDomainEvent |
| Errors | {Entity}Errors | UserErrors |
---
Critical Rules
1. Domain has ZERO dependencies on other layers or external packages (except MediatR for IDomainEvent) 2. Application depends only on Domain - no infrastructure concerns 3. Infrastructure implements interfaces defined in Domain/Application 4. API only references Application and Infrastructure - never Domain directly for services 5. Use Result pattern instead of exceptions for business logic errors 6. Commands modify state, Queries read state (CQRS) 7. One handler per Command/Query - no shared handlers 8. Repositories are per aggregate root - not per entity 9. Domain events are raised in domain, handled in application layer 10. Always use CancellationToken in async operations 11. Application features use one folder per command or query under {Feature}/ — either flat {Feature}/{UseCase}/ or grouped {Feature}/Queries/{UseCase}/ and {Feature}/Commands/{UseCase}/ (see Application `{Feature}` folder layout above); never one folder mixing several use cases
---
Related Skills
cqrs-command-generator- Generate Commands with handlerscqrs-query-generator- Generate Queries with handlersdomain-entity-generator- Generate Domain entitiesrepository-pattern- Generate Repositoriesef-core-configuration- Generate EF configurationsresult-pattern- Implement Result patternpipeline-behaviors- Create MediatR behaviors
Related skills
FAQ
What .NET version and libraries does it target?
It targets .NET 8+ and uses MediatR, FluentValidation, Entity Framework Core and Dapper.
Where does the entity repository interface live?
Persistence-only repositories go in the Domain layer; DTO-returning or cross-aggregate reads go in Application abstractions, and Domain never references Application types.