mcpField
D
dotnet-clean-architecture

dotnet-clean-architecture

github.com/ronnythedev开发77135 次安装15 周前更新
代码仓库

关于 dotnet-clean-architecture

用于搭建遵循 Clean Architecture 的 .NET 解决方案,面向创建分层应用的开发者。内容涵盖项目结构、依赖方向,以及 CQRS 和 Result 等模式。

主要功能

  1. 01四层解决方案结构
  2. 02依赖方向规范
  3. 03领域与应用层抽象
  4. 04CQRS 命令与查询模式
  5. 05依赖注入设置
  6. 06命名约定与设计规则

适用场景

  1. 01启动分层 .NET 解决方案
  2. 02组织领域层和应用层项目
  3. 03在应用逻辑中采用 CQRS

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ronnythedev/dotnet-clean-architecture-skills --skill dotnet-clean-architecture

安装后在 Claude Code 中输入/dotnet-clean-architecture确认已加载。

详细介绍

.NET 整洁架构项目脚手架

概览

此技能可生成完整的 .NET 解决方案,遵循整洁架构(也称为洋葱架构或六边形架构)。该架构通过划分不同层来确保关注点分离,并规定依赖关系单向指向内部。

架构层

┌─────────────────────────────────────────────────────────────┐
│                        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         │
└─────────────────────────────────────────────────────────────┘

依赖规则:依赖关系指向内部。Domain 没有依赖项。Application 仅依赖 Domain。Infrastructure 实现 Domain/Application 中定义的接口。

快速参考

任务命令/操作
创建解决方案dotnet new sln -n {SolutionName}
创建 Domain 项目dotnet new classlib -n {name}.domain
创建 Application 项目dotnet new classlib -n {name}.application
创建 Infrastructure 项目dotnet new classlib -n {name}.infrastructure
创建 API 项目dotnet new webapi -n {name}.api
将项目添加到解决方案dotnet sln add src/{project}/{project}.csproj
添加项目引用dotnet add reference ../other/other.csproj

项目结构

{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

第 1 步:创建解决方案和项目

# 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

第 2 步:设置 Domain 层

Entity 基类
// 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 接口
// src/{name}.domain/Abstractions/IDomainEvent.cs
using MediatR;

namespace {name}.domain.abstractions;

public interface IDomainEvent : INotification
{
}
Unit of Work 接口
// src/{name}.domain/Abstractions/IUnitOfWork.cs
namespace {name}.domain.abstractions;

public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
Result 模式(完整实现请参见 result-pattern 技能)
// 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");
}

第 3 步:设置 Application 层

包引用

<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 抽象
// 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> { }
依赖注入
// 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;
    }
}

第 4 步:设置 Infrastructure 层

包引用

<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
    }
}
依赖注入
// 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")!);
    }
}

第 5 步:设置 API 层

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

命名约定

项目约定示例
解决方案PascalCaseHumanPwr
项目小写并以点分隔humanpwr.domain
命名空间小写humanpwr.domain.users
类PascalCaseUserRepository
接口IPascalCaseIUserRepository
命令{Action}{Entity}CommandCreateUserCommand
查询Get{Entity}QueryGetUserByIdQuery
处理器{Command/Query}HandlerCreateUserCommandHandler
响应{Entity}ResponseUserResponse
Domain Event{Entity}{Action}DomainEventUserCreatedDomainEvent
错误{Entity}ErrorsUserErrors

关键规则

  1. Domain 不依赖其他层或外部包(IDomainEvent 的 MediatR 除外)
  2. Application 仅依赖 Domain,不涉及基础设施
  3. Infrastructure 实现在 Domain/Application 中定义的接口
  4. API 仅引用 Application 和 Infrastructure,服务不得直接引用 Domain
  5. 业务逻辑错误使用 Result 模式,而非异常
  6. 命令修改状态,查询读取状态(CQRS)
  7. 每个 Command/Query 对应一个处理器,不得共用处理器
  8. Repository 按聚合根划分,而非按 Entity 划分
  9. Domain Event 在 Domain 中引发,并由 Application 层处理
  10. 异步操作始终使用 CancellationToken

相关技能

  • dotnet-cqrs-command-generator - 生成带处理器的 Command
  • dotnet-cqrs-query-generator - 生成带处理器的 Query
  • dotnet-domain-entity-generator - 生成 Domain Entity
  • dotnet-repository-pattern - 生成 Repository
  • dotnet-ef-core-configuration - 生成 EF 配置
  • dotnet-result-pattern - 实现 Result 模式
  • dotnet-pipeline-behaviors - 创建 MediatR 行为

评论

支持 Markdown