本文へ移動
cccskills
無料GitHub で公開

dotnet-clean-architecture

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)

  • SKILL.md18.7 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

.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

TaskCommand/Action
Create solutiondotnet new sln -n {SolutionName}
Create Domain projectdotnet new classlib -n {name}.domain
Create Application projectdotnet new classlib -n {name}.application
Create Infrastructure projectdotnet new classlib -n {name}.infrastructure
Create API projectdotnet new webapi -n {name}.api
Add project to solutiondotnet sln add src/{project}/{project}.csproj
Add project referencedotnet 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

ItemConventionExample
SolutionPascalCaseHumanPwr
Projectslowercase with dotshumanpwr.domain
Namespaceslowercasehumanpwr.domain.users
ClassesPascalCaseUserRepository
InterfacesIPascalCaseIUserRepository
Commands{Action}{Entity}CommandCreateUserCommand
QueriesGet{Entity}QueryGetUserByIdQuery
Handlers{Command/Query}HandlerCreateUserCommandHandler
Responses{Entity}ResponseUserResponse
Domain Events{Entity}{Action}DomainEventUserCreatedDomainEvent
Errors{Entity}ErrorsUserErrors

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

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Generates audit trail infrastructure for entities. Implements IAuditable interface, EF Core SaveChanges interceptor, and automatic population of CreatedAt, UpdatedAt, CreatedBy, and UpdatedBy fields.

日本語の概要は準備中です。原文の説明を表示しています。

ronnythedev/dotnet-clean-architecture-skills752026年7月14日 更新

Generates CQRS Commands with Handlers, Validators, and Request DTOs following Clean Architecture patterns. Commands represent actions that modify state and return Result types for proper error handling.

日本語の概要は準備中です。原文の説明を表示しています。

ronnythedev/dotnet-clean-architecture-skills752026年7月14日 更新

Generates CQRS Queries with Handlers and Response DTOs for read operations. Uses Dapper for optimized read queries, bypassing the domain model for better performance.

日本語の概要は準備中です。原文の説明を表示しています。

ronnythedev/dotnet-clean-architecture-skills752026年7月14日 更新

Generates optimized read queries using Dapper. Includes multi-mapping for joins, pagination, dynamic filtering, CTEs, and best practices for high-performance data access.

日本語の概要は準備中です。原文の説明を表示しています。

ronnythedev/dotnet-clean-architecture-skills752026年7月14日 更新

Generates Domain Entities following DDD principles with factory methods, private setters, domain events, and proper encapsulation. Supports aggregate roots, child entities, and value objects.

日本語の概要は準備中です。原文の説明を表示しています。

ronnythedev/dotnet-clean-architecture-skills752026年7月14日 更新

Generates Domain Events and their handlers following DDD patterns. Implements event raising in entities, MediatR notification handlers, and the Outbox pattern for reliable event processing.

日本語の概要は準備中です。原文の説明を表示しています。

ronnythedev/dotnet-clean-architecture-skills752026年7月14日 更新

ronnythedev のスキルをすべて見る

このスキルの問題を報告する