CommunityCodierung & Entwicklunggithub.com

type-design-performance

Entwerfen Sie .NET-Typen für hohe Leistung: Klassen versiegeln, readonly-Strukturen verwenden, statische reine Funktionen bevorzugen, vorzeitige Enumeration vermeiden und die richtigen Sammlungstypen wählen.

Was ist type-design-performance?

Diese Skill-Richtlinie beschreibt bewährte Methoden für das Design leistungsoptimierter .NET-Typen. Sie ist für Entwickler gedacht, die neue Typen und APIs entwerfen, Code auf Performance-Probleme überprüfen oder sich zwischen class, struct und record entscheiden müssen. Die Kernprinzipien umfassen das Versiegeln von Klassen, um JIT-Devirtualisierung zu ermöglichen, die Verwendung von readonly-Strukturen für kleine, unveränderliche Werttypen, die Bevorzugung statischer reiner Funktionen für bessere Testbarkeit und Thread-Sicherheit, die Aufschiebung der Enumeration (z. B. durch Vermeidung von vorzeitigem .ToList()) sowie die Wahl der richtigen Sammlungsrückgabetypen (z. B. IReadOnlyList aus APIs oder FrozenDictionary für statische Daten). Die Anleitung behandelt auch den effizienten Umgang mit async-Enumerationen, die Unterscheidung zwischen ValueTask und Task sowie die Verwendung von Span<T> und Memory<T> für low-level Byte-Operationen. Anti-Patterns wie mutable Strukturen, unversiegelte Klassen ohne Grund oder mehrfache ToList()-Aufrufe werden deutlich benannt. Diese Skill ist plattformunabhängig für alle .NET-Projekte anwendbar.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/csharp-type-design-performance

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Typ-Design für Leistung

Wann diese Skill verwendet werden sollte

Verwenden Sie diese Skill, wenn:

  • Sie neue Typen und APIs entwerfen
  • Sie Code auf Leistungsprobleme überprüfen
  • Sie sich zwischen class, struct und record entscheiden
  • Sie mit Sammlungen und Enumerables arbeiten

Kernprinzipien

  1. Typen versiegeln – Sofern nicht explizit für Vererbung ausgelegt
  2. Readonly-Strukturen bevorzugen – Für kleine, unveränderliche Werttypen
  3. Statische reine Funktionen bevorzugen – Bessere Leistung und Testbarkeit
  4. Enumeration aufschieben – Nicht materialisieren, bis es nötig ist
  5. Unveränderliche Sammlungen zurückgeben – An API-Grenzen

Klassen standardmäßig versiegeln

Das Versiegeln von Klassen ermöglicht JIT-Devirtualisierung und kommuniziert die API-Intention.

// DO: Klassen versiegeln, die nicht für Vererbung ausgelegt sind
public sealed class OrderProcessor
{
    public void Process(Order order) { }
}

// DO: Records versiegeln (es sind Klassen)
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);

// DON'T: Ohne Grund unversiegelt lassen
public class OrderProcessor  // Kann abgeleitet werden – beabsichtigt?
{
    public virtual void Process(Order order) { }  // Virtual = langsamer
}

Vorteile:

  • Der JIT kann Methodenaufrufe devirtualisieren
  • Kommuniziert „Dies ist kein Erweiterungspunkt“
  • Verhindert versehentliche Breaking Changes

Readonly-Strukturen für Werttypen

Strukturen sollten readonly sein, wenn sie unveränderlich sind. Dies verhindert defensive Kopien.

// DO: Readonly-Struktur für unveränderliche Werttypen
public readonly record struct OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
    public override string ToString() => Value.ToString();
}

// DO: Readonly-Struktur für kleine, kurzlebige Daten
public readonly struct Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }
}

// DON'T: Veränderbare Struktur (verursacht defensive Kopien)
public struct Point  // Nicht readonly!
{
    public int X { get; set; }  // Veränderbar!
    public int Y { get; set; }
}

Wann Strukturen verwenden

Struktur verwenden, wennKlasse verwenden, wenn
Klein (typisch ≤16 Byte)Größere Objekte
KurzlebigLanglebig
Häufig allokiertGemeinsame Referenzen benötigt
Wertesemantik erforderlichIdentitätssemantik erforderlich
UnveränderlichVeränderbarer Zustand

Statische reine Funktionen bevorzugen

Statische Methoden ohne Seiteneffekte sind schneller und besser testbar.

// DO: Statische reine Funktion
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");
    }
}

// Verwendung – vorhersagbar, testbar
var total = OrderCalculator.CalculateTotal(items);

Vorteile:

  • Keine Vtable-Lookup (schneller)
  • Kein versteckter Zustand
  • Einfacher zu testen (reine Eingabe → Ausgabe)
  • Von Natur aus threadsicher
  • Erzwingt explizite Abhängigkeiten
// DON'T: Instanzmethode, die Abhängigkeiten versteckt
public class OrderCalculator
{
    private readonly ITaxService _taxService;  // Versteckte Abhängigkeit
    private readonly IDiscountService _discountService;  // Versteckte Abhängigkeit

    public Money CalculateTotal(IReadOnlyList<OrderItem> items)
    {
        // Wovon hängt das eigentlich ab?
    }
}

// BESSER: Explizite Abhängigkeiten über Parameter
public static class OrderCalculator
{
    public static Money CalculateTotal(
        IReadOnlyList<OrderItem> items,
        decimal taxRate,
        decimal discountPercent)
    {
        // Alle Eingaben sichtbar
    }
}

Übertreiben Sie es nicht – Verwenden Sie Instanzmethoden, wenn Sie wirklich Zustand oder Polymorphie benötigen.


Enumeration aufschieben

Materialisieren Sie Enumerables nicht, bevor es nötig ist. Vermeiden Sie übermäßige LINQ-Ketten.

// SCHLECHT: Vorzeitige Materialisierung
public IReadOnlyList<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .ToList()  // Materialisiert!
        .OrderBy(o => o.CreatedAt)  // Weitere Iteration
        .ToList();  // Schon wieder materialisiert!
}

// GUT: Bis zum Ende aufschieben
public IReadOnlyList<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .OrderBy(o => o.CreatedAt)
        .ToList();  // Einmalige Materialisierung
}

// GUT: IEnumerable zurückgeben, wenn der Aufrufer möglicherweise nicht alle Elemente benötigt
public IEnumerable<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .OrderBy(o => o.CreatedAt);
    // Der Aufrufer entscheidet, wann materialisiert wird
}

Async-Enumeration

Seien Sie vorsichtig mit async und IEnumerable:

// SCHLECHT: Async in LINQ – versteckte Allokationen
var results = orders
    .Select(async o => await ProcessOrderAsync(o))  // Task pro Element!
    .ToList();
await Task.WhenAll(results);

// GUT: IAsyncEnumerable für Streaming verwenden
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);
    }
}

// GUT: Batch-Verarbeitung für Parallelität
var results = await Task.WhenAll(
    orders.Select(o => ProcessOrderAsync(o)));

ValueTask vs. Task

Verwenden Sie ValueTask für heiße Pfade, die oft synchron abgeschlossen werden. Für echte I/O verwenden Sie einfach Task.

// DO: ValueTask für gecachte/synchrone Pfade
public ValueTask<User?> GetUserAsync(UserId id)
{
    if (_cache.TryGetValue(id, out var user))
    {
        return ValueTask.FromResult<User?>(user);  // Keine Allokation
    }

    return new ValueTask<User?>(FetchUserAsync(id));
}

// DO: Task für echte I/O (einfacher, keine Fallstricke)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
    // Dies trifft immer die Datenbank
    return _repository.CreateAsync(cmd);
}

ValueTask-Regeln:

  • Ein ValueTask niemals mehr als einmal erwarten
  • Niemals .Result oder .GetAwaiter().GetResult() vor Abschluss verwenden
  • Im Zweifel Task verwenden

Span und Memory für Bytes

Verwenden Sie Span<T> und Memory<T> anstelle von byte[] für Low-Level-Operationen.

// DO: Span für synchrone Operationen akzeptieren
public static int ParseInt(ReadOnlySpan<char> text)
{
    return int.Parse(text);
}

// DO: Memory für asynchrone Operationen akzeptieren
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
    await _stream.WriteAsync(data);
}

// DON'T: Array-Allokation erzwingen
public static int ParseInt(string text)  // String allokiert
{
    return int.Parse(text);
}

Häufige Span-Muster

// Slice ohne Allokation
ReadOnlySpan<char> span = "Hallo, Welt!".AsSpan();
var hallo = span[..5];  // Keine Allokation

// Stack-Allokation für kleine Puffer
Span<byte> buffer = stackalloc byte[256];

// ArrayPool für größere Puffer verwenden
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
    // Puffer verwenden...
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

Sammlungsrückgabetypen

Unveränderliche Sammlungen aus APIs zurückgeben

// DO: Unveränderliche Sammlung zurückgeben
public IReadOnlyList<Order> GetOrders()
{
    return _orders.ToList();  // Der Aufrufer kann den internen Zustand nicht ändern
}

// DO: Frozen Collections für statische Daten verwenden (.NET 8+)
private static readonly FrozenDictionary<string, Handler> _handlers =
    new Dictionary<string, Handler>
    {
        ["create"] = new CreateHandler(),
        ["update"] = new UpdateHandler(),
    }.ToFrozenDictionary();

// DON'T: Veränderbare Sammlung zurückgeben
public List<Order> GetOrders()
{
    return _orders;  // Der Aufrufer kann sie ändern!
}

Interne Mutation ist in Ordnung

public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
    var items = new List<OrderItem>();  // Intern veränderbar

    foreach (var cartItem in cart.Items)
    {
        items.Add(CreateOrderItem(cartItem));
    }

    return items;  // Als IReadOnlyList zurückgeben
}

Richtlinien für Sammlungen

SzenarioRückgabetyp
API-GrenzeIReadOnlyList<T>, IReadOnlyCollection<T>
Statische Lookup-DatenFrozenDictionary<K,V>, FrozenSet<T>
Internes AufbauenList<T>, dann als readonly zurückgeben
Einzelnes Element oder keinsT? (nullable)
Null oder mehr, lazyIEnumerable<T>

Kurzreferenz

MusterVorteil
sealed classDevirtualisierung, klare API
readonly record structKeine defensiven Kopien, Wertesemantik
Statische reine FunktionenKeine Vtable, testbar, threadsicher
.ToList() aufschiebenEinmalige Materialisierung
ValueTask für heiße PfadeTask-Allokation vermeiden
Span<T> für BytesStack-Allokation, kein Kopieren
IReadOnlyList<T> RückgabeUnveränderlicher API-Vertrag
FrozenDictionarySchnellster Lookup für statische Daten

Anti-Patterns

// DON'T: Unversiegelte Klasse ohne Grund
public class OrderService { }  // Versiegeln Sie es!

// DON'T: Veränderbare Struktur
public struct Point { public int X; public int Y; }  // Readonly machen

// DON'T: Instanzmethode, die statisch sein könnte
public int Add(int a, int b) => a + b;  // Statisch machen

// DON'T: Mehrere ToList()-Aufrufe
items.Where(...).ToList().OrderBy(...).ToList();  // Ein ToList() am Ende

// DON'T: List<T> aus einer öffentlichen API zurückgeben
public List<Order> GetOrders();  // IReadOnlyList<T> zurückgeben

// DON'T: ValueTask für immer asynchrone Operationen
public ValueTask<Order> CreateOrderAsync();  // Einfach Task verwenden

Ressourcen

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

Verwandte Skills