.NET 2026年のClean Architecture: CQRS、MediatR、面接で問われる設計パターン
.NETでのClean Architecture実装を徹底解説。CQRS、MediatR 14、パイプラインビヘイビアの実践的なコード例と、シニア開発者面接で頻出の設計質問への回答方法を紹介します。

.NETにおけるClean Architectureは、ビジネスロジックをインフラストラクチャの関心事から分離し、テスト容易性、保守性、スケーラビリティを向上させる設計手法です。CQRS(Command Query Responsibility Segregation)とMediatRを組み合わせることで、エンタープライズアプリケーション構築における構造化されたアプローチを実現できます。この設計パターンは、シニア.NET開発者の面接において頻繁に評価される重要なスキルです。
Clean Architectureに関する質問は、シニア.NET面接の70%以上で出題されます。面接官は依存関係の規則について説明できることを期待しています:依存関係は内側に向かい、ドメイン層は外部フレームワークへの依存を持ちません。
Clean Architectureのレイヤー構成と依存関係の規則
ASP.NET Core 10向けのArdalis Clean Architectureテンプレートは、コードを4つの同心円状のレイヤーに整理します。各レイヤーには特定の責務と厳格な依存関係の規則があります。
// Core/Domain Layer - No external dependencies
// src/Core/Domain/Entities/Order.cs
namespace Core.Domain.Entities;
public class Order
{
public Guid Id { get; private set; }
public string CustomerEmail { get; private set; }
public List<OrderLine> Lines { get; private set; } = new();
public OrderStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; }
// Domain logic encapsulated in the entity
public void AddLine(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot modify a submitted order");
var existingLine = Lines.FirstOrDefault(l => l.ProductId == product.Id);
if (existingLine is not null)
{
existingLine.IncreaseQuantity(quantity);
return;
}
Lines.Add(new OrderLine(product.Id, product.Price, quantity));
}
public void Submit()
{
if (!Lines.Any())
throw new InvalidOperationException("Cannot submit an empty order");
Status = OrderStatus.Submitted;
}
}ドメイン層には、エンティティ、値オブジェクト、ドメインサービスが含まれます。データベース、HTTP、外部フレームワークについては一切知識を持ちません。この設計により、ビジネスルールの純粋性が保たれ、単体テストが容易になります。
CQRSパターン:読み取りと書き込みの分離
CQRSは、操作をCommand(状態を変更する書き込み操作)とQuery(データを返す読み取り操作)に分離します。この分離により、各パスの独立したスケーリングと最適化が可能になります。
// UseCases Layer - Commands and Queries
// src/UseCases/Orders/Commands/CreateOrder/CreateOrderCommand.cs
namespace UseCases.Orders.Commands.CreateOrder;
public record CreateOrderCommand(
string CustomerEmail,
List<OrderLineDto> Lines
) : IRequest<Result<Guid>>;
public record OrderLineDto(Guid ProductId, int Quantity);Commandは最小限のデータを返します。通常は成功インジケーターまたは新規作成されたIDのみです。Queryはコンシューマー向けに最適化されたDTOを返します。
namespace UseCases.Orders.Queries.GetOrderById;
public record GetOrderByIdQuery(Guid OrderId) : IRequest<Result<OrderDetailsDto>>;
public record OrderDetailsDto(
Guid Id,
string CustomerEmail,
string Status,
decimal TotalAmount,
List<OrderLineDetailsDto> Lines
);読み取りモデルと書き込みモデルを分離することで、それぞれの要件に最適化された設計が可能です。例えば、読み取り側では非正規化されたビューを使用し、書き込み側では正規化されたドメインモデルを維持できます。
MediatR 14:パイプラインビヘイビアとハンドラー実装
MediatR 14.2は、CQRSのためのメッセージングインフラストラクチャを提供します。各CommandまたはQueryには正確に1つのハンドラーがあり、単一責任の原則を強制します。
namespace UseCases.Orders.Commands.CreateOrder;
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateOrderHandler(
IOrderRepository orderRepository,
IProductRepository productRepository,
IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
_unitOfWork = unitOfWork;
}
public async Task<Result<Guid>> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
// Load products to validate and get prices
var productIds = request.Lines.Select(l => l.ProductId).ToList();
var products = await _productRepository
.GetByIdsAsync(productIds, cancellationToken);
if (products.Count != productIds.Count)
return Result.NotFound("One or more products not found");
// Create order using domain logic
var order = new Order(request.CustomerEmail);
foreach (var line in request.Lines)
{
var product = products.First(p => p.Id == line.ProductId);
order.AddLine(product, line.Quantity);
}
await _orderRepository.AddAsync(order, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success(order.Id);
}
}ハンドラーは、リポジトリインターフェースを通じてデータにアクセスし、ドメインロジックを呼び出して操作を実行します。Unit of Workパターンにより、複数のリポジトリ操作を1つのトランザクションとして管理できます。
.NETの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
パイプラインビヘイビアによる横断的関心事の処理
MediatRのパイプラインビヘイビアは、ロギング、バリデーション、キャッシング、トランザクション管理をハンドラーを汚染することなく処理します。
namespace UseCases.Common.Behaviors;
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!_validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}ビヘイビアは実行順序で登録します。通常、バリデーションが最初に実行され、次にロギング、そしてトランザクション処理の順序となります。
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});このパイプラインアーキテクチャにより、横断的関心事をモジュール化し、必要に応じて追加・削除できます。面接では、この設計がAOPの代替としてどのように機能するかを説明できることが重要です。
インフラストラクチャ層:リポジトリの実装
インフラストラクチャ層は、コア層で定義されたインターフェースを実装します。Entity Framework Coreリポジトリは、ドメイン操作をデータベース呼び出しに変換します。
namespace Infrastructure.Data.Repositories;
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await _context.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task AddAsync(
Order order,
CancellationToken cancellationToken = default)
{
await _context.Orders.AddAsync(order, cancellationToken);
}
public async Task<IReadOnlyList<Order>> GetByCustomerEmailAsync(
string email,
CancellationToken cancellationToken = default)
{
return await _context.Orders
.Where(o => o.CustomerEmail == email)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
}
}リポジトリパターンにより、データアクセスロジックがカプセル化され、ドメイン層はデータの永続化方法を意識する必要がありません。テスト時にはモックリポジトリを注入することで、データベースなしでビジネスロジックをテストできます。
面接での質問:シニア開発者が知るべきこと
シニア.NETポジションの技術面接では、Clean Architectureに関する質問が頻繁に含まれます。以下は、面接官が評価するパターンです。
Q: なぜドメイン層は外部フレームワークに依存してはいけないのですか?
ドメイン層には、インフラストラクチャの変更に関係なく安定すべきビジネスルールが含まれます。ドメインがEntity Frameworkに依存している場合、Dapperや別のデータベースに切り替えるにはビジネスロジックの変更が必要になります。依存関係の規則により、インフラストラクチャはコアの動作に影響を与えずに交換できる詳細となります。
Q: CQRSが過剰な場合はどのような時ですか?
CQRSは、シンプルなCRUDアプリケーションには不要な複雑さを追加します。読み取りモデルと書き込みモデルがほぼ同一で、別々のスケーリングの必要がなく、チームが小規模な場合は、よりシンプルなアーキテクチャが適切です。CQRSは、読み取りモデルが書き込みモデルと大きく異なる場合、イベントソーシングが必要な場合、または読み取りと書き込みの負荷を独立してスケーリングする必要がある場合に効果を発揮します。
Q: 複数の集約にまたがるトランザクションをどのように処理しますか?
// Transaction behavior wraps the entire handler execution
public class TransactionBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IUnitOfWork _unitOfWork;
public TransactionBehavior(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
// Commands that modify multiple aggregates use explicit transactions
if (request is not ICommand)
return await next();
await using var transaction = await _unitOfWork
.BeginTransactionAsync(cancellationToken);
try
{
var response = await next();
await transaction.CommitAsync(cancellationToken);
return response;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
}複数の集約にまたがる操作では、可能な限りドメインイベントを使用した結果整合性を採用します。集約間の同期トランザクションは、設計上の問題を示唆している可能性があります:それらの集約は一つに統合すべきかもしれません。
読み取り側の最適化:分離されたクエリモデル
パフォーマンスが重要な場合、Queryはドメイン層を完全にバイパスします。Dapperや生のSQLを使用した直接データベースアクセスにより、完全なエンティティグラフの実体化によるオーバーヘッドを回避できます。
namespace Infrastructure.Queries;
public class GetOrderByIdQueryHandler
: IRequestHandler<GetOrderByIdQuery, Result<OrderDetailsDto>>
{
private readonly IDbConnection _connection;
public GetOrderByIdQueryHandler(IDbConnection connection)
{
_connection = connection;
}
public async Task<Result<OrderDetailsDto>> Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
const string sql = """
SELECT o.Id, o.CustomerEmail, o.Status, o.CreatedAt,
SUM(ol.UnitPrice * ol.Quantity) AS TotalAmount
FROM Orders o
LEFT JOIN OrderLines ol ON ol.OrderId = o.Id
WHERE o.Id = @OrderId
GROUP BY o.Id, o.CustomerEmail, o.Status, o.CreatedAt
""";
var order = await _connection.QueryFirstOrDefaultAsync<OrderDetailsDto>(
new CommandDefinition(sql, new { request.OrderId }, cancellationToken: cancellationToken));
return order is null
? Result.NotFound($"Order {request.OrderId} not found")
: Result.Success(order);
}
}このアプローチにより、読み取り操作は最大のパフォーマンスを達成し、書き込み操作はドメインの整合性を維持できます。面接では、この設計の理由とトレードオフを説明できることが求められます。
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
Clean Architecture面接対策:重要なポイント
- 依存関係の規則は、ソースコードの依存関係が内側にのみ向かうことを規定しています。ドメイン層は、データベース、フレームワーク、配信メカニズムについての知識を持ちません。
- CQRSは、Command(状態変更)とQuery(データ取得)を分離します。Commandはドメインモデルとバリデーションを経由します。Queryはパフォーマンスのためにドメインエンティティをバイパスできます。
- MediatR 14はリクエストをハンドラーにルーティングします。パイプラインビヘイビアは、バリデーション、ロギング、トランザクションなどの横断的関心事をコンポーザブルな方法で処理します。
- リポジトリインターフェースはコア層に配置されます。実装はインフラストラクチャ層に配置されます。この反転により、ビジネスロジックに触れることなくEF CoreをDapperに交換できます。
- 面接での回答にはトレードオフを含めるべきです。Clean Architectureは初期の複雑さを追加しますが、複数の開発者がいる大規模で長期間運用されるアプリケーションではその価値を発揮します。
- SharpSkillのASP.NET Core Clean Architectureモジュールでは、Specification、Resultオブジェクト、ドメインイベントなどの追加パターンをカバーしています。
.NET のバグを見つけられますか
実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

執筆
Anthony Fillion-MailletSharpSkill 創業者
10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。
2026年9月15日 更新
共有
関連記事

ASP.NET CoreにおけるDbContextのライフタイム管理:非同期操作でのパフォーマンスとスレッドセーフティ
ASP.NET CoreでのDbContextライフタイム管理を解説します。スコープ付きライフタイムとトランジェントの使い分け、非同期操作でのスレッドセーフティ、DbContextプーリングによるパフォーマンス最適化について学びます。

C#クリーンアーキテクチャ完全ガイド:面接対策と実装パターン 2026年版
C#におけるクリーンアーキテクチャの設計原則、SOLID原則の実践的な適用方法、そして技術面接で頻出する質問と回答を徹底解説します。

2026年版 C# LINQ上級ガイド:演算子、パフォーマンス最適化、面接対策
LINQ の上級演算子、遅延実行、パフォーマンス最適化を詳しく解説します。GroupBy、SelectMany、クエリ最適化、および.NET開発者向け面接質問を網羅しています。