# Dotnet Performance

> Performance-aware .NET design decisions and where they matter - the layer that decides whether an allocation/memory-layout or serialization-format choice is worth spending on here. Type design: struct vs class, readonly struct, seal by default, fewer allocations, `Span`, `ValueTask`, frozen/immutable returns. Serialization: pick the format - `System.Text.Json` source-gen for JSON, Protobuf for wire, MessagePack for cache/messaging. Load when a type sits on a hot path or high-throughput loop, or when choosing how bytes cross a process boundary. Do NOT start here for 'my app is slow' - that is usually a bad query or an N+1: route to `dotnet-diagnostics` and measure first. Companions: `csharp`, `dotnet`, `dotnet-diagnostics`.

- Skill: `envoydev/dotnet-performance` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add envoydev/dotnet-performance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/envoydev/dotnet-performance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: envoydev (https://skillmd.com/u/envoydev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/envoydev/dotnet-performance

---


# dotnet-performance (decision layer)

Two performance-aware design calls that are cheap to get right up front and expensive to retrofit: how a type allocates, and how bytes cross a boundary. This hub decides *whether* the call is worth spending on here and routes to the depth - it does not restate it.

- Type allocation / memory layout -> `references/type-design.md`
- Serialization-format choice -> `references/serialization.md`
- .NET Framework 4.8 caveats (the 'slow span', NuGet-only fast-path packages) -> `references/net-framework-48.md`

The language baseline (naming, async, records, disposal, DI) is `csharp`; the full .NET map is `dotnet`.

## Measure first

Do not optimize on a hunch. A performance change is only earned by a measurement: a BenchmarkDotNet microbenchmark for a hot path, a profiler or dump for a live regression, allocation counts under load. Reach for `dotnet-diagnostics` before you tune. Most 'slow' code is a bad query or an N+1, not a struct-vs-class problem - profile before you touch a type, because optimizing the wrong layer buys nothing and costs readability.

## When allocation and memory layout matter (type design)

Spend on the type-design defaults when a type sits on a **hot path** - a per-request allocation inside a tight loop, a high-throughput pipeline, a `BackgroundService` draining a channel (`dotnet-hosted-services`), a serializer inner loop. There the choices pay their way: seal by default, a small immutable value becomes a readonly struct, byte work moves to `Span`, a usually-cached async result returns `ValueTask`, static lookup data becomes a `FrozenDictionary`.

Off the hot path, do not contort a domain model for allocations you never measured - correctness and clarity win. The seal-by-default and immutable-return defaults still apply everywhere, though, because they cost nothing and prevent whole classes of bug. Load `references/type-design.md` for the rules and the anti-patterns.

## When serialization-format choice matters

The format decision is a wire-compatibility decision, and it is hard to reverse once data is persisted or a contract is published. Decide by where the bytes go - the pick-by-destination table is in `references/serialization.md`.

Inside a single process (in-memory only), format is irrelevant - do not serialize at all. The rules that override taste: never `BinaryFormatter`, and never embed .NET type names in a payload (it breaks on the first rename). Load `references/serialization.md` for setup, the Newtonsoft migration, and the versioning rules; the Roslyn source-generator skill owns the source-gen mechanics and the ASP.NET web hub the JSON wiring - with neither installed, the `JsonSerializerContext` snippet in that reference is enough to ship.

