关于 dotnet-clean-architecture
用于搭建遵循 Clean Architecture 的 .NET 解决方案,面向创建分层应用的开发者。内容涵盖项目结构、依赖方向,以及 CQRS 和 Result 等模式。
主要功能
- 01四层解决方案结构
- 02依赖方向规范
- 03领域与应用层抽象
- 04CQRS 命令与查询模式
- 05依赖注入设置
- 06命名约定与设计规则
适用场景
- 01启动分层 .NET 解决方案
- 02组织领域层和应用层项目
- 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"
}
}
命名约定
| 项目 | 约定 | 示例 |
|---|---|---|
| 解决方案 | PascalCase | HumanPwr |
| 项目 | 小写并以点分隔 | humanpwr.domain |
| 命名空间 | 小写 | humanpwr.domain.users |
| 类 | PascalCase | UserRepository |
| 接口 | IPascalCase | IUserRepository |
| 命令 | {Action}{Entity}Command | CreateUserCommand |
| 查询 | Get{Entity}Query | GetUserByIdQuery |
| 处理器 | {Command/Query}Handler | CreateUserCommandHandler |
| 响应 | {Entity}Response | UserResponse |
| Domain Event | {Entity}{Action}DomainEvent | UserCreatedDomainEvent |
| 错误 | {Entity}Errors | UserErrors |
关键规则
- Domain 不依赖其他层或外部包(IDomainEvent 的 MediatR 除外)
- Application 仅依赖 Domain,不涉及基础设施
- Infrastructure 实现在 Domain/Application 中定义的接口
- API 仅引用 Application 和 Infrastructure,服务不得直接引用 Domain
- 业务逻辑错误使用 Result 模式,而非异常
- 命令修改状态,查询读取状态(CQRS)
- 每个 Command/Query 对应一个处理器,不得共用处理器
- Repository 按聚合根划分,而非按 Entity 划分
- Domain Event 在 Domain 中引发,并由 Application 层处理
- 异步操作始终使用 CancellationToken
相关技能
dotnet-cqrs-command-generator- 生成带处理器的 Commanddotnet-cqrs-query-generator- 生成带处理器的 Querydotnet-domain-entity-generator- 生成 Domain Entitydotnet-repository-pattern- 生成 Repositorydotnet-ef-core-configuration- 生成 EF 配置dotnet-result-pattern- 实现 Result 模式dotnet-pipeline-behaviors- 创建 MediatR 行为
评论
支持 Markdown