skills/core/design-patterns/SKILL.md
Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator.
npx skillsauth add faysilalshareef/dotnet-ai-kit design-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 Responsibilitydata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.