Clean Architecture .NET 2026: CQRS, MediatR และคำถามสัมภาษณ์นักพัฒนา
คู่มือฉบับสมบูรณ์เกี่ยวกับ Clean Architecture ใน .NET พร้อม CQRS และ MediatR 14 เรียนรู้รูปแบบสถาปัตยกรรมแบบเลเยอร์ pipeline behaviors และเตรียมตัวสัมภาษณ์ senior developer

Clean Architecture ใน .NET แยก business logic ออกจาก infrastructure concerns ทำให้แอปพลิเคชันง่ายต่อการทดสอบ บำรุงรักษา และขยายขนาด เมื่อรวมกับ CQRS (Command Query Responsibility Segregation) และ MediatR รูปแบบสถาปัตยกรรมนี้ให้แนวทางที่มีโครงสร้างสำหรับการสร้างแอปพลิเคชัน enterprise ที่ผู้สัมภาษณ์มักจะประเมิน
คำถามเกี่ยวกับ Clean Architecture ปรากฏใน 70% ของการสัมภาษณ์ senior .NET ผู้สัมภาษณ์คาดหวังให้ผู้สมัครอธิบาย dependency rule: dependencies ชี้เข้าด้านใน โดย domain layer ไม่มี dependencies กับ framework ภายนอก
เลเยอร์ของ Clean Architecture และ Dependency Rule
Template Clean Architecture จาก Ardalis สำหรับ ASP.NET Core 10 จัดระเบียบโค้ดเป็นสี่เลเยอร์แบบ concentric แต่ละเลเยอร์มีความรับผิดชอบเฉพาะและกฎ dependency ที่เข้มงวด
// Core/Domain Layer - ไม่มี 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 ถูก encapsulate ใน 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;
}
}Domain layer ประกอบด้วย entities, value objects และ domain services เลเยอร์นี้ไม่รู้อะไรเกี่ยวกับ database, HTTP หรือ framework ภายนอก
รูปแบบ CQRS: แยก Read จาก Write
CQRS แบ่งการดำเนินการเป็น Commands (การดำเนินการ write ที่เปลี่ยน state) และ Queries (การดำเนินการ read ที่ส่งคืนข้อมูล) การแยกนี้ช่วยให้สามารถ scale และ optimize แต่ละเส้นทางได้อย่างอิสระ
// UseCases Layer - Commands และ 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);Commands ส่งคืนข้อมูลน้อยที่สุด โดยปกติเป็นเพียงตัวบ่งชี้ความสำเร็จหรือ ID ที่สร้างใหม่ Queries ส่งคืน DTO ที่ optimize สำหรับ consumer
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: Pipeline Behaviors และการ Implement Handler
MediatR 14.2 ให้ messaging infrastructure สำหรับ CQRS แต่ละ command หรือ query มี handler เพียงตัวเดียว บังคับใช้ single responsibility
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)
{
// โหลด products เพื่อ validate และรับราคา
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");
// สร้าง order โดยใช้ 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);
}
}พร้อมที่จะพิชิตการสัมภาษณ์ .NET แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Pipeline Behaviors สำหรับ Cross-Cutting Concerns
Pipeline behaviors ใน MediatR จัดการ logging, validation, caching และ transaction management โดยไม่ทำให้ handlers สกปรก
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();
}
}ลงทะเบียน behaviors ตามลำดับการทำงาน Validation มักจะทำงานก่อน ตามด้วย logging แล้วก็ transaction handling
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});Infrastructure Layer: การ Implement Repository
Infrastructure layer implement interfaces ที่กำหนดใน core layer Repository ของ Entity Framework Core แปลการดำเนินการ domain เป็นการเรียก database
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);
}
}คำถามสัมภาษณ์: สิ่งที่ Senior Developer ควรรู้
การสัมภาษณ์เทคนิคสำหรับตำแหน่ง senior .NET มักรวมคำถามเกี่ยวกับ Clean Architecture ต่อไปนี้คือรูปแบบที่ผู้สัมภาษณ์ประเมิน:
Q: ทำไม domain layer ไม่มี dependencies กับ framework ภายนอก?
Domain layer มีกฎธุรกิจที่ควรคงที่โดยไม่คำนึงถึงการเปลี่ยนแปลง infrastructure หาก domain พึ่งพา Entity Framework การเปลี่ยนไปใช้ Dapper หรือ database อื่นต้องเปลี่ยน business logic Dependency rule รับประกันว่า infrastructure เป็นรายละเอียดที่สามารถสลับได้โดยไม่กระทบ behavior หลัก
Q: เมื่อไหร่ที่ CQRS มากเกินไป?
CQRS เพิ่มความซับซ้อนที่แอปพลิเคชัน CRUD ง่ายๆ ไม่ต้องการ หาก model read และ write เกือบเหมือนกัน หากไม่ต้องการ scaling แยก และหากทีมเล็ก สถาปัตยกรรมที่ง่ายกว่าเหมาะสมกว่า CQRS โดดเด่นเมื่อ model read แตกต่างจาก model write อย่างมาก เมื่อต้องการ event sourcing หรือเมื่อ load read และ write ต้องการ scaling อิสระ
Q: จะจัดการ transactions ข้าม aggregates หลายตัวได้อย่างไร?
// Transaction behavior ครอบคลุมการทำงานทั้งหมดของ handler
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 ที่แก้ไข aggregates หลายตัวใช้ 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;
}
}
}สำหรับการดำเนินการที่ครอบคลุม aggregates หลายตัว ใช้ eventual consistency กับ domain events เมื่อเป็นไปได้ Transactions แบบ synchronous ข้าม aggregates บ่งชี้ปัญหาการออกแบบที่อาจเกิดขึ้น: aggregates เหล่านั้นอาจควรอยู่ด้วยกัน
การ Optimize Read-Side ด้วย Query Model แยก
Queries ข้าม domain layer ทั้งหมดเมื่อ performance สำคัญ การเข้าถึง database โดยตรงด้วย Dapper หรือ raw SQL หลีกเลี่ยง overhead ของการ materialize entity graph ทั้งหมด
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: ประเด็นสำคัญ
- Dependency rule ระบุว่า source code dependencies สามารถชี้เข้าด้านในเท่านั้น Domain layer ไม่รู้เกี่ยวกับ databases, frameworks หรือกลไก delivery
- CQRS แยก commands (การเปลี่ยน state) จาก queries (การดึงข้อมูล) Commands ผ่าน domain model และ validation Queries สามารถข้าม domain entities เพื่อ performance
- MediatR 14 route requests ไปยัง handlers Pipeline behaviors จัดการ cross-cutting concerns เช่น validation, logging และ transactions อย่าง composable
- Repository interfaces อยู่ใน core layer Implementations อยู่ใน infrastructure การกลับด้านนี้ช่วยให้สลับ EF Core เป็น Dapper ได้โดยไม่ต้องแตะ business logic
- คำตอบสัมภาษณ์ควรรวม trade-offs Clean Architecture เพิ่มความซับซ้อนเริ่มต้นแต่ให้ผลตอบแทนในแอปพลิเคชันขนาดใหญ่ที่มีอายุยาวนานกับ developers หลายคน
- โมดูล ASP.NET Core Clean Architecture บน SharpSkill ครอบคลุมรูปแบบเพิ่มเติมรวมถึง Specification, Result objects และ Domain Events
คุณหาบั๊กใน .NET เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 15 กันยายน 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Clean Code Architecture C#: คู่มือฉบับสมบูรณ์และคำถามสัมภาษณ์ 2026
เรียนรู้ Clean Code Architecture ใน C# พร้อมหลักการ SOLID, Repository Pattern, Dependency Injection และคำถามสัมภาษณ์ยอดนิยมสำหรับนักพัฒนา .NET

.NET MAUI ในปี 2026: คู่มือพัฒนา Cross-Platform และคำถามสัมภาษณ์งาน
บทความสอน .NET MAUI ครอบคลุมการพัฒนา cross-platform ด้วย .NET 10, handler, MVVM, HybridWebView และคำถามสัมภาษณ์งานที่สำคัญในปี 2026

คำถามสัมภาษณ์ C# และ .NET: คู่มือฉบับสมบูรณ์ 2026
คำถามสัมภาษณ์ C# และ .NET ที่พบบ่อยที่สุด 17 ข้อ LINQ, async/await, dependency injection, Entity Framework และ best practice พร้อมคำตอบละเอียดและตัวอย่างโค้ด