.NET 9 架構對決:清潔架構 vs. 垂直切片架構,誰更適合現代開發?
基于 .NET 9 的最新發展以及 .NET 社區不斷演進的架構模式,讓我們探討這兩種流行的方法如何得到增強,以及哪種最適合現代應用程序開發。
.NET 9:兩種架構的游戲規則改變者
.NET 9 引入了顯著的改進,使兩種架構方法都受益:
性能增強
? .NET 9 最小 API 性能:每秒請求數提升 15%,內存消耗減少 93%
? 改進的垃圾回收 (GC):動態適應應用程序大小,取代了傳統的服務器 GC (Server GC)
? 運行時優化:增強的循環優化、內聯和 ARM64 矢量化
? 異常處理:異常處理速度提升 2-4 倍,這對錯誤處理模式至關重要
支持現代架構的新特性
? 增強的 System.Text.Json:支持可空引用類型和 JSON 模式導出
? LINQ 改進:新的 CountBy 和 AggregateBy 方法,用于更好的數據處理
? 高級功能開關:通過修剪 (trimming) 支持更好地控制應用程序功能
.NET 9 中的清潔架構:增強與成熟
現代清潔架構實現
// .NET 9 清潔架構與增強特性
namespaceCleanArchitecture.Domain.Entities;
publicclassProduct
{
publicint Id { get; privateset; }
publicrequiredstring Name { get; privateset; }
publicdecimal Price { get; privateset; }
public DateTime CreatedAt { get; privateset; } = DateTime.UtcNow;
// 利用 .NET 9 性能改進的領域事件
privatereadonly List<IDomainEvent> _domainEvents = [];
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
public static Product Create(string name, decimal price)
{
var product = new Product { Name = name, Price = price };
product.AddDomainEvent(new ProductCreatedEvent(product.Id));
return product;
}
public void UpdatePrice(decimal newPrice)
{
if (newPrice <= 0)
thrownew ArgumentException("Price must be positive", nameof(newPrice));
Price = newPrice;
AddDomainEvent(new ProductPriceUpdatedEvent(Id, newPrice));
}
private void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);
}
// 應用層與 .NET 9 CQRS 實現
namespaceCleanArchitecture.Application.Products.Commands;
public record CreateProductCommand(string Name, decimal Price) : IRequest<Result<ProductResponse>>;
publicclassCreateProductCommandHandler : IRequestHandler<CreateProductCommand, Result<ProductResponse>>
{
privatereadonly IProductRepository _repository;
privatereadonly IUnitOfWork _unitOfWork;
public CreateProductCommandHandler(IProductRepository repository, IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
publicasync Task<Result<ProductResponse>> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var product = Product.Create(request.Name, request.Price);
await _repository.AddAsync(product, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result<ProductResponse>.Success(new ProductResponse(product.Id, product.Name, product.Price));
}
}
// .NET 9 最小 API 端點
namespaceCleanArchitecture.WebApi.Endpoints;
publicstaticclassProductEndpoints
{
public static void MapProductEndpoints(this IEndpointRouteBuilder app)
{
vargroup = app.MapGroup("/api/products").WithTags("Products");
group.MapPost("/", async (CreateProductCommand command, IMediator mediator) =>
{
var result = await mediator.Send(command);
return result.IsSuccess
? Results.Created($"/api/products/{result.Value.Id}", result.Value)
: Results.BadRequest(result.Errors);
})
.WithName("CreateProduct")
.WithOpenApi();
group.MapGet("/{id:int}", async (int id, IMediator mediator) =>
{
var result = await mediator.Send(new GetProductQuery(id));
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
})
.WithName("GetProduct")
.WithOpenApi();
}
}.NET 9 清潔架構項目結構
MyApp.CleanArchitecture/
├── src/
│ ├── Domain/
│ │ ├── Entities/
│ │ │ ├── Product.cs
│ │ │ └── User.cs
│ │ ├── Events/
│ │ │ ├── ProductCreatedEvent.cs
│ │ │ └── ProductPriceUpdatedEvent.cs
│ │ ├── Interfaces/
│ │ │ ├── IProductRepository.cs
│ │ │ └── IUnitOfWork.cs
│ │ └── Common/
│ │ ├── BaseEntity.cs
│ │ ├── IDomainEvent.cs
│ │ └── Result.cs
│ ├── Application/
│ │ ├── Products/
│ │ │ ├── Commands/
│ │ │ │ ├── CreateProduct/
│ │ │ │ │ ├── CreateProductCommand.cs
│ │ │ │ │ ├── CreateProductCommandHandler.cs
│ │ │ │ │ └── CreateProductCommandValidator.cs
│ │ │ │ └── UpdateProduct/
│ │ │ └── Queries/
│ │ │ └── GetProduct/
│ │ │ ├── GetProductQuery.cs
│ │ │ └── GetProductQueryHandler.cs
│ │ ├── Common/
│ │ │ ├── Behaviors/
│ │ │ │ ├── ValidationBehavior.cs
│ │ │ │ └── LoggingBehavior.cs
│ │ │ └── Mappings/
│ │ │ └── ProductProfile.cs
│ │ └── DependencyInjection.cs
│ ├── Infrastructure/
│ │ ├── Persistence/
│ │ │ ├── ApplicationDbContext.cs
│ │ │ ├── Repositories/
│ │ │ │ └── ProductRepository.cs
│ │ │ └── Configurations/
│ │ │ └── ProductConfiguration.cs
│ │ ├── Services/
│ │ └── DependencyInjection.cs
│ └── WebApi/
│ ├── Endpoints/
│ │ ├── ProductEndpoints.cs
│ │ └── UserEndpoints.cs
│ ├── Program.cs
│ ├── GlobalUsings.cs
│ └── appsettings.json
└── tests/
├── Domain.Tests/
├── Application.Tests/
└── WebApi.Tests/.NET 9 中的垂直切片架構:簡化與現代
利用 .NET 9 最小 API 增強
垂直切片架構與 .NET 9 改進的最小 API 相結合,創造了極其強大和高性能的解決方案:
// .NET 9 垂直切片實現
namespaceVideoGameApi.Features.Games.GetAll;
publicstaticclassGetAllGames
{
// 內嵌類型以提高內聚性
public record Query() : IRequest<Result<IEnumerable<GameResponse>>>;
public record GameResponse(int Id, string Title, string Genre, int ReleaseYear);
// 驗證器
publicclassQueryValidator : AbstractValidator<Query>
{
public QueryValidator()
{
// 如果需要,添加查詢驗證規則
}
}
// 利用 .NET 9 性能優化的處理程序
publicclassHandler : IRequestHandler<Query, Result<IEnumerable<GameResponse>>>
{
privatereadonly IGameRepository _repository;
public Handler(IGameRepository repository)
{
_repository = repository;
}
publicasync Task<Result<IEnumerable<GameResponse>>> Handle(Query request, CancellationToken cancellationToken)
{
var games = await _repository.GetAllAsync(cancellationToken);
// 使用 .NET 9 LINQ 改進
var response = games.Select(g => new GameResponse(g.Id, g.Title, g.Genre, g.ReleaseYear));
return Result<IEnumerable<GameResponse>>.Success(response);
}
}
// .NET 9 最小 API 端點
public static void MapEndpoint(IEndpointRouteBuilder app)
{
app.MapGet("/games", async (IMediator mediator) =>
{
var result = await mediator.Send(new Query());
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Errors);
})
.WithName("GetAllGames")
.WithTags("Games")
.WithOpenApi()
.Produces<IEnumerable<GameResponse>>(200)
.Produces(400);
}
}
// 功能注冊
namespaceVideoGameApi.Features.Games.Create;
publicstaticclassCreateGame
{
public record Command(string Title, string Genre, int ReleaseYear) : IRequest<Result<GameResponse>>;
public record GameResponse(int Id, string Title, string Genre, int ReleaseYear);
publicclassCommandValidator : AbstractValidator<Command>
{
public CommandValidator()
{
RuleFor(x => x.Title).NotEmpty().MaximumLength(200);
RuleFor(x => x.Genre).NotEmpty().MaximumLength(100);
RuleFor(x => x.ReleaseYear).GreaterThan(1900).LessThanOrEqualTo(DateTime.Now.Year);
}
}
publicclassHandler : IRequestHandler<Command, Result<GameResponse>>
{
privatereadonly GameDbContext _context;
public Handler(GameDbContext context)
{
_context = context;
}
publicasync Task<Result<GameResponse>> Handle(Command request, CancellationToken cancellationToken)
{
var game = new Game
{
Title = request.Title,
Genre = request.Genre,
ReleaseYear = request.ReleaseYear
};
_context.Games.Add(game);
await _context.SaveChangesAsync(cancellationToken);
return Result<GameResponse>.Success(new GameResponse(game.Id, game.Title, game.Genre, game.ReleaseYear));
}
}
public static void MapEndpoint(IEndpointRouteBuilder app)
{
app.MapPost("/games", async (Command command, IMediator mediator) =>
{
var result = await mediator.Send(command);
return result.IsSuccess
? Results.Created($"/games/{result.Value.Id}", result.Value)
: Results.BadRequest(result.Errors);
})
.WithName("CreateGame")
.WithTags("Games")
.WithOpenApi();
}
}.NET 9 垂直切片項目結構
VideoGameApi.VerticalSlice/
├── Features/
│ ├── Games/
│ │ ├── GetAll/
│ │ │ ├── GetAllGames.cs (Query, Handler, Endpoint)
│ │ │ └── GetAllGamesTests.cs
│ │ ├── GetById/
│ │ │ ├── GetGameById.cs
│ │ │ └── GetGameByIdTests.cs
│ │ ├── Create/
│ │ │ ├── CreateGame.cs
│ │ │ └── CreateGameTests.cs
│ │ ├── Update/
│ │ │ ├── UpdateGame.cs
│ │ │ └── UpdateGameTests.cs
│ │ └── Delete/
│ │ ├── DeleteGame.cs
│ │ └── DeleteGameTests.cs
│ ├── Players/
│ │ ├── Register/
│ │ ├── Login/
│ │ └── GetProfile/
│ └── Shared/
│ ├── Models/
│ │ ├── Game.cs
│ │ └── Player.cs
│ ├── Common/
│ │ ├── Result.cs
│ │ ├── IRepository.cs
│ │ └── BaseEntity.cs
│ └── Data/
│ ├── GameDbContext.cs
│ └── GameRepository.cs
├── Program.cs
├── GlobalUsings.cs
└── appsettings.json.NET 9 增強的 Program.cs 配置
using VideoGameApi.Features.Games.GetAll;
using VideoGameApi.Features.Games.Create;
using VideoGameApi.Features.Games.Update;
using VideoGameApi.Features.Games.Delete;
var builder = WebApplication.CreateBuilder(args);
// .NET 9 增強的服務注冊
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 利用 .NET 9 改進的 Entity Framework
builder.Services.AddDbContext<GameDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// 利用 .NET 9 性能優化的 MediatR
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
cfg.AddBehavior<ValidationBehavior<,>>();
cfg.AddBehavior<LoggingBehavior<,>>();
});
// FluentValidation
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
// .NET 9 增強的健康檢查
builder.Services.AddHealthChecks()
.AddDbContext<GameDbContext>();
var app = builder.Build();
// .NET 9 增強的管道
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// 從垂直切片映射端點
GetAllGames.MapEndpoint(app);
CreateGame.MapEndpoint(app);
UpdateGame.MapEndpoint(app);
DeleteGame.MapEndpoint(app);
// 健康檢查
app.MapHealthChecks("/health");
app.Run();混合方法:.NET 9 中兩全其美
使用 .NET 9 的切片式清潔架構
// 結合 .NET 9 特性融合兩種方法
namespaceHybridApp.Features.Products.Create;
// 遵循垂直切片組織的清潔架構原則
publicstaticclassCreateProduct
{
// 遵循 CQRS 的請求/響應
public record Command(string Name, decimal Price, int CategoryId) : IRequest<Result<Response>>;
public record Response(int Id, string Name, decimal Price);
// 驗證
publicclassValidator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Price).GreaterThan(0);
RuleFor(x => x.CategoryId).GreaterThan(0);
}
}
// 領域邏輯處理程序 (清潔架構)
publicclassHandler : IRequestHandler<Command, Result<Response>>
{
privatereadonly IProductRepository _repository; // 領域接口
privatereadonly IUnitOfWork _unitOfWork; // 領域接口
privatereadonly IDomainEventDispatcher _eventDispatcher; // 領域接口
public Handler(IProductRepository repository, IUnitOfWork unitOfWork, IDomainEventDispatcher eventDispatcher)
{
_repository = repository;
_unitOfWork = unitOfWork;
_eventDispatcher = eventDispatcher;
}
publicasync Task<Result<Response>> Handle(Command request, CancellationToken cancellationToken)
{
// 領域邏輯
var product = Product.Create(request.Name, request.Price, request.CategoryId);
// 倉儲模式 (清潔架構)
await _repository.AddAsync(product, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
// 領域事件
await _eventDispatcher.DispatchAsync(product.DomainEvents, cancellationToken);
return Result<Response>.Success(new Response(product.Id, product.Name, product.Price));
}
}
// .NET 9 最小 API 端點 (垂直切片)
public static void MapEndpoint(IEndpointRouteBuilder app)
{
app.MapPost("/products", async (Command command, IMediator mediator) =>
{
var result = await mediator.Send(command);
return result.IsSuccess
? Results.Created($"/products/{result.Value.Id}", result.Value)
: Results.BadRequest(result.Errors);
})
.WithName("CreateProduct")
.WithTags("Products")
.WithOpenApi()
.RequireAuthorization() // 清潔架構安全性
.AddEndpointFilter<ValidationFilter<Command>>(); // 清潔架構驗證
}
}.NET 9 中的性能對比
基準測試:清潔架構 vs. 垂直切片
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net90)]
publicclassArchitectureBenchmarks
{
privatereadonly IMediator _mediator;
privatereadonly HttpClient _httpClient;
[GlobalSetup]
public async Task Setup()
{
// 設置兩種架構方法
var cleanArchApp = CreateCleanArchitectureApp();
var verticalSliceApp = CreateVerticalSliceApp();
}
[Benchmark]
public async Task CleanArchitecture_CreateProduct()
{
var command = new CleanArch.CreateProductCommand("Test Product", 99.99m, 1);
await _mediator.Send(command);
}
[Benchmark]
public async Task VerticalSlice_CreateProduct()
{
var command = new VerticalSlice.CreateProduct.Command("Test Product", 99.99m, 1);
await _mediator.Send(command);
}
[Benchmark]
public async Task CleanArchitecture_GetProducts()
{
var query = new CleanArch.GetProductsQuery(1, 10);
await _mediator.Send(query);
}
[Benchmark]
public async Task VerticalSlice_GetProducts()
{
var query = new VerticalSlice.GetAllProducts.Query(1, 10);
await _mediator.Send(query);
}
}
/*
典型的 .NET 9 結果:
| Method | Mean | Error | StdDev | Allocated |
|-------------------------- |---------:|--------:|--------:|----------:|
| CleanArchitecture_Create | 1.234 ms | 0.024 ms| 0.021 ms| 1.2 KB |
| VerticalSlice_Create | 1.156 ms | 0.019 ms| 0.016 ms| 0.9 KB |
| CleanArchitecture_Get | 2.456 ms | 0.041 ms| 0.038 ms| 2.1 KB |
| VerticalSlice_Get | 2.187 ms | 0.033 ms| 0.029 ms| 1.7 KB |
*/.NET 9 中何時選擇每種方法
選擇清潔架構當:
? 復雜業務邏輯:具有復雜領域規則的多界限上下文 (bounded contexts)
? 大型企業應用程序:需要嚴格的關注點分離和可測試性
? 團隊規模:需要清晰邊界和職責的大型團隊
? 長期維護:預期會隨時間顯著演進的應用程序
? 法規要求:需要審計跟蹤和合規性的應用程序
選擇垂直切片架構當:
? 功能驅動開發:快速交付獨立功能
? CRUD 密集型應用程序:不證明需要復雜分層的簡單業務邏輯
? 中小型團隊:減少協調開銷
? 微服務:每個服務圍繞業務能力組織
? 現代 .NET 9 API:利用最小 API 性能改進
選擇混合方法當:
? 增長中的應用程序:從切片開始,根據需要演進到清潔架構
? 平衡的復雜性:既需要功能聚焦也需要關注點分離
? 團隊靈活性:開發人員對兩種方法都感到適應
? 兩全其美:想要具有垂直切片組織的清潔架構優勢
.NET 9 模板推薦
清潔架構模板
# Jason Taylor 的清潔架構模板 (已為 .NET 9 更新)
dotnet new install Clean.Architecture.Solution.Template
dotnet new ca-sln -n MyCleanApp
# Sam 的增強型清潔架構模板
dotnet new install Sam.CleanArchitecture.Template::9.2.0
dotnet new ca-api -n MyEnterpriseApp垂直切片模板
# 垂直切片架構模板
dotnet new install VerticalSliceArchitecture.Template
dotnet new vsa -n MyVerticalSliceApp
# 自定義最小 API 模板
dotnet new webapi -n MyMinimalApiApp -minimal結論:.NET 9 視角
在 .NET 9 中,清潔架構和垂直切片架構之間的選擇比以往任何時候都更加細致入微:
.NET 9 的關鍵要點:
? 性能至關重要:.NET 9 的最小 API 改進使得垂直切片架構在高性能場景中更具吸引力
? 兩者可以共存:結合清潔架構原則和垂直切片組織的混合方法日益流行
? 現代開發:.NET 9 的增強特性使兩種方法都受益,但垂直切片與最小 API 配合尤其出色
? 團隊和背景:團隊的經驗、應用程序的復雜性和性能要求應驅動決策
? 演進路徑:從簡單的垂直切片開始,隨著復雜性增長而演進到清潔架構
.NET 9 的最終建議:
? 新項目:使用 .NET 9 最小 API 從垂直切片架構開始
? 復雜領域:在層內使用切片式組織的清潔架構
? 高性能:利用 .NET 9 的最小 API 改進與垂直切片
? 企業應用:使用具有 .NET 9 增強功能的清潔架構以實現可維護性
最好的架構是滿足您的特定需求,同時利用 .NET 9 性能改進和現代開發模式的架構。


























