$npx -y skills add Aaronontheweb/dotnet-skills --skill csharp-type-design-performanceDesign .NET types for performance. Seal classes, use readonly structs, prefer static pure functions, avoid premature enumeration, and choose the right collection types.
| 1 | # Type Design for Performance |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | Use this skill when: |
| 6 | - Designing new types and APIs |
| 7 | - Reviewing code for performance issues |
| 8 | - Choosing between class, struct, and record |
| 9 | - Working with collections and enumerables |
| 10 | |
| 11 | --- |
| 12 | |
| 13 | ## Core Principles |
| 14 | |
| 15 | 1. **Seal your types** - Unless explicitly designed for inheritance |
| 16 | 2. **Prefer readonly structs** - For small, immutable value types |
| 17 | 3. **Prefer static pure functions** - Better performance and testability |
| 18 | 4. **Defer enumeration** - Don't materialize until you need to |
| 19 | 5. **Return immutable collections** - From API boundaries |
| 20 | |
| 21 | --- |
| 22 | |
| 23 | ## Seal Classes by Default |
| 24 | |
| 25 | Sealing classes enables JIT devirtualization and communicates API intent. |
| 26 | |
| 27 | ```csharp |
| 28 | // DO: Seal classes not designed for inheritance |
| 29 | public sealed class OrderProcessor |
| 30 | { |
| 31 | public void Process(Order order) { } |
| 32 | } |
| 33 | |
| 34 | // DO: Seal records (they're classes) |
| 35 | public sealed record OrderCreated(OrderId Id, CustomerId CustomerId); |
| 36 | |
| 37 | // DON'T: Leave unsealed without reason |
| 38 | public class OrderProcessor // Can be subclassed - intentional? |
| 39 | { |
| 40 | public virtual void Process(Order order) { } // Virtual = slower |
| 41 | } |
| 42 | ``` |
| 43 | |
| 44 | **Benefits:** |
| 45 | - JIT can devirtualize method calls |
| 46 | - Communicates "this is not an extension point" |
| 47 | - Prevents accidental breaking changes |
| 48 | |
| 49 | --- |
| 50 | |
| 51 | ## Readonly Structs for Value Types |
| 52 | |
| 53 | Structs should be `readonly` when immutable. This prevents defensive copies. |
| 54 | |
| 55 | ```csharp |
| 56 | // DO: Readonly struct for immutable value types |
| 57 | public readonly record struct OrderId(Guid Value) |
| 58 | { |
| 59 | public static OrderId New() => new(Guid.NewGuid()); |
| 60 | public override string ToString() => Value.ToString(); |
| 61 | } |
| 62 | |
| 63 | // DO: Readonly struct for small, short-lived data |
| 64 | public readonly struct Money |
| 65 | { |
| 66 | public decimal Amount { get; } |
| 67 | public string Currency { get; } |
| 68 | |
| 69 | public Money(decimal amount, string currency) |
| 70 | { |
| 71 | Amount = amount; |
| 72 | Currency = currency; |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // DON'T: Mutable struct (causes defensive copies) |
| 77 | public struct Point // Not readonly! |
| 78 | { |
| 79 | public int X { get; set; } // Mutable! |
| 80 | public int Y { get; set; } |
| 81 | } |
| 82 | ``` |
| 83 | |
| 84 | ### When to Use Structs |
| 85 | |
| 86 | | Use Struct When | Use Class When | |
| 87 | |-----------------|----------------| |
| 88 | | Small (≤16 bytes typically) | Larger objects | |
| 89 | | Short-lived | Long-lived | |
| 90 | | Frequently allocated | Shared references needed | |
| 91 | | Value semantics required | Identity semantics required | |
| 92 | | Immutable | Mutable state | |
| 93 | |
| 94 | --- |
| 95 | |
| 96 | ## Prefer Static Pure Functions |
| 97 | |
| 98 | Static methods with no side effects are faster and more testable. |
| 99 | |
| 100 | ```csharp |
| 101 | // DO: Static pure function |
| 102 | public static class OrderCalculator |
| 103 | { |
| 104 | public static Money CalculateTotal(IReadOnlyList<OrderItem> items) |
| 105 | { |
| 106 | var total = items.Sum(i => i.Price * i.Quantity); |
| 107 | return new Money(total, "USD"); |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // Usage - predictable, testable |
| 112 | var total = OrderCalculator.CalculateTotal(items); |
| 113 | ``` |
| 114 | |
| 115 | **Benefits:** |
| 116 | - No vtable lookup (faster) |
| 117 | - No hidden state |
| 118 | - Easier to test (pure input → output) |
| 119 | - Thread-safe by design |
| 120 | - Forces explicit dependencies |
| 121 | |
| 122 | ```csharp |
| 123 | // DON'T: Instance method hiding dependencies |
| 124 | public class OrderCalculator |
| 125 | { |
| 126 | private readonly ITaxService _taxService; // Hidden dependency |
| 127 | private readonly IDiscountService _discountService; // Hidden dependency |
| 128 | |
| 129 | public Money CalculateTotal(IReadOnlyList<OrderItem> items) |
| 130 | { |
| 131 | // What does this actually depend on? |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | // BETTER: Explicit dependencies via parameters |
| 136 | public static class OrderCalculator |
| 137 | { |
| 138 | public static Money CalculateTotal( |
| 139 | IReadOnlyList<OrderItem> items, |
| 140 | decimal taxRate, |
| 141 | decimal discountPercent) |
| 142 | { |
| 143 | // All inputs visible |
| 144 | } |
| 145 | } |
| 146 | ``` |
| 147 | |
| 148 | **Don't go overboard** - Use instance methods when you genuinely need state or polymorphism. |
| 149 | |
| 150 | --- |
| 151 | |
| 152 | ## Defer Enumeration |
| 153 | |
| 154 | Don't materialize enumerables until necessary. Avoid excessive LINQ chains. |
| 155 | |
| 156 | ```csharp |
| 157 | // BAD: Premature materialization |
| 158 | public IReadOnlyList<Order> GetActiveOrders() |
| 159 | { |
| 160 | return _orders |
| 161 | .Where(o => o.IsActive) |
| 162 | .ToList() // Materialized! |
| 163 | .OrderBy(o => o.CreatedAt) // Another iteration |
| 164 | .ToList(); // Materialized again! |
| 165 | } |
| 166 | |
| 167 | // GOOD: Defer until the end |
| 168 | public IReadOnlyList<Order> GetActiveOrders() |
| 169 | { |
| 170 | return _orders |
| 171 | .Where(o => o.IsActive) |
| 172 | .OrderBy(o => o.CreatedAt) |
| 173 | .ToList(); // Single materialization |
| 174 | } |
| 175 | |
| 176 | // GOOD: Return IEnumerable if caller might not need all items |
| 177 | public IEnumerable<Order> GetActiveOrders() |
| 178 | { |
| 179 | return _orders |
| 180 | .Where(o => o.IsActive) |
| 181 | .OrderBy(o => o.CreatedAt); |
| 182 | // Caller decides when to materialize |
| 183 | } |
| 184 | ``` |
| 185 | |
| 186 | ### Async Enumeration |
| 187 | |
| 188 | Be careful with async and IEnumerable: |
| 189 | |
| 190 | ```csharp |
| 191 | // BAD: Async in LINQ - hidden allocations |
| 192 | var results = orders |
| 193 | .Select(async o => await ProcessOrderAsync(o)) // Task p |