원클릭으로
design-patterns
Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when adding caching to .NET APIs or optimizing response times with distributed cache, output cache, or ETags.
Use when configuring API response formats, custom formatters, or Accept header handling.
Use when building controller-based REST APIs with action results, model binding, or MediatR integration.
Use when creating RESTful API controllers with MediatR dispatch and ProblemDetails error responses.
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when building minimal API endpoints with route groups, filters, or TypedResults.
| name | design-patterns |
| description | Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator. |
| metadata | {"category":"core","agent":"dotnet-architect","alwaysApply":true} |
Use DI as the modern replacement. Keep Factory Method only for polymorphic creation where the type is chosen at runtime.
// Modern: DI + factory delegate
services.AddScoped<Func<string, INotificationSender>>(sp => channel => channel switch
{
"email" => sp.GetRequiredService<EmailSender>(),
"sms" => sp.GetRequiredService<SmsSender>(),
_ => throw new ArgumentOutOfRangeException(nameof(channel))
});
Modern C# init properties and record positional syntax eliminate most Builder needs. Keep Builder only for complex multi-step construction with validation.
// Modern replacement: record with positional syntax
public sealed record OrderRequest(string CustomerId, List<LineItem> Items, string? CouponCode = null);
// Keep Builder when construction is complex and multi-step
public sealed class ReportBuilder
{
private readonly List<ReportSection> _sections = [];
public ReportBuilder AddSection(ReportSection section) { _sections.Add(section); return this; }
public Report Build() => _sections.Count == 0
? throw new InvalidOperationException("Report must have at least one section")
: new Report([.. _sections]);
}
init properties covers all fields cleanlyNever use the classic double-lock singleton. The DI container owns object lifetime.
// Modern: DI container manages the lifetime
services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
services.AddSingleton<HybridCache>();
AddSingleton in DIstatic Instance pattern; let the DI container handle itWraps an incompatible interface so it conforms to the one your code expects.
public sealed class LegacyPaymentAdapter(LegacyPaymentApi legacy) : IPaymentGateway
{
public async Task<PaymentResult> ChargeAsync(decimal amount, CancellationToken ct)
{
var code = await legacy.ProcessPaymentXml($"<amount>{amount}</amount>");
return new PaymentResult(code == 0, code.ToString());
}
}
Adds behavior to an object without modifying it. MediatR pipeline behaviors are the modern poster child.
// MediatR pipeline behavior as Decorator
public sealed class LoggingBehavior<TRequest, TResponse>(
ILogger<LoggingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
return await next(ct);
}
}
Provides a simplified API over a complex subsystem. Common in application services.
public sealed class OrderFacade(
IOrderRepository orders, IPaymentGateway payments, IInventoryService inventory)
{
public async Task<OrderResult> PlaceOrderAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
await inventory.ReserveAsync(cmd.Items, ct);
var payment = await payments.ChargeAsync(cmd.Total, ct);
return await orders.CreateAsync(cmd, payment.TransactionId, ct);
}
}
Controls access to an object — commonly for lazy loading, caching, or authorization.
public sealed class CachedProductService(
IProductService inner, HybridCache cache) : IProductService
{
public async Task<Product?> GetByIdAsync(Guid id, CancellationToken ct)
=> await cache.GetOrCreateAsync($"product:{id}",
async token => await inner.GetByIdAsync(id, token), cancellationToken: ct);
}
Delegates and Func<T> are the lightweight alternative. Use DI for injectable strategies.
// Lightweight: Func<T> delegate
public sealed class PricingEngine(Func<Order, decimal> discountStrategy)
{
public decimal Calculate(Order order) => order.SubTotal - discountStrategy(order);
}
// DI-based: keyed services (.NET 8+)
services.AddKeyedScoped<IShippingCalculator, StandardShipping>("standard");
services.AddKeyedScoped<IShippingCalculator, ExpressShipping>("express");
Func<T> or lambda covers the variation without needing a full interfaceC# events and MediatR notifications replace the classic Observer pattern.
// Modern: MediatR notification (pub-sub)
public sealed record OrderPlacedEvent(Guid OrderId, decimal Total) : INotification;
public sealed class SendConfirmationEmail(IEmailSender email)
: INotificationHandler<OrderPlacedEvent>
{
public async Task Handle(OrderPlacedEvent e, CancellationToken ct)
=> await email.SendOrderConfirmationAsync(e.OrderId, ct);
}
MediatR IRequest is the standard modern implementation.
public sealed record CreateOrderCommand(string CustomerId, List<LineItem> Items)
: IRequest<Guid>;
public sealed class CreateOrderHandler(IOrderRepository repo, IUnitOfWork uow)
: IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId, cmd.Items);
repo.Add(order);
await uow.SaveChangesAsync(ct);
return order.Id;
}
}
MediatR itself implements this pattern — decouples senders from receivers.
// Controller sends; handler receives — no direct dependency
app.MapPost("/orders", async (CreateOrderCommand cmd, ISender sender) =>
Results.Created($"/orders/{await sender.Send(cmd)}", null));
ASP.NET Core middleware pipeline is the canonical modern example.
// Custom middleware — chain of responsibility
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew();
await next(context);
context.Response.Headers.Append("X-Elapsed-Ms", sw.ElapsedMilliseconds.ToString());
});
Use enum-based state machines for simple cases. Reserve the full State pattern for complex transitions.
// Simple: enum + switch
public sealed class Order
{
public OrderState State { get; private set; } = OrderState.Draft;
public void Submit() => State = State switch
{
OrderState.Draft => OrderState.Submitted,
_ => throw new InvalidOperationException($"Cannot submit from {State}")
};
public void Approve() => State = State switch
{
OrderState.Submitted => OrderState.Approved,
_ => throw new InvalidOperationException($"Cannot approve from {State}")
};
}
| GoF Pattern | Modern C# Replacement | Example |
|---|---|---|
| Strategy | Func<T>, delegates | Func<Order, decimal> passed to constructor |
| Observer | event, INotification | MediatR notifications or C# events |
| Template Method | Func<T> parameters | Pass steps as delegates instead of subclassing |
| Prototype | record with with | order with { Status = "Shipped" } |
| Iterator | IEnumerable<T>, LINQ | yield return, foreach, LINQ operators |
| Singleton | DI AddSingleton | Container-managed lifetime |
| Command | record + MediatR | IRequest<T> with pipeline behaviors |
| Visitor | Pattern matching | Switch expressions with type patterns |
| Null Object | Nullable reference types | T? with null-conditional operators |
| Bridge | Generics + interfaces | IRepository<T> with generic constraints |
| Problem | Recommended Pattern | Why |
|---|---|---|
| Runtime type selection | Factory Method via DI delegate | Clean, testable, no new keywords |
| Complex object construction | Builder | Enforces step order and validation |
| Incompatible third-party API | Adapter | Isolates integration behind your interface |
| Cross-cutting concerns (logging, caching) | Decorator / MediatR behaviors | Composable without modifying core logic |
| Multiple handlers for one event | Observer via MediatR notifications | Decoupled pub-sub |
| Varying algorithm at runtime | Strategy via Func<T> or keyed DI | Lightweight, no interface explosion |
| Sequential request processing | Chain of Responsibility (middleware) | ASP.NET Core already provides the pipeline |
| Entity with complex lifecycle | State (enum-based first) | Prevents invalid transitions |
| Simplified entry point to subsystem | Facade | Reduces coupling for callers |
| Problem | Why It Hurts | Correct Approach |
|---|---|---|
| Applying every GoF pattern "just in case" | Massive over-engineering, hard to navigate | Add patterns only when a clear problem recurs |
Classic static Instance singleton | Untestable, hides dependencies, threading issues | Use AddSingleton in DI container |
| Interface for every class | Empty abstractions, ceremony without benefit | Add interfaces when you have 2+ implementations or need testability |
| God Factory that creates everything | Single point of coupling, hard to extend | Separate factories per concern or use DI delegates |
| Repository pattern wrapping EF's DbSet | Double abstraction over an already-abstract ORM | Use DbContext directly or a thin specification pattern |
| Mediator for every call including simple queries | Indirection without benefit, harder debugging | Use Mediator for commands with pipeline needs; inject services directly for simple reads |
| Strategy interface with one implementation | YAGNI overhead | Use a concrete class; extract interface later if needed |
| Deep decorator chains | Hard to debug, unclear execution order | Limit to 2-3 decorators; prefer MediatR pipeline behaviors for ordering clarity |
IPipelineBehavior — indicates MediatR decorator/pipeline usageINotification and INotificationHandler — Observer via MediatRIRequest<T> and IRequestHandler — Command via MediatRProgram.cs for AddSingleton, AddScoped, AddTransient patternsAddKeyedSingleton / AddKeyedScoped — keyed Strategy via DIapp.Use( middleware registrations — Chain of Responsibility