.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.
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}/
│ │ │ ├── Create{Entity}/
│ │ │ ├── Update{Entity}/
│ │ │ ├── Delete{Entity}/
│ │ │ └── Get{Entity}/
│ │ ├── 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