Design patterns in C#
Proven solutions to common design problems: Strategy, Factory, Decorator, Observer, Builder, Adapter and Singleton, written in modern C# with real examples.
| Pattern | Kind | Problem it solves |
|---|---|---|
Strategy
public interface ITensionCheck{ string Name { get; } double DesignStrengthN(double areaMm2, double fyMpa);} public class Is800Check : ITensionCheck{ public string Name => "IS 800"; public double DesignStrengthN(double a, double fy) => a * fy / 1.10;} public class Eurocode3Check : ITensionCheck{ public string Name => "EN 1993"; public double DesignStrengthN(double a, double fy) => a * fy / 1.00;} public class MemberReport(ITensionCheck check){ public string Run(double a, double fy) => $"{check.Name}: {check.DesignStrengthN(a, fy) / 1000:F1} kN";} Console.WriteLine(new MemberReport(new Is800Check()).Run(5000, 250));Console.WriteLine(new MemberReport(new Eurocode3Check()).Run(5000, 250));IS 800: 1136.4 kN EN 1993: 1250.0 kN
Factory
public abstract record Section(string Name);public record ISection(string Name) : Section(Name);public record HollowRectSection(string Name) : Section(Name); public static class SectionFactory{ public static Section Create(string name) => name.ToUpperInvariant() switch { var n when n.StartsWith("IPE") || n.StartsWith("HEA") => new ISection(n), var n when n.StartsWith("RHS") || n.StartsWith("SHS") => new HollowRectSection(n), _ => throw new ArgumentException($"Unknown section \"{name}\"") };} Section s = SectionFactory.Create("ipe300");Console.WriteLine(s);ISection { Name = IPE300 }Section. All the “which class?” knowledge lives in one place.
Builder
var report = new ReportBuilder() .WithTitle("Level 2 steel") .AddSection("Beams", beams) .AddSection("Columns", columns) .WithLogo("logo.png") .Build(); public class ReportBuilder{ private readonly Report _r = new(); public ReportBuilder WithTitle(string t) { _r.Title = t; return this; } // return this → chaining public ReportBuilder AddSection(string name, IEnumerable<Beam> rows) { _r.Sections.Add(new(name, rows.ToList())); return this; } public ReportBuilder WithLogo(string path) { _r.LogoPath = path; return this; } public Report Build() => string.IsNullOrEmpty(_r.Title) ? throw new InvalidOperationException("Title required") : _r;}StringBuilder, WebApplication.CreateBuilder(), and UriBuilder all follow this idea.
Decorator
public interface IBeamRepository { Beam? Get(string mark); } public class CachingRepository(IBeamRepository inner) : IBeamRepository{ private readonly Dictionary<string, Beam?> _cache = new(); public Beam? Get(string mark) { if (_cache.TryGetValue(mark, out var hit)) return hit; // fast path var beam = inner.Get(mark); // delegate to the wrapped object _cache[mark] = beam; return beam; }} IBeamRepository repo = new LoggingRepository(new CachingRepository(new SqlBeamRepository(conn)));Stream in .NET works this way: new GZipStream(new BufferedStream(fileStream), …).
Adapter
// A third-party library we can't changepublic class VendorIfcReader { public VendorElement[] ReadAll(string file) => /* … */ []; } // Our app wants IModelSourcepublic class IfcModelSource(VendorIfcReader reader, string file) : IModelSource{ public IReadOnlyList<Beam> LoadBeams() => reader.ReadAll(file) .Where(e => e.Category == "IfcBeam") .Select(e => new Beam(e.Tag, e.Length / 1000.0, e.MassPerLength)) .ToList();}Observer and Singleton
public sealed class UnitSettings{ public static UnitSettings Instance { get; } = new(); // created once, thread-safe private UnitSettings() { } // nobody else can call new public string Length { get; set; } = "mm";}Using patterns well
Start with plain code. Introduce a pattern when the problem it solves actually appears. Name classes after the pattern when it helps: SectionFactory,CachingRepository,ReportBuilder.Patterns combine: a factory can return strategies; decorators wrap repositories created by DI.
TimingRepository decorator for IBeamRepository that measures each Get call with Stopwatch and prints Get(B12) took N ms, then returns the inner result.
Show the solution
public class TimingRepository(IBeamRepository inner) : IBeamRepository{ public Beam? Get(string mark) { var sw = Stopwatch.StartNew(); try { return inner.Get(mark); } finally { Console.WriteLine($"Get({mark}) took {sw.ElapsedMilliseconds} ms"); } }} IBeamRepository repo = new TimingRepository(new SqlBeamRepository(conn));repo.Get("B12");Get(B12) took 14 ms
Remember
Strategy swaps algorithms; Factory chooses classes; Builder assembles complex objects. Decorator wraps an object with extra behaviour behind the same interface; Adapter converts interfaces. C# events are the Observer pattern; prefer DI singletons to static Singleton classes. Use a pattern when its problem appears, not before.