| name | csharp-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. Use when designing or reviewing .NET types, classes, structs, and collections for performance. |
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