| name | type-design-performance |
| description | Design .NET types for performance. Seal classes, use readonly structs, prefer static pure functions, avoid premature enumeration, and choose the right collection types. |
| invocable | false |
Type Design for Performance
When to Use This Skill
Use this skill when:
- Designing new types and APIs
- Reviewing code for performance issues
- Choosing between class, struct, and record
- Working with collections and enumerables
Core Principles
- Seal your types - Unless explicitly designed for inheritance
- Prefer readonly structs - For small, immutable value types
- Prefer static pure functions - Better performance and testability
- Defer enumeration - Don't materialize until you need to
- Return immutable collections - From API boundaries
Seal Classes by Default
Sealing classes enables JIT devirtualization and communicates API intent.
public sealed class OrderProcessor
{
public void Process(Order order) { }
}
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
public class OrderProcessor
{
public virtual void Process(Order order) { }
}
Benefits:
- JIT can devirtualize method calls
- Communicates "this is not an extension point"
- Prevents accidental breaking changes
Readonly Structs for Value Types
Structs should be readonly when immutable. This prevents defensive copies.
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
When to Use Structs
| Use Struct When | Use Class When |
|---|
| Small (≤16 bytes typically) | Larger objects |
| Short-lived | Long-lived |
| Frequently allocated | Shared references needed |
| Value semantics required | Identity semantics required |
| Immutable | Mutable state |
Prefer Static Pure Functions
Static methods with no side effects are faster and more testable.
public static class OrderCalculator
{
public static Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
var total = items.Sum(i => i.Price * i.Quantity);
return new Money(total, "USD");
}
}
var total = OrderCalculator.CalculateTotal(items);
Benefits:
- No vtable lookup (faster)
- No hidden state
- Easier to test (pure input → output)
- Thread-safe by design
- Forces explicit dependencies
public class OrderCalculator
{
private readonly ITaxService _taxService;
private readonly IDiscountService _discountService;
public Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
}
}
public static class OrderCalculator
{
public static Money CalculateTotal(
IReadOnlyList<OrderItem> items,
decimal taxRate,
decimal discountPercent)
{
}
}
Don't go overboard - Use instance methods when you genuinely need state or polymorphism.
Defer Enumeration
Don't materialize enumerables until necessary. Avoid excessive LINQ chains.
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.ToList()
.OrderBy(o => o.CreatedAt)
.ToList();
}
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt)
.ToList();
}
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
}
Async Enumeration
Be careful with async and IEnumerable:
var results = orders
.Select(async o => await ProcessOrderAsync(o))
.ToList();
await Task.WhenAll(results);
public async IAsyncEnumerable<OrderResult> ProcessOrdersAsync(
IEnumerable<Order> orders,
[EnumeratorCancellation] CancellationToken ct = default)
{
foreach (var order in orders)
{
ct.ThrowIfCancellationRequested();
yield return await ProcessOrderAsync(order, ct);
}
}
var results = await Task.WhenAll(
orders.Select(o => ProcessOrderAsync(o)));
ValueTask vs Task
Use ValueTask for hot paths that often complete synchronously. For real I/O, just use Task.
public ValueTask<User?> GetUserAsync(UserId id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user);
}
return new ValueTask<User?>(FetchUserAsync(id));
}
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
return _repository.CreateAsync(cmd);
}
ValueTask rules:
- Never await a ValueTask more than once
- Never use
.Result or .GetAwaiter().GetResult() before completion
- If in doubt, use Task
Span and Memory for Bytes
Use Span<T> and Memory<T> instead of byte[] for low-level operations.
public static int ParseInt(ReadOnlySpan<char> text)
{
return int.Parse(text);
}
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
await _stream.WriteAsync(data);
}
public static int ParseInt(string text)
{
return int.Parse(text);
}
Common Span Patterns
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5];
Span<byte> buffer = stackalloc byte[256];
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
Collection Return Types
Return Immutable Collections from APIs
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList();
}
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
public List<Order> GetOrders()
{
return _orders;
}
Internal Mutation is Fine
public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
var items = new List<OrderItem>();
foreach (var cartItem in cart.Items)
{
items.Add(CreateOrderItem(cartItem));
}
return items;
}
Collection Guidelines
| Scenario | Return Type |
|---|
| API boundary | IReadOnlyList<T>, IReadOnlyCollection<T> |
| Static lookup data | FrozenDictionary<K,V>, FrozenSet<T> |
| Internal building | List<T>, then return as readonly |
| Single item or none | T? (nullable) |
| Zero or more, lazy | IEnumerable<T> |
Quick Reference
| Pattern | Benefit |
|---|
sealed class | Devirtualization, clear API |
readonly record struct | No defensive copies, value semantics |
| Static pure functions | No vtable, testable, thread-safe |
Defer .ToList() | Single materialization |
ValueTask for hot paths | Avoid Task allocation |
Span<T> for bytes | Stack allocation, no copying |
IReadOnlyList<T> return | Immutable API contract |
FrozenDictionary | Fastest lookup for static data |
Anti-Patterns
public class OrderService { }
public struct Point { public int X; public int Y; }
public int Add(int a, int b) => a + b;
items.Where(...).ToList().OrderBy(...).ToList();
public List<Order> GetOrders();
public ValueTask<Order> CreateOrderAsync();
Resources