.agents/skills/csharp-type-design-performance/SKILL.md
Design .NET types for performance. Seal classes, use readonly structs, prefer static pure functions, avoid premature enumeration, and choose the right collection types.
npx skillsauth add woutervanranst/Arius7 type-design-performanceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when:
Sealing classes enables JIT devirtualization and communicates API intent.
// DO: Seal classes not designed for inheritance
public sealed class OrderProcessor
{
public void Process(Order order) { }
}
// DO: Seal records (they're classes)
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
// DON'T: Leave unsealed without reason
public class OrderProcessor // Can be subclassed - intentional?
{
public virtual void Process(Order order) { } // Virtual = slower
}
Benefits:
Structs should be readonly when immutable. This prevents defensive copies.
// DO: Readonly struct for immutable value types
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
// DO: Readonly struct for small, short-lived data
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// DON'T: Mutable struct (causes defensive copies)
public struct Point // Not readonly!
{
public int X { get; set; } // Mutable!
public int Y { get; set; }
}
| 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 |
Static methods with no side effects are faster and more testable.
// DO: Static pure function
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");
}
}
// Usage - predictable, testable
var total = OrderCalculator.CalculateTotal(items);
Benefits:
// DON'T: Instance method hiding dependencies
public class OrderCalculator
{
private readonly ITaxService _taxService; // Hidden dependency
private readonly IDiscountService _discountService; // Hidden dependency
public Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
// What does this actually depend on?
}
}
// BETTER: Explicit dependencies via parameters
public static class OrderCalculator
{
public static Money CalculateTotal(
IReadOnlyList<OrderItem> items,
decimal taxRate,
decimal discountPercent)
{
// All inputs visible
}
}
Don't go overboard - Use instance methods when you genuinely need state or polymorphism.
Don't materialize enumerables until necessary. Avoid excessive LINQ chains.
// BAD: Premature materialization
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.ToList() // Materialized!
.OrderBy(o => o.CreatedAt) // Another iteration
.ToList(); // Materialized again!
}
// GOOD: Defer until the end
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt)
.ToList(); // Single materialization
}
// GOOD: Return IEnumerable if caller might not need all items
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
// Caller decides when to materialize
}
Be careful with async and IEnumerable:
// BAD: Async in LINQ - hidden allocations
var results = orders
.Select(async o => await ProcessOrderAsync(o)) // Task per item!
.ToList();
await Task.WhenAll(results);
// GOOD: Use IAsyncEnumerable for streaming
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);
}
}
// GOOD: Batch processing for parallelism
var results = await Task.WhenAll(
orders.Select(o => ProcessOrderAsync(o)));
Use ValueTask for hot paths that often complete synchronously. For real I/O, just use Task.
// DO: ValueTask for cached/synchronous paths
public ValueTask<User?> GetUserAsync(UserId id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user); // No allocation
}
return new ValueTask<User?>(FetchUserAsync(id));
}
// DO: Task for real I/O (simpler, no footguns)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
// This always hits the database
return _repository.CreateAsync(cmd);
}
ValueTask rules:
.Result or .GetAwaiter().GetResult() before completionUse Span<T> and Memory<T> instead of byte[] for low-level operations.
// DO: Accept Span for synchronous operations
public static int ParseInt(ReadOnlySpan<char> text)
{
return int.Parse(text);
}
// DO: Accept Memory for async operations
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
await _stream.WriteAsync(data);
}
// DON'T: Force array allocation
public static int ParseInt(string text) // String allocated
{
return int.Parse(text);
}
// Slice without allocation
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5]; // No allocation
// Stack allocation for small buffers
Span<byte> buffer = stackalloc byte[256];
// Use ArrayPool for larger buffers
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
// Use buffer...
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
// DO: Return immutable collection
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList(); // Caller can't modify internal state
}
// DO: Use frozen collections for static data (.NET 8+)
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
// DON'T: Return mutable collection
public List<Order> GetOrders()
{
return _orders; // Caller can modify!
}
public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
var items = new List<OrderItem>(); // Mutable internally
foreach (var cartItem in cart.Items)
{
items.Add(CreateOrderItem(cartItem));
}
return items; // Return as IReadOnlyList
}
| 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> |
| 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 |
// DON'T: Unsealed class without reason
public class OrderService { } // Seal it!
// DON'T: Mutable struct
public struct Point { public int X; public int Y; } // Make readonly
// DON'T: Instance method that could be static
public int Add(int a, int b) => a + b; // Make static
// DON'T: Multiple ToList() calls
items.Where(...).ToList().OrderBy(...).ToList(); // One ToList at end
// DON'T: Return List<T> from public API
public List<Order> GetOrders(); // Return IReadOnlyList<T>
// DON'T: ValueTask for always-async operations
public ValueTask<Order> CreateOrderAsync(); // Just use Task
testing
Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.
data-ai
Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
development
Guided onboarding for OpenSpec - walk through a complete workflow cycle with narration and real codebase work.
tools
Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.