---
description: C# Performance
alwaysApply: false
---

# C# Performance

Measure first. Optimize second. BenchmarkDotNet is your truth.

## Profile First

```bash
dotnet-counters monitor --process-id <pid>   # Live runtime metrics
dotnet-trace collect --process-id <pid>       # Performance tracing
```

Tools: dotnet-counters, dotnet-trace, dotnet-dump, dotnet-gcdump, BenchmarkDotNet

## Allocation Reduction

- **`ArrayPool<T>.Shared.Rent()`** — rent buffers instead of allocating
- **`ObjectPool<T>`** — reuse expensive-to-create objects
- **`stackalloc`** — small temporary buffers on stack
- **`Span<T>` / `ReadOnlySpan<T>`** — zero-allocation slicing

## struct vs class

- Use `struct` when: logically single value, < 16 bytes, immutable, rarely boxed
- Use `class` when: reference semantics needed, polymorphic, large

```csharp
public readonly record struct Coordinate(double Lat, double Lon);
```

## Collection Performance

- **`FrozenDictionary`** (.NET 8+) — read-heavy, write-once lookups
- **`CollectionsMarshal.GetValueRefOrAddDefault`** — avoid double lookup
- Right type: `HashSet` for contains, `Dictionary` for lookup, `List` for random access

## EF Core Performance

```csharp
var users = await _db.Users
    .AsNoTracking()                                    // Read-only
    .Where(u => u.IsActive)
    .Select(u => new UserDto(u.Id, u.Name, u.Email))  // Project to DTO
    .ToListAsync(ct);
```

- `AsNoTracking()` for read-only queries
- Project to DTOs — don't load full entities
- Use `Include()` or projection to avoid N+1
- Compiled queries for hot paths

## Caching

- **`IMemoryCache`** — single-instance, set sliding + absolute expiration
- **`IDistributedCache`** — multi-instance (Redis)
- **`HybridCache`** (.NET 9+) — stampede protection built-in

## Anti-Patterns

- **Optimizing without measurement** — prove it with BenchmarkDotNet
- **Multiple enumeration of `IEnumerable`** — materialize with `.ToList()` first
- **Blocking async in constructors** — use async factory method instead
