.NET-Entwicklungsmuster
Idiomatische C#- und .NET-Muster zum Erstellen robuster, performanter und wartbarer Anwendungen.
Wann aktivieren
- Schreiben von neuem C#-Code
- Überprüfen von C#-Code
- Refactorn vorhandener .NET-Anwendungen
- Entwerfen von Dienstarten mit ASP.NET Core
Kernprinzipien
1. Unveränderlichkeit bevorzugen
Verwenden Sie Records und init-only-Properties für Datenmodelle. Veränderlichkeit sollte eine explizite, begründete Wahl sein.
// Gut: Unveränderliches Wertobjekt
public sealed record Money(decimal Betrag, string Währung);
// Gut: Unveränderliches DTO mit Init-Setzern
public sealed class CreateOrderRequest
{
public required string KundenId { get; init; }
public required IReadOnlyList<OrderItem> Positionen { get; init; }
}
// Schlecht: Veränderliches Modell mit öffentlichen Setzern
public class Bestellung
{
public string KundenId { get; set; }
public List<OrderItem> Positionen { get; set; }
}
2. Explizit statt implizit
Seien Sie klar bei Nullable, Zugriffsmodifikatoren und Absicht.
// Gut: Explizite Zugriffsmodifikatoren und Nullable
public sealed class BenutzerService
{
private readonly IBenutzerRepository _repository;
private readonly ILogger<BenutzerService> _logger;
public BenutzerService(IBenutzerRepository repository, ILogger<BenutzerService> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<Benutzer?> FindePerIdAsync(Guid id, CancellationToken cancellationToken)
{
return await _repository.FindByIdAsync(id, cancellationToken);
}
}
3. Von Abstraktionen abhängen
Verwenden Sie Schnittstellen für Dienstgrenzen. Registrierung über DI-Container.
// Gut: Schnittstellenbasierte Abhängigkeit
public interface IBestellungRepository
{
Task<Bestellung?> FindePerIdAsync(Guid id, CancellationToken cancellationToken);
Task<IReadOnlyList<Bestellung>> FindePerKundeAsync(string kundenId, CancellationToken cancellationToken);
Task FügeHinzuAsync(Bestellung bestellung, CancellationToken cancellationToken);
}
// Registrierung
builder.Services.AddScoped<IBestellungRepository, SqlBestellungRepository>();
Async/Await-Muster
Korrekte Async-Verwendung
// Gut: Async durchgängig mit CancellationToken
public async Task<BestellungsZusammenfassung> HoleBestellungsZusammenfassungAsync(
Guid bestellungsId,
CancellationToken cancellationToken)
{
var bestellung = await _repository.FindByIdAsync(bestellungsId, cancellationToken)
?? throw new NotFoundException($"Bestellung {bestellungsId} nicht gefunden");
var kunde = await _kundenService.HoleAsync(bestellung.KundenId, cancellationToken);
return new BestellungsZusammenfassung(bestellung, kunde);
}
// Schlecht: Blockieren auf async
public BestellungsZusammenfassung HoleBestellungsZusammenfassung(Guid bestellungsId)
{
var bestellung = _repository.FindByIdAsync(bestellungsId, CancellationToken.None).Result; // Deadlock-Risiko
return new BestellungsZusammenfassung(bestellung);
}
Parallele Async-Operationen
// Gut: Gleichzeitige unabhängige Operationen
public async Task<DashboardDaten> LadeDashboardAsync(CancellationToken cancellationToken)
{
var bestellungenTask = _bestellungsService.HoleAktuelleAsync(cancellationToken);
var metrikenTask = _metrikService.HoleAktuelleAsync(cancellationToken);
var benachrichtigungenTask = _benachrichtigungsService.HoleAktiveAsync(cancellationToken);
await Task.WhenAll(bestellungenTask, metrikenTask, benachrichtigungenTask);
return new DashboardDaten(
Bestellungen: await bestellungenTask,
Metriken: await metrikenTask,
Benachrichtigungen: await benachrichtigungenTask);
}
Options-Muster
Binden Sie Konfigurationsabschnitte an stark typisierte Objekte.
public sealed class SmtpOptionen
{
public const string AbschnittsName = "Smtp";
public required string Host { get; init; }
public required int Port { get; init; }
public required string Benutzername { get; init; }
public bool BenutzeSsl { get; init; } = true;
}
// Registrierung
builder.Services.Configure<SmtpOptionen>(
builder.Configuration.GetSection(SmtpOptionen.AbschnittsName));
// Verwendung via Injection
public class EmailDienst(IOptions<SmtpOptionen> optionen)
{
private readonly SmtpOptionen _smtp = optionen.Value;
}
Ergebnis-Muster
Geben Sie expliziten Erfolg/Misserfolg zurück, anstatt bei erwarteten Fehlern Ausnahmen zu werfen.
public sealed record Ergebnis<T>
{
public bool IstErfolg { get; }
public T? Wert { get; }
public string? Fehler { get; }
private Ergebnis(T wert) { IstErfolg = true; Wert = wert; }
private Ergebnis(string fehler) { IstErfolg = false; Fehler = fehler; }
public static Ergebnis<T> Erfolg(T wert) => new(wert);
public static Ergebnis<T> Fehlschlag(string fehler) => new(fehler);
}
// Verwendung
public async Task<Ergebnis<Bestellung>> PlatziereBestellungAsync(CreateOrderRequest anfrage)
{
if (anfrage.Positionen.Count == 0)
return Ergebnis<Bestellung>.Fehlschlag("Bestellung muss mindestens eine Position enthalten");
var bestellung = Bestellung.Erstellen(anfrage);
await _repository.FügeHinzuAsync(bestellung, CancellationToken.None);
return Ergebnis<Bestellung>.Erfolg(bestellung);
}
Repository-Muster mit EF Core
public sealed class SqlBestellungRepository : IBestellungRepository
{
private readonly AppDbContext _db;
public SqlBestellungRepository(AppDbContext db) => _db = db;
public async Task<Bestellung?> FindePerIdAsync(Guid id, CancellationToken cancellationToken)
{
return await _db.Bestellungen
.Include(b => b.Positionen)
.AsNoTracking()
.FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
}
public async Task<IReadOnlyList<Bestellung>> FindePerKundeAsync(
string kundenId,
CancellationToken cancellationToken)
{
return await _db.Bestellungen
.Where(b => b.KundenId == kundenId)
.OrderByDescending(b => b.ErstelltAm)
.AsNoTracking()
.ToListAsync(cancellationToken);
}
public async Task FügeHinzuAsync(Bestellung bestellung, CancellationToken cancellationToken)
{
_db.Bestellungen.Add(bestellung);
await _db.SaveChangesAsync(cancellationToken);
}
}
Middleware und Pipeline
// Benutzerdefinierte Middleware
public sealed class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext kontext)
{
var stopwatch = Stopwatch.StartNew();
try
{
await _next(kontext);
}
finally
{
stopwatch.Stop();
_logger.LogInformation(
"Anfrage {Methode} {Pfad} abgeschlossen in {VerstricheneMs}ms mit Status {Statuscode}",
kontext.Request.Method,
kontext.Request.Path,
stopwatch.ElapsedMilliseconds,
kontext.Response.StatusCode);
}
}
}
Minimal-API-Muster
// Organisiert mit Routengruppen
var bestellungen = app.MapGroup("/api/bestellungen")
.RequireAuthorization()
.WithTags("Bestellungen");
bestellungen.MapGet("/{id:guid}", async (
Guid id,
IBestellungRepository repository,
CancellationToken cancellationToken) =>
{
var bestellung = await repository.FindePerIdAsync(id, cancellationToken);
return bestellung is not null
? TypedResults.Ok(bestellung)
: TypedResults.NotFound();
});
bestellungen.MapPost("/", async (
CreateOrderRequest anfrage,
IBestellungsService service,
CancellationToken cancellationToken) =>
{
var ergebnis = await service.PlatziereBestellungAsync(anfrage, cancellationToken);
return ergebnis.IstErfolg
? TypedResults.Created($"/api/bestellungen/{ergebnis.Wert!.Id}", ergebnis.Wert)
: TypedResults.BadRequest(ergebnis.Fehler);
});
Guard-Klauseln
// Gut: Frühe Rückgaben mit klarer Validierung
public async Task<VerarbeitungsErgebnis> VerarbeiteZahlungAsync(
ZahlungsAnfrage anfrage,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(anfrage);
if (anfrage.Betrag <= 0)
throw new ArgumentOutOfRangeException(nameof(anfrage.Betrag), "Betrag muss positiv sein");
if (string.IsNullOrWhiteSpace(anfrage.Währung))
throw new ArgumentException("Währung ist erforderlich", nameof(anfrage.Währung));
// Der Happy Path wird hier ohne Verschachtelung fortgesetzt
var gateway = _gatewayFactory.Erstellen(anfrage.Währung);
return await gateway.BelastungDurchführenAsync(anfrage, cancellationToken);
}
Anti-Patterns, die vermieden werden sollten
| Anti-Pattern | Lösung |
|---|---|
async void-Methoden | Task zurückgeben (außer Ereignisbehandlern) |
.Result oder .Wait() | await verwenden |
catch (Exception) { } | Behandeln oder mit Kontext erneut werfen |
new Service() in Konstruktoren | Konstruktor-Injection verwenden |
public-Felder | Properties mit entsprechenden Zugriffsmodifikatoren verwenden |
dynamic in Geschäftslogik | Generics oder explizite Typen verwenden |
Veränderlicher static-Zustand | DI-Scoping oder ConcurrentDictionary verwenden |
string.Format in Schleifen | StringBuilder oder interpolierte String-Handler verwenden |