dotnet-clean-architecture

.NET Clean Architecture Project Scaffolder

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dotnet-clean-architecture" with this command: npx skills add ronnythedev/dotnet-clean-architecture-skills/ronnythedev-dotnet-clean-architecture-skills-dotnet-clean-architecture

.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&#x3C;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&#x3C;TValue> Success&#x3C;TValue>(TValue value) => new(value, true, Error.None);
public static Result&#x3C;TValue> Failure&#x3C;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&#x3C;TValue>(TValue? value) =>
    value is not null ? Success(value) : Failure&#x3C;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&#x3C;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&#x3C;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&#x3C;ApplicationDbContext>(options =>
    {
        options.UseNpgsql(connectionString)
               .UseSnakeCaseNamingConvention();
    });

    services.AddScoped&#x3C;IUnitOfWork>(sp => sp.GetRequiredService&#x3C;ApplicationDbContext>());

    services.AddSingleton&#x3C;ISqlConnectionFactory>(_ => new SqlConnectionFactory(connectionString));

    // Register repositories here
    // services.AddScoped&#x3C;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

  • Domain has ZERO dependencies on other layers or external packages (except MediatR for IDomainEvent)

  • Application depends only on Domain - no infrastructure concerns

  • Infrastructure implements interfaces defined in Domain/Application

  • API only references Application and Infrastructure - never Domain directly for services

  • Use Result pattern instead of exceptions for business logic errors

  • Commands modify state, Queries read state (CQRS)

  • One handler per Command/Query - no shared handlers

  • Repositories are per aggregate root - not per entity

  • Domain events are raised in domain, handled in application layer

  • Always use CancellationToken in async operations

Related Skills

  • cqrs-command-generator

  • Generate Commands with handlers

  • cqrs-query-generator

  • Generate Queries with handlers

  • domain-entity-generator

  • Generate Domain entities

  • repository-pattern

  • Generate Repositories

  • ef-core-configuration

  • Generate EF configurations

  • result-pattern

  • Implement Result pattern

  • pipeline-behaviors

  • Create MediatR behaviors

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

unit-testing

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

repository-pattern

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

permission-authorization

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

dapper-query-builder

No summary provided by upstream source.

Repository SourceNeeds Review