In modernen Versionen von C# wurde der Zugriff auf Elemente innerhalb von Arrays und anderen indexbasierten Datenstrukturen durch die Einführung der System.Index- und System.Range-Strukturen signifikant erweitert. Während traditionell einfache Ganzzahlen als Index verwendet wurden, erlaubt System.Index eine formale und erweiterte Definition einer Position – einschließlich der Fähigkeit, rückwärts vom Ende einer Sequenz zu zählen. Dies geschieht durch den Konstruktor von Index, der optional den Parameter fromEnd verwendet, oder durch Verwendung des Zirkumflexoperators ^, welcher die Richtung visuell signalisiert.

Beispielsweise adressiert Index i1 = 3; das vierte Element in einem Array, wohingegen Index i2 = ^5; das fünfte Element vom Ende markiert. Beide syntaktischen Varianten – explizite Konstruktoraufrufe und implizite Konvertierungen – existieren nebeneinander, um sowohl Klarheit als auch Kürze im Quelltext zu ermöglichen.

Noch mächtiger wird dieses Konzept durch System.Range, das zwei Index-Werte kapselt – einen Start- und einen Endpunkt. Diese Struktur erlaubt das sogenannte Slicing: ein Extrahieren von Teilbereichen einer Sequenz. Range r1 = 3..7; extrahiert zum Beispiel alle Elemente vom vierten bis zum siebten (exklusive). Auch hier sind mehrere Ausdrucksformen erlaubt, einschließlich der Konstruktorform new Range(...) sowie der vereinfachten Syntax start..end.

Eine der nützlichsten Eigenschaften von System.Range ist die Fähigkeit zur offenen Begrenzung. Range r4 = 3..; beginnt bei Index 3 und reicht bis zum Ende, wohingegen Range r7 = ..3; vom Anfang bis zum dritten Index schneidet. Diese Ausdruckskraft erhöht die Lesbarkeit und Wartbarkeit von Code erheblich, besonders bei der Arbeit mit Subsequenzen oder beim Trimmen von Daten.

Mit C# 9 und .NET 5 wurden darüber hinaus neue Sprachmerkmale eingeführt, die eng mit modernen API-Designs und funktionalem Programmieren verwandt sind. Besonders hervorzuheben sind hier die sogenannten Records – unveränderliche Objekttypen, die sich besonders für Datenmodelle eignen. Durch die Verwendung des init-Accessors wird eine Property lediglich während der Initialisierung beschreibbar, danach jedoch schreibgeschützt. Dies ist ein entscheidender Schritt hin zu funktionaler Unveränderlichkeit und damit einhergehender Nebenwirkungsfreiheit.

Records lassen sich zusätzlich in einer kompakten, positionalen Syntax definieren, die Konstruktor, Properties und Dekonstruktor automatisch generiert: public record ImmutableAnimal(string Name, string Species);. Solche Strukturen bieten nicht nur syntaktische Eleganz, sondern auch semantische Klarheit – etwa in domänenspezifischen Modellen oder bei der Verwendung mit Pattern Matching.

Ein weiteres prägnantes Merkmal ist die Einführung von sogenannten Top-Level Statements. Vor C# 9 war es zwingend notwendig, jede Anwendung mit einer expliziten Main-Methode in einer Program-Klasse zu starten. Die neue Top-Level-Syntax erlaubt es, diesen Boilerplate-Code vollständig zu eliminieren: Ein einfaches Console.WriteLine("Hello World!"); genügt als gültiges Startprogramm. Dies verbessert nicht nur die Lesbarkeit, sondern senkt auch die Einstiegshürde für neue Entwickler, ohne auf Funktionalität zu verzichten. Dabei gilt, dass nur eine solche Datei pro Projekt existieren darf, und eventuelle Klassendeklarationen erst am Ende stehen dürfen.

In C# 9 wurde zudem die sogenannte target-typed new-Syntax eingeführt, die es ermöglicht, Objekte zu instanziieren, ohne den Typ mehrfach anzugeben. Statt XmlDocument xmlDoc = new XmlDocument(); genügt XmlDocument xmlDoc = new();. Auch hier liegt die Stärke im Ausdruck sparsamen, aber dennoch präzisen Codes.

Mit C# 10 und .NET 6 folgte eine weitere Vereinfachung der Projektstruktur: Standardmäßige Aktivierung von Nullüberprüfungen und implizit global importierte Namespaces. War es bisher erforderlich, jede benötigte Namespace manuell per using-Direktive zu importieren, so geschieht dies nun automatisch durch das SDK. Durch die Verwendung von global using können häufig genutzte Namespaces wie System oder System.Linq an zentraler Stelle definiert und projektweit verfügbar gemacht werden.

Dies reduziert die Redundanz im Code und verschlankt insbesondere größere Projekte, etwa Webanwendungen mit ASP.NET Core, bei denen traditionell zahlreiche Imports notwendig waren. Eine empfohlene Praxis ist es, alle globalen using-Anweisungen in einer separaten Datei – beispielsweise GlobalUsings.cs – zu bündeln.

Wichtig ist zu verstehen, dass sich diese sprachlichen Neuerungen nicht nur als syntaktischer Zucker verstehen lassen, sondern eine tiefergehende strukturelle Veränderung in der Art und Weise darstellen, wie moderne C#-Anwendungen konzipiert und geschrieben werden. Sie fördern funktionale Programmierparadigmen, klare Trennung von Zuständigkeiten, verbesserte Lesbarkeit und Wartbarkeit und nicht zuletzt eine erheblich vereinfachte Projektstruktur.

Auch wenn viele dieser Features optional erscheinen, offenbaren sie ihren Wert insbesondere in der Kombination: So lassen sich mit Records, init-Properties, System.Range-Slicing und globalen using-Direktiven leistungsfähige, moderne Anwendungen schreiben, die robust, effizient und elegant zugleich sind. Wer sich mit diesen Neuerungen vertraut macht, erschließt sich nicht nur syntaktische Vorteile, sondern ein tiefgreifenderes Verständnis für die Designprinzipien hinter der .NET-Plattform selbst.

Wie man Rate Limiting und JWT-Authentifizierung in einer Web-API implementiert

Die Implementierung von Rate Limiting und JWT-Authentifizierung ist ein wesentlicher Bestandteil der Sicherheit und Skalierbarkeit moderner Web-APIs. Diese Funktionen tragen dazu bei, die Leistung zu optimieren und sicherzustellen, dass nur autorisierte Benutzer auf sensible Ressourcen zugreifen können. In diesem Kapitel werden zwei grundlegende Techniken vorgestellt: die Verwendung von Rate Limiting zur Begrenzung der API-Anfragen und die Nutzung von JWTs zur Authentifizierung von Benutzern.

Der erste Schritt besteht darin, die Microsoft-Bibliotheken für Rate Limiting zu integrieren, was durch das Hinzufügen der entsprechenden Namensräume im Program.cs-Code erfolgt. Mit der Anweisung using Microsoft.AspNetCore.RateLimiting; sowie using System.Threading.RateLimiting; können wir auf die Funktionen zugreifen, die zur Implementierung von Rate Limiting erforderlich sind. Um sicherzustellen, dass die Rate-Limiting-Funktion aktiv ist, wird eine boolesche Variable namens useMicrosoftRateLimiting auf true gesetzt. Diese steuert, ob die eingebaute Microsoft-Rate-Limiting-Funktion verwendet wird.

Für das Rate Limiting gibt es eine einfache Konfiguration, die in Program.cs hinzugefügt wird. Ein Beispiel für eine Rate-Limiting-Policy ist die fixed5per10seconds, die eine Begrenzung von fünf Anfragen alle zehn Sekunden vorsieht. Dies bedeutet, dass ein Client innerhalb dieses Zeitraums nur fünf Anfragen an die API stellen darf, bevor er für die verbleibende Zeit des Fensters blockiert wird. Um dies zu implementieren, wird ein RateLimiter mit einer Konfiguration von fünf erlaubten Anfragen und einer Fenstergröße von zehn Sekunden definiert. Zudem wird eine Warteschlangenbearbeitung mit der Option QueueProcessingOrder.OldestFirst eingerichtet, um sicherzustellen, dass die ältesten Anfragen zuerst bearbeitet werden, wenn das Limit überschritten wird.

Ein weiteres zentrales Element der API-Sicherheit ist die Authentifizierung mittels JWT (JSON Web Token). JWT ermöglicht es, Benutzer sicher zu authentifizieren, indem ein Token verwendet wird, das bei jeder Anfrage gesendet wird. JWTs bestehen aus drei Teilen: dem Header, der Nutzlast und der Signatur. Diese Teile werden in Base64 kodiert und sorgen dafür, dass das Token unverändert und vertrauenswürdig ist.

In einer typischen Implementierung von JWT-Authentifizierung für eine Web-API wird das Token beim Login eines Benutzers erstellt und dann bei jeder Anfrage zur Authentifizierung verwendet. Im Code wird dies durch die Integration der JWT-Authentifizierung in Program.cs erreicht. Hierbei wird der Dienst für die JWT-Authentifizierung registriert und die Middleware für die Autorisierung aktiviert.

Ein praktisches Beispiel zeigt, wie die JWT-Authentifizierung in einer ASP.NET Core API konfiguriert wird. Nachdem die Referenz zum JWT-Bearer-Paket hinzugefügt wurde, erfolgt die Registrierung des Authentifizierungsdienstes und der Autorisierungsdienste in Program.cs. Dies stellt sicher, dass jede eingehende Anfrage ein gültiges JWT im Header enthalten muss, um Zugang zu den geschützten Ressourcen zu erhalten.

Im lokalen Entwicklungsumfeld können Entwickler den Befehl dotnet user-jwts verwenden, um JWTs zu generieren und zu verwalten. Diese Tokens werden in einer JSON-Datei gespeichert und können für Tests verwendet werden, um sicherzustellen, dass die Authentifizierungsmechanismen korrekt funktionieren.

Um die Sicherheit der API zu gewährleisten, ist es jedoch nicht nur wichtig, Rate Limiting und JWT-Authentifizierung zu implementieren, sondern auch die zugrundeliegenden Identitätsdienste zu verstehen. Diese sind notwendig, um Benutzer zu authentifizieren und zu autorisieren. Microsoft empfiehlt die Verwendung von OpenID Connect und OAuth 2.0 als Standardprotokolle für diese Dienste. Ein weiterer Punkt, der nicht außer Acht gelassen werden sollte, ist, dass Microsoft nicht die Unterstützung von Drittanbieter-Authentifizierungsdiensten wie IdentityServer4 plant. Stattdessen wird Azure Active Directory als Lösung angeboten, die es ermöglicht, bis zu 500.000 Objekte kostenlos zu verwalten.

Neben der Rate-Limiting-Strategie und der JWT-Authentifizierung ist es wichtig, sich auch mit den besten Praktiken im Umgang mit Sicherheitstoken auseinanderzusetzen. JWTs sollten sicher aufbewahrt werden, da sie sensible Informationen enthalten können, die nicht unbefugt abgerufen oder manipuliert werden dürfen. Ebenso sollte beim Einsatz von Rate Limiting darauf geachtet werden, dass die Implementierung flexibel genug ist, um bei Bedarf angepasst oder erweitert zu werden, ohne die Benutzererfahrung zu beeinträchtigen.