C# Design Patterns
Guidance for selecting and implementing design patterns in modern C#/.NET. Content is structured after refactoring.guru (intent, applicability, pros and cons) and dofactory (.NET-optimized variants that use framework features instead of hand-rolled GoF structure).
Core principles
- Pattern selection comes from the problem, not the other way around. Never recommend a pattern because it is 'good practice'. Identify the design pain first, then check the selection table below. If no pain exists, no pattern is needed.
- Prefer the .NET-native form. Many GoF patterns are already built into the framework or the language. Hand-rolling the classic UML structure when the platform provides it is an anti-pattern. The reference files mark these as '.NET-native form'.
- Modern C# changes the implementation. Use DI containers, delegates, lambdas, generics, records, pattern matching,
IObservable<T>,IEnumerable<T>/yield, and source generators where they replace boilerplate classes. Show the classic structure only when the user asks for it explicitly (e.g. for learning or interviews). - Always state the cost. Every pattern adds indirection. When recommending one, name the tradeoff in one or two sentences (more types, harder navigation, runtime overhead, etc.).
- Style comes from
csharp, not from here. All samples follow its conventions (primary constructors for dependency capture, properties before constructors,sealedby default). When generating real project code, loadcsharpalongside this skill; on any style conflict,csharpwins.
Workflow
When the user asks for help with patterns:
- Identify the actual design problem (ask one clarifying question only if the problem is genuinely ambiguous).
- Use the selection table below to shortlist 1-2 candidate patterns.
- Read the matching reference file for the chosen pattern before writing any code - it carries the modern .NET form, the tradeoffs, and a worked example:
references/creational.md- Factory Method, Abstract Factory, Builder, Prototype, Singletonreferences/structural.md- Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxyreferences/behavioral.md- Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor
- Implement using the modern .NET form from the reference. Include the classic form only on request.
- State the tradeoff and, where relevant, the simpler alternative that was rejected.
Pattern selection table
| Design problem | Pattern | First check |
|---|---|---|
Object creation logic duplicated or new scattered with conditionals |
Factory Method | Is DI registration enough? |
| Families of related objects must stay consistent (e.g. per-provider, per-theme) | Abstract Factory | Can keyed DI services solve it? |
| Constructor has too many parameters / complex optional configuration | Builder | Do init properties + required solve it? |
| Object copying is expensive or type unknown at compile time | Prototype | Does a record with expression solve it? |
| Exactly one instance needed app-wide | Singleton | Use DI singleton lifetime, not a static class |
| Incompatible interface between your code and a library | Adapter | - |
| Abstraction and implementation both vary independently | Bridge | - |
| Tree structure where leaves and containers are treated uniformly | Composite | - |
| Add behavior to objects without subclassing | Decorator | Pipelines/middleware may already do this |
| Complex subsystem needs a simple entry point | Facade | A plain service class is often enough |
| Thousands of similar objects, memory pressure | Flyweight | Measure first; strings are already interned |
| Control access to an object (lazy, remote, caching, auth) | Proxy | Lazy<T>, DispatchProxy, or interceptors |
| Request handled by one of several handlers in sequence | Chain of Responsibility | ASP.NET Core middleware is this pattern |
| Operations as objects (undo, queueing, scheduling) | Command | MediatR-style handlers, ICommand in WPF |
| Custom traversal over a collection | Iterator | IEnumerable<T> + yield return is this pattern |
| Many-to-many object communication creates spaghetti | Mediator | - |
| Save/restore object state (undo, snapshots) | Memento | - |
| One-to-many change notification | Observer | event, IObservable<T>, INotifyPropertyChanged |
| Behavior changes drastically with internal state | State | Pattern matching on a state enum may be enough |
| Swap an algorithm at runtime | Strategy | A Func<> parameter may be enough |
| Fixed algorithm skeleton with customizable steps | Template Method | Prefer Strategy/composition if variance grows |
| New operations over a stable class hierarchy | Visitor | Pattern matching (switch on type) is the modern rival |
Anti-pattern checks
Run these before finalizing any recommendation:
- Singleton via static class or static instance field: in any app with a DI container, register a normal class with singleton lifetime instead. Static singletons block testing and hide dependencies.
- Factory for a single concrete type: if there is only one implementation and no creation logic, inject the type directly.
- Strategy with one strategy: same problem. Wait until a second algorithm actually exists.
- Hand-rolled Observer: C# has
event. WPF/MVVM hasINotifyPropertyChanged. Rx hasIObservable<T>. Do not write Subject/Observer classes manually. - Service Locator disguised as Factory: a factory that resolves arbitrary types from a container by name/type is a Service Locator. Flag it.
- Pattern stacking: if a proposed design uses 3+ patterns for a feature of moderate size, simplify before presenting.
Output format for pattern recommendations
When recommending a pattern, structure the answer as:
- Problem restated in one line (the design pain, not the user's words)
- Pattern + why it fits (2-3 sentences)
- Modern C# implementation (compilable code, .NET 8+, nullable enabled)
- Tradeoff (1-2 sentences)
- Simpler alternative considered (when one exists)
For 'explain pattern X' requests, follow the refactoring.guru structure instead: Intent, Problem, Solution, C# example (modern form first, classic on request), Applicability, Pros and cons, Relations with other patterns.
Related skills
csharp(itsreferences/dependency-injection.md) - registration mechanics (keyed services,Add*extensions, factories) when a pattern collapses into DI, as Factory/Strategy/Singleton usually do.csharp(itsreferences/concurrency.md) - when Observer or Mediator turns into producer-consumer flows (Channel<T>, backpressure); the worker host around such a flow is the hosted-worker skill's (BackgroundService lifecycle), where installed.- The WPF conventions skill, where installed -
ICommand/[RelayCommand]specifics when Command or Observer lands in WPF/MVVM.