All skills
ronnythedev avatar

/01-dotnet-clean-architecture

@184ee6f

Scaffolds a complete .NET solution following Clean Architecture principles with proper layer separation (API, Application, Domain, Infrastructure). Creates project structure, dependency injection setup, and cross-cutting concerns configuration.

  • 1 file
  • 18.7 KB
  • Updated 4 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/ronnythedev/dotnet-clean-architecture-skills/01-dotnet-clean-architecture

This session only. Nothing lands on disk.

SKILL.md

โ‰ˆ69 tokens always: the name and description. โ‰ˆ4.7k when used: this file.

.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}.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

Related Skills

  • dotnet-cqrs-command-generator - Generate Commands with handlers
  • dotnet-cqrs-query-generator - Generate Queries with handlers
  • dotnet-domain-entity-generator - Generate Domain entities
  • dotnet-repository-pattern - Generate Repositories
  • dotnet-ef-core-configuration - Generate EF configurations
  • dotnet-result-pattern - Implement Result pattern
  • dotnet-pipeline-behaviors - Create MediatR behaviors

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at 184ee6f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 4 months ago
version
1.0.0
language
C#
framework
.NET 8+
Other metadata
dependencies
MediatR, FluentValidation, Entity Framework Core, Dapper

README badge

README badge for ronnythedev/dotnet-clean-architecture-skills/01-dotnet-clean-architecture