Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
aaronontheweb avatar

Type Design Performance

  • 485 installs
  • 1.1k repo stars
  • Updated July 3, 2026
  • aaronontheweb/dotnet-skills

type-design-performance is an agent skill that designs .NET structs, records, and hot-path types to cut allocations and improve throughput for developers profiling GC pressure, cache misses, or collection overhead in C#

About

type-design-performance is a non-invocable reference skill in Aaronontheweb/dotnet-skills, a Claude Code plugin with 30 skills and 5 specialized agents for production .NET development. It teaches five core principles: seal classes by default for JIT devirtualization, prefer readonly structs for small immutable value types, use static pure functions, defer enumeration until materialization is required, and return immutable collections from API boundaries. Guidance covers choosing between class, struct, and record, avoiding defensive copies, and selecting performant collection types on hot paths. The dotnet-skills README routes C# performance reviews to type-design-performance alongside modern-csharp-coding-standards and api-design. Reach for it when designing new types, reviewing allocation-heavy APIs, or refactoring enumerables that trigger excess GC collections in .NET services.

  • Struct vs class decisions
  • Record and readonly patterns
  • Span and memory usage
  • Allocation-free hot paths
  • Benchmark-driven type tweaks

Type Design Performance by the numbers

  • 485 all-time installs (skills.sh)
  • Ranked #40 of 153 .NET & C# skills by installs in the Skillselion catalog
  • Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aaronontheweb/dotnet-skills --skill type-design-performance

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs485
repo stars1.1k
Last updatedJuly 3, 2026
Repositoryaaronontheweb/dotnet-skills

How do you design .NET types for performance?

Design structs, records, and hot-path types in .NET to cut allocations and improve throughput when profiling shows GC pressure or cache misses.

Who is it for?

.NET developers designing or reviewing hot-path types when profilers show GC pressure, boxing, or unnecessary enumeration materialization.

Skip if: Greenfield projects with no performance constraints, or teams needing BenchmarkDotNet setup rather than type-level design guidance.

When should I use this skill?

The user designs new C# types, chooses between class struct and record, or reviews code where profiling shows GC pressure or cache misses on hot paths.

What you get

Performance-oriented C# type designs with sealed classes, readonly structs, pure static helpers, deferred IEnumerable usage, and immutable collection returns.

  • sealed type designs
  • readonly struct patterns
  • collection API guidance

By the numbers

  • Documents 5 core performance principles for sealed classes, readonly structs, and enumeration
  • Part of dotnet-skills plugin with 30 skills and 5 specialized agents

Files

SKILL.mdMarkdownGitHub ↗

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

1. Seal your types - Unless explicitly designed for inheritance 2. Prefer readonly structs - For small, immutable value types 3. Prefer static pure functions - Better performance and testability 4. Defer enumeration - Don't materialize until you need to 5. Return immutable collections - From API boundaries

---

Seal Classes by Default

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:

  • 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.

// 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; }
}

When to Use Structs

Use Struct WhenUse Class When
Small (≤16 bytes typically)Larger objects
Short-livedLong-lived
Frequently allocatedShared references needed
Value semantics requiredIdentity semantics required
ImmutableMutable state

---

Prefer Static Pure Functions

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:

  • No vtable lookup (faster)
  • No hidden state
  • Easier to test (pure input → output)
  • Thread-safe by design
  • Forces explicit dependencies
// 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.

---

Defer Enumeration

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
}

Async Enumeration

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)));

---

ValueTask vs Task

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:

  • 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.

// 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);
}

Common Span Patterns

// 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);
}

---

Collection Return Types

Return Immutable Collections from APIs

// 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!
}

Internal Mutation is Fine

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
}

Collection Guidelines

ScenarioReturn Type
API boundaryIReadOnlyList<T>, IReadOnlyCollection<T>
Static lookup dataFrozenDictionary<K,V>, FrozenSet<T>
Internal buildingList<T>, then return as readonly
Single item or noneT? (nullable)
Zero or more, lazyIEnumerable<T>

---

Quick Reference

PatternBenefit
sealed classDevirtualization, clear API
readonly record structNo defensive copies, value semantics
Static pure functionsNo vtable, testable, thread-safe
Defer .ToList()Single materialization
ValueTask for hot pathsAvoid Task allocation
Span<T> for bytesStack allocation, no copying
IReadOnlyList<T> returnImmutable API contract
FrozenDictionaryFastest lookup for static data

---

Anti-Patterns

// 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

---

Resources

  • Performance Best Practices: https://learn.microsoft.com/en-us/dotnet/standard/performance/
  • Span<T> Guidance: https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/
  • Frozen Collections: https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen

Related skills

How it compares

Pick type-design-performance for type-level C# allocation design; use dotnet-performance-analyst agent when interpreting profiler traces or BenchmarkDotNet results holistically.

FAQ

When should type-design-performance be used?

type-design-performance applies when designing new .NET types, reviewing performance issues, choosing between class, struct, and record, or optimizing collections and enumerables on profiler-identified hot paths.

What principles does type-design-performance emphasize?

type-design-performance teaches five rules: seal types by default, prefer readonly structs, use static pure functions, defer enumeration, and return immutable collections from API boundaries to cut allocations.

.NET & C#backend

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.