Was macht Moderne C#-Codierungsstandards?
Wann dieser Skill verwendet werden sollte
Verwende diesen Skill, wenn:
- Du neuen C#-Code schreibst oder bestehenden Code umgestaltest
- Du öffentliche APIs für Bibliotheken oder Dienste entwirfst
- Du leistungskritische Codepfade optimierst
- Du Domänenmodelle mit starker Typisierung implementierst
- Du stark async/await-lastige Anwendungen entwickelst
- Du mit Binärdaten, Puffern oder Szenarien mit hohem Durchsatz arbeitest
Referenzdateien
- value-objects-and-patterns.md: Vollständige Value-Object-Beispiele und Pattern-Matching-Code
- performance-and-api-design.md: Span/Memory-Beispiele und API-Entwurfsprinzipien
- composition-and-error-handling.md: Komposition vor Vererbung, Result-Typ, Testmuster
- anti-patterns-and-reflection.md: Vermeidung von Reflection und häufige Anti-Patterns
Kernprinzipien
- Unveränderlichkeit als Standard – Verwende
record-Typen undinit-only-Eigenschaften - Typsicherheit – Nutze nullable Referenztypen und Value Objects
- Modernes Pattern Matching – Verwende
switch-Ausdrücke und Patterns umfassend - Async überall – Bevorzuge asynchrone APIs mit ordnungsgemäßer Unterstützung für Abbruch
- Null-Allocation-Muster – Verwende
Span<T>undMemory<T>für leistungskritischen Code - API-Entwurf – Akzeptiere Abstraktionen, gib geeignet spezifische Typen zurück
- Komposition vor Vererbung – Vermeide abstrakte Basisklassen, bevorzuge Komposition
- Value Objects als Structs – Verwende
readonly record structfür Value Objects
Sprachmuster
Records für unveränderliche Daten (C# 9+)
Verwende record-Typen für DTOs, Nachrichten, Ereignisse und Domänenentitäten.
// Einfaches unveränderliches DTO
public record CustomerDto(string Id, string Name, string Email);
// Record mit Validierung im Konstruktor
public record EmailAddress
{
public string Value { get; init; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
throw new ArgumentException("Ungültige E-Mail-Adresse", nameof(value));
Value = value;
}
}
// Records mit Sammlungen – verwende IReadOnlyList
public record ShoppingCart(
string CartId,
string CustomerId,
IReadOnlyList<CartItem> Items
)
{
public decimal Total => Items.Sum(item => item.Price * item.Quantity);
}
Wann record class vs. record struct verwenden:
record class(Standard): Referenztypen, verwende für Entitäten, Aggregate, DTOs mit mehreren Eigenschaftenrecord struct: Werttypen, verwende für Value Objects (siehe nächster Abschnitt)
Value Objects als readonly record struct
Value Objects sollten immer readonly record struct sein, für Leistung und Wertesemantik. Verwende explizite Konvertierungen, niemals implizite Operatoren.
public readonly record struct OrderId(string Value)
{
public OrderId(string value) : this(
!string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("OrderId darf nicht leer sein", nameof(value)))
{ }
public override string ToString() => Value;
}
public readonly record struct Money(decimal Amount, string Currency);
public readonly record struct CustomerId(Guid Value)
{
public static CustomerId New() => new(Guid.NewGuid());
}
Siehe value-objects-and-patterns.md für vollständige Beispiele, darunter Multi-Value-Objects, Factory-Muster und die Regel der fehlenden impliziten Konvertierung.
Pattern Matching (C# 8-12)
Verwende switch-Ausdrücke, Eigenschaftsmuster, relationale Muster und Listenmuster für saubereren Code.
public decimal CalculateDiscount(Order order) => order switch
{
{ Total: > 1000m } => order.Total * 0.15m,
{ Total: > 500m } => order.Total * 0.10m,
{ Total: > 100m } => order.Total * 0.05m,
_ => 0m
};
Siehe value-objects-and-patterns.md für vollständige Pattern-Matching-Beispiele.
Nullable Referenztypen (C# 8+)
Aktiviere nullable Referenztypen in deinem Projekt und behandle Nullwerte explizit.
// In .csproj
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// Explizite Nullable-Behandlung
public string? FindUserName(string userId)
{
var user = _repository.Find(userId);
return user?.Name;
}
// Pattern Matching mit Null-Prüfungen
public decimal GetDiscount(Customer? customer) => customer switch
{
null => 0m,
{ IsVip: true } => 0.20m,
{ OrderCount: > 10 } => 0.10m,
_ => 0.05m
};
// Guard-Klauseln mit ArgumentNullException.ThrowIfNull (C# 11+)
public void ProcessOrder(Order? order)
{
ArgumentNullException.ThrowIfNull(order);
// order ist jetzt in diesem Gültigkeitsbereich nicht mehr nullable
Console.WriteLine(order.Id);
}
Komposition vor Vererbung
Vermeide abstrakte Basisklassen. Verwende Schnittstellen + Komposition. Verwende statische Helfer für gemeinsame Logik. Verwende Records mit Factory-Methoden für Varianten.
Siehe composition-and-error-handling.md für vollständige Beispiele.
Leistungsmuster
Async/Await Best Practices
// Async bis zum Ende – akzeptiere immer CancellationToken
public async Task<Order> GetOrderAsync(string orderId, CancellationToken cancellationToken)
{
var order = await _repository.GetAsync(orderId, cancellationToken);
return order;
}
// ValueTask für häufig aufgerufene, oft synchrone Methoden
public ValueTask<Order?> GetCachedOrderAsync(string orderId, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(orderId, out var order))
return ValueTask.FromResult<Order?>(order);
return GetFromDatabaseAsync(orderId, cancellationToken);
}
// IAsyncEnumerable für Streaming
public async IAsyncEnumerable<Order> StreamOrdersAsync(
string customerId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var order in _repository.StreamAllAsync(cancellationToken))
{
if (order.CustomerId == customerId)
yield return order;
}
}
Wichtige Regeln:
- Akzeptiere immer
CancellationTokenmit= default - Verwende
ConfigureAwait(false)in Bibliothekscode - Blockiere niemals asynchronen Code (kein
.Resultoder.Wait()) - Verwende verknüpfte CancellationTokenSource für Zeitüberschreitungen
Span und Memory
Verwende Span<T> für synchrone Null-Allocation-Operationen, Memory<T> für asynchrone Operationen und ArrayPool<T> für große temporäre Puffer.
Siehe performance-and-api-design.md für vollständige Span/Memory-Beispiele und den API-Entwurfsabschnitt.
Fehlerbehandlung: Result-Typ
Verwende für erwartete Fehler Result<T, TError> anstelle von Ausnahmen. Verwende Ausnahmen nur für unerwartete/Systemfehler.
Siehe composition-and-error-handling.md für die vollständige Implementierung des Result-Typs und Nutzungsbeispiele.
Vermeide reflektionsbasierte Metaprogrammierung
Verboten: AutoMapper, Mapster, ExpressMapper. Verwende stattdessen explizite Mapping-Erweiterungsmethoden. Verwende UnsafeAccessorAttribute (.NET 8+), wenn du wirklich Zugriff auf private Member benötigst.
Siehe anti-patterns-and-reflection.md für vollständige Anleitungen.
Code-Organisation
// Datei: Domain/Orders/Order.cs
namespace MyApp.Domain.Orders;
// 1. Primärer Domänentyp
public record Order(
OrderId Id,
CustomerId CustomerId,
Money Total,
OrderStatus Status,
IReadOnlyList<OrderItem> Items
)
{
public bool IsCompleted => Status is OrderStatus.Completed;
public Result<Order, OrderError> AddItem(OrderItem item)
{
if (Status is not OrderStatus.Draft)
return Result<Order, OrderError>.Failure(
new OrderError("ORDER_NOT_DRAFT", "Elemente können nur zu Entwurfsbestellungen hinzugefügt werden"));
var newItems = Items.Append(item).ToList();
var newTotal = new Money(
Items.Sum(i => i.Total.Amount) + item.Total.Amount,
Total.Currency);
return Result<Order, OrderError>.Success(
this with { Items = newItems, Total = newTotal });
}
}
// 2. Enums für Zustände
public enum OrderStatus { Draft, Submitted, Processing, Completed, Cancelled }
// 3. Verwandte Typen
public record OrderItem(ProductId ProductId, Quantity Quantity, Money UnitPrice)
{
public Money Total => new(UnitPrice.Amount * Quantity.Value, UnitPrice.Currency);
}
// 4. Value Objects
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
}
// 5. Fehler
public readonly record struct OrderError(string Code, string Message);
Zusammenfassung der Best Practices
TUN
- Verwende
recordfür DTOs, Nachrichten und Domänenentitäten - Verwende
readonly record structfür Value Objects - Nutze Pattern Matching mit
switch-Ausdrücken - Aktiviere und respektiere nullable Referenztypen
- Verwende async/await für alle I/O-Operationen
- Akzeptiere
CancellationTokenin allen asynchronen Methoden - Verwende
Span<T>undMemory<T>für leistungskritische Szenarien - Akzeptiere Abstraktionen (
IEnumerable<T>,IReadOnlyList<T>) - Verwende
Result<T, TError>für erwartete Fehler - Puffere mit
ArrayPool<T>für große Allokationen - Bevorzuge Komposition vor Vererbung
NICHT TUN
- Verwende keine veränderlichen Klassen, wenn Records funktionieren
- Verwende keine Klassen für Value Objects (verwende
readonly record struct) - Erstelle keine tiefen Vererbungshierarchien
- Ignoriere keine Warnungen zu nullable Referenztypen
- Blockiere nicht auf asynchronem Code (
.Result,.Wait()) - Verwende nicht
byte[], wennSpan<byte>ausreicht - Vergiss keine
CancellationToken-Parameter - Gib keine veränderlichen Sammlungen von APIs zurück
- Wirf keine Ausnahmen für erwartete Geschäftsfehler
- Alloziere nicht wiederholt große Arrays (verwende
ArrayPool)
Siehe anti-patterns-and-reflection.md für detaillierte Anti-Pattern-Beispiele.
Zusätzliche Ressourcen
- C#-Sprachspezifikation: https://learn.microsoft.com/de-de/dotnet/csharp/
- Pattern Matching: https://learn.microsoft.com/de-de/dotnet/csharp/fundamentals/functional/pattern-matching
- Span und Memory: https://learn.microsoft.com/de-de/dotnet/standard/memory-and-spans/
- Async Best Practices: https://learn.microsoft.com/de-de/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
- .NET-Leistungstipps: https://learn.microsoft.com/de-de/dotnet/framework/performance/