Die Erstellung und Verwaltung eines Datenkontexts in Entity Framework Core (EF Core) stellt einen zentralen Schritt in der Arbeit mit relationalen Datenbanken dar. Dabei wird die Datenbankstruktur in Form von Entity-Klassen modelliert, die über eine DbContext-Klasse verwaltet werden. Ein praktisches Beispiel ist das Scaffolding von Entity-Klassen aus einer bestehenden SQL Server-Datenbank. Mit dem Kommandozeilenwerkzeug dotnet ef dbcontext scaffold lässt sich basierend auf einer Verbindungszeichenfolge ein vollständiges Modell generieren. Hierbei ist es möglich, Datenanmerkungen (--data-annotations) einzubinden, die sowohl Validierung als auch Mapping-Informationen enthalten und so die Datenintegrität auf Anwendungsebene unterstützen.

Das Ergebnis dieser automatisierten Modellgenerierung sind Klassen, die die Tabellen der Datenbank repräsentieren – im vorliegenden Fall 28 Klassen, die von AlphabeticalListOfProduct bis Territory reichen. Ein besonders wichtiger Aspekt bei der Definition von Primärschlüsseln ist die zusätzliche Validierung, wie das Beispiel des Customer-Entities zeigt. Dort wird durch eine Regular Expression sichergestellt, dass der Primärschlüssel nur aus fünf Großbuchstaben besteht. Solche Validierungen erhöhen die Robustheit und verhindern unerwartete Fehler durch ungültige Daten.

Eine bewährte Praxis besteht darin, den Datenkontext in eine separate Klassenbibliothek auszulagern. Dies fördert die Modularität und erleichtert die Wiederverwendung in verschiedenen Projekten. Dazu wird ein neues Projekt als „Class Library“ angelegt und die automatisch generierten Entity-Modelle sowie die Kontextklasse hierher verschoben. Wichtig ist, alle Abhängigkeiten, insbesondere auf EF Core SQL Server Provider und EntityModels, korrekt zu referenzieren und Compiler-Warnungen konsequent als Fehler zu behandeln, um Qualität sicherzustellen.

Die Erweiterung des Datenkontexts durch sogenannte Extension Methods ist eine elegante Lösung, um den Kontext in die Abhängigkeitsinjektion einzubinden. So kann z.B. eine Methode AddNorthwindContext implementiert werden, die den Kontext mit einer konfigurierbaren Verbindungszeichenfolge zum DI-Container hinzufügt. Diese Flexibilität ermöglicht es, etwa Verbindungseinstellungen dynamisch aus Konfigurationsdateien zu laden und vereinfacht die Anpassung an unterschiedliche Umgebungen.

Mit der Einführung von EF Core 7 wurde das Interface IMaterializationInterceptor bereitgestellt, das eine feingranulare Steuerung bei der Materialisierung von Entities ermöglicht. Dies erlaubt die automatische Initialisierung von berechneten Eigenschaften, die zur Laufzeit generiert werden sollen, wie z.B. ein Zeitstempel LastRefreshed, der dokumentiert, wann eine Entity zuletzt aus der Datenbank geladen wurde. Diese Funktionalität kann durch ein selbst definiertes Interface ergänzt werden, das von Entities implementiert wird, sowie durch eine Implementierung des Interceptors, die beim Instanziieren einer Entity diesen Wert setzt. Die Registrierung dieses Interceptors erfolgt im DbContext und stellt sicher, dass alle betroffenen Entities konsistent und automatisch aktualisiert werden.

Zusätzlich zu den beschriebenen Schritten ist es von Bedeutung, den Umgang mit Verbindungssicherheit und Zertifikatsvalidierung zu beachten, vor allem im produktiven Umfeld. Das Beispiel verwendet TrustServerCertificate=True für eine unkomplizierte Verbindung, was in Entwicklungsumgebungen akzeptabel sein kann, in produktiven Systemen jedoch durch sichere Zertifikatsverwaltung ersetzt werden sollte. Ebenso ist die Behandlung von Mehrfach-Resultsets und Verschlüsselungseinstellungen im Connection-String entscheidend für Performance und Sicherheit.

Insgesamt demonstriert die beschriebene Vorgehensweise einen systematischen und modularen Ansatz für die Integration von EF Core mit SQL Server, der sowohl die Entwicklungseffizienz als auch die Wartbarkeit der Anwendung erhöht. Das Verstehen dieser Konzepte ist essenziell, um moderne, skalierbare und sichere Datenzugriffsschichten zu gestalten.

Wie man beliebte Drittanbieter-Bibliotheken wie Serilog und AutoMapper in C# integriert

Serilog ist eine weit verbreitete Logging-Bibliothek in C#, die es Entwicklern ermöglicht, detaillierte und strukturierte Logs zu erstellen. Mit Serilog können Entwickler ihre Anwendungen so konfigurieren, dass sie verschiedene Log-Levels verwenden, um Informationen zu protokollieren, Fehler zu erkennen und auf kritische Probleme hinzuweisen. Ein typisches Beispiel für die Konfiguration von Serilog in einer Anwendung umfasst das Erstellen eines Loggers und das Protokollieren von Informationen wie folgt:

csharp
.WriteTo.File("log.txt", rollingInterval: RollingInterval.Day)
.CreateLogger(); Log.Logger = log; Log.Information("Der globale Logger wurde konfiguriert."); Log.Warning("Achtung, Serilog, Gefahr!"); Log.Error("Dies ist ein Fehler!"); Log.Fatal("Fataler Fehler!");

In diesem Beispiel werden verschiedene Log-Level verwendet, um Nachrichten in eine Datei zu schreiben, die täglich rotiert wird. Der Log-Level „Information“ wird verwendet, um allgemeine Nachrichten zu protokollieren, „Warning“ für Warnungen, „Error“ für Fehler und „Fatal“ für kritische Probleme. Solche Log-Nachrichten sind besonders hilfreich, um das Verhalten einer Anwendung zu überwachen und potenzielle Fehlerquellen frühzeitig zu identifizieren.

Ein weiterer wichtiger Punkt bei der Verwendung von Serilog ist die Fähigkeit, zusätzliche Informationen zusammen mit den Log-Nachrichten zu protokollieren. In folgendem Beispiel wird eine Produktseitenansicht protokolliert, einschließlich des Produkttitels und des Abschnitts der Seite:

csharp
ProductPageView pageView = new() {
PageTitle = "Chai", SiteSection = "Beverages", ProductId = 1 }; Log.Information("{@PageView} occurred at {Viewed}", pageView, DateTimeOffset.UtcNow);

Diese Art von detailliertem Logging hilft dabei, das Benutzerverhalten zu analysieren und die Leistung der Anwendung zu überwachen. Nach dem Beenden der Anwendung kann der Logger mit der Methode Log.CloseAndFlush() geschlossen und die Daten gespeichert werden.

Ein weiterer wichtiger Aspekt der Softwareentwicklung ist das Mapping von Objekten. Bei der Arbeit mit verschiedenen Schichten einer Anwendung müssen oft Datenmodelle zwischen verschiedenen Strukturen konvertiert werden. Die Integration von Drittanbieter-Bibliotheken wie AutoMapper erleichtert diesen Prozess erheblich, indem sie die Konfiguration und den Umgang mit verschiedenen Modellen vereinfacht.

AutoMapper ist eine weit verbreitete Bibliothek, die das Mapping zwischen Objekten auf einfache Weise ermöglicht. Dabei werden Modelle, die aus verschiedenen Schichten einer Anwendung stammen – wie Entitätsmodelle, Datenübertragungsobjekte (DTO) oder ViewModels – auf einfache Weise miteinander verbunden. Ein Beispiel für den Einsatz von AutoMapper ist das Mapping von Kundeninformationen und deren Warenkorb auf ein zusammengefasstes ViewModel, das an den Benutzer angezeigt wird:

csharp
public class Customer {
public string FirstName { get; set; } public string LastName { get; set; } } public class LineItem {
public string ProductName { get; set; }
public decimal UnitPrice { get; set; } public int Quantity { get; set; } } public class Cart { public Customer Customer { get; set; } public List<LineItem> Items { get; set; } } public class Summary { public string FullName { get; set; } public decimal Total { get; set; } }

Mit AutoMapper können Entwickler automatisch Felder aus einem Quellobjekt auf die entsprechenden Felder eines Zielobjekts mappen, ohne manuell jedes einzelne Feld zu kopieren. Dies spart nicht nur Zeit, sondern verhindert auch Fehler, die bei der manuellen Zuweisung auftreten könnten. Im Fall des oben gezeigten Beispiels könnte AutoMapper so konfiguriert werden, dass der vollständige Name eines Kunden aus den Feldern FirstName und LastName zusammengeführt wird und in das ViewModel Summary übernommen wird:

csharp
public static class CartToSummaryMapper {
public static MapperConfiguration GetMapperConfiguration() { return new MapperConfiguration(cfg => { cfg.CreateMap<Cart, Summary>() .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.Customer.FirstName} {src.Customer.LastName}") ); }); } }

Die Konfiguration von AutoMapper ermöglicht es, komplexe Datenstrukturen einfach zu transformieren, und erleichtert so die Implementierung von Logik, die zwischen verschiedenen Schichten einer Anwendung interagiert.

Wichtig ist es, bei der Verwendung von AutoMapper die Konfiguration gründlich zu testen. Bevor man mit dem eigentlichen Mapping arbeitet, sollte immer sichergestellt werden, dass alle Konfigurationen korrekt sind. Dies lässt sich durch Unit-Tests erreichen, in denen die Mapper-Konfiguration validiert wird:

csharp
[Fact]
public void Mapping_Configuration_IsValid() { var mapperConfig = CartToSummaryMapper.GetMapperConfiguration(); mapperConfig.AssertConfigurationIsValid(); }

Zusätzlich zur Verwendung von Serilog und AutoMapper ist es für Entwickler entscheidend, immer die neuesten Versionen dieser Bibliotheken zu verwenden und regelmäßig auf Updates zu überprüfen. In vielen Fällen verbessern neue Versionen nicht nur die Funktionalität, sondern auch die Leistung und Sicherheit der Bibliotheken. Es empfiehlt sich, regelmäßig die Dokumentation der Bibliotheken zu konsultieren, um bestmögliche Praktiken zu verstehen und die Implementierungen auf dem neuesten Stand zu halten.

In modernen Anwendungen, die aus mehreren Schichten bestehen, wird die Integration solcher Bibliotheken unerlässlich, um eine saubere Trennung der Verantwortlichkeiten und eine klare Strukturierung der Daten zu gewährleisten. Serilog stellt sicher, dass alle wichtigen Ereignisse und Fehler effizient protokolliert werden, während AutoMapper die Datenübertragung zwischen den Schichten vereinfacht. Diese beiden Tools zusammen ermöglichen es Entwicklern, Anwendungen effizient und wartbar zu gestalten.

Wie man Benutzerdaten mit Salt und Hash schützt und Daten signiert

In modernen Softwarearchitekturen ist die sichere Speicherung von Benutzerdaten, insbesondere von Passwörtern, von größter Bedeutung. Ein üblicher Mechanismus zur Sicherstellung der Datensicherheit ist die Verwendung von Salt und Hashing. Diese Technik schützt die Passwörter der Benutzer vor der Gefahr, dass diese im Falle eines Datenlecks einfach zu entschlüsseln sind. Der folgende Abschnitt beschreibt, wie man Passwörter mit Salt und Hash absichert und zusätzlich eine Methode zur Signierung von Daten mit RSA und SHA256 integriert.

Im ersten Schritt wird eine neue Klasse User.cs im Projekt CryptographyLib erstellt, die die grundlegenden Benutzerdaten enthält. Die Klasse definiert ein record-Objekt mit den Eigenschaften Name, Salt und SaltedHashedPassword, die für die Speicherung des Benutzernamens, eines zufällig generierten Salt-Werts und des salted und gehashten Passworts verantwortlich sind.

csharp
namespace Packt.Shared; public record class User(string Name, string Salt, string SaltedHashedPassword);

Im zweiten Schritt müssen wir in der Klasse Protector.cs zwei wichtige Methoden hinzufügen. Eine Methode dient der Registrierung eines neuen Benutzers, die andere zur Validierung des Passworts, wenn sich der Benutzer einloggt. Beim Registrieren eines neuen Benutzers wird zuerst ein zufälliges Salt erzeugt, das mit dem Passwort kombiniert und anschließend gehasht wird. Der Benutzer wird mit seinem Namen, dem Salt und dem gehashten Passwort in einem Dictionary gespeichert.

csharp
private static Dictionary<string, User> Users = new();
public static User Register(string username, string password) { RandomNumberGenerator rng = RandomNumberGenerator.Create(); byte[] saltBytes = new byte[16]; rng.GetBytes(saltBytes); string saltText = ToBase64String(saltBytes); string saltedhashedPassword = SaltAndHashPassword(password, saltText); User user = new(username, saltText, saltedhashedPassword); Users.Add(user.Name, user); return user; } public static bool CheckPassword(string username, string password) { if (!Users.ContainsKey(username)) return false; User u = Users[username]; return CheckPassword(password, u.Salt, u.SaltedHashedPassword); } private static bool CheckPassword(string password, string salt, string hashedPassword) { string saltedhashedPassword = SaltAndHashPassword(password, salt); return (saltedhashedPassword == hashedPassword); } private static string SaltAndHashPassword(string password, string salt) { using (SHA256 sha = SHA256.Create()) { string saltedPassword = password + salt; return ToBase64String(sha.ComputeHash(Encoding.Unicode.GetBytes(saltedPassword))); } }

Die Funktionalität wird durch das Erstellen eines neuen Konsolenprojekts namens HashingApp weiter ausgebaut, das es dem Benutzer ermöglicht, einen neuen Benutzer zu registrieren und ein Passwort zu überprüfen. Das Projekt ermöglicht es, die Funktionsweise der Passwortregistrierung und -validierung live zu erleben.

csharp
WriteLine("Registering Alice with Pa$$w0rd:"); User alice = Protector.Register("Alice", "Pa$$w0rd"); WriteLine($" Name: {alice.Name}"); WriteLine($" Salt: {alice.Salt}"); WriteLine(" Password (salted and hashed): {0}", alice.SaltedHashedPassword); WriteLine(); Write("Enter a new user to register: "); string? username = ReadLine(); if (string.IsNullOrEmpty(username)) username = "Bob"; Write($"Enter a password for {username}: "); string? password = ReadLine(); if (string.IsNullOrEmpty(password)) password = "Pa$$w0rd"; WriteLine("Registering a new user:"); User newUser = Protector.Register(username, password); WriteLine($" Name: {newUser.Name}"); WriteLine($" Salt: {newUser.Salt}"); WriteLine(" Password (salted and hashed): {0}", newUser.SaltedHashedPassword); WriteLine(); bool correctPassword = false; while (!correctPassword) { Write("Enter a username to log in: "); string? loginUsername = ReadLine(); if (string.IsNullOrEmpty(loginUsername)) { WriteLine("Login username cannot be empty."); Write("Press Ctrl+C to end or press ENTER to retry."); ReadLine(); continue; } Write("Enter a password to log in: "); string? loginPassword = ReadLine(); if (string.IsNullOrEmpty(loginPassword)) { WriteLine("Login password cannot be empty."); Write("Press Ctrl+C to end or press ENTER to retry."); ReadLine(); continue; } correctPassword = Protector.CheckPassword(loginUsername, loginPassword); if (correctPassword) { WriteLine($"Correct! {loginUsername} has been logged in."); } else { WriteLine("Invalid username or password. Try again."); } }

Durch die Verwendung eines zufällig generierten Salts wird sichergestellt, dass selbst wenn zwei Benutzer dasselbe Passwort haben, ihre gehashten Passwörter aufgrund der unterschiedlichen Salts unterschiedlich sind. Dies schützt vor Angriffen wie Rainbow-Table-Angriffen.

Ein weiterer wichtiger Aspekt der Datensicherheit ist die Möglichkeit, Daten zu signieren. Dies dient der Verifizierung der Authentizität von Daten. Statt die Daten selbst zu signieren, wird ein Hash der Daten signiert, da die meisten Signaturalgorithmen zunächst einen Hash der Daten erzeugen, bevor sie diese signieren. Wir verwenden in diesem Beispiel den SHA256-Algorithmus zum Erzeugen des Hashs und RSA zur Signierung dieses Hashes. Diese Kombination wird häufig bevorzugt, da RSA für die Signaturvalidierung schneller ist als die Signaturerstellung.

csharp
public static string? PublicKey;
public static string GenerateSignature(string data) { byte[] dataBytes = Encoding.Unicode.GetBytes(data); SHA256 sha = SHA256.Create(); byte[] hashedData = sha.ComputeHash(dataBytes); RSA rsa = RSA.Create(); PublicKey = rsa.ToXmlString(false); // exclude private key return ToBase64String(rsa.SignHash(hashedData, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); } public static bool ValidateSignature(string data, string signature) {
if (PublicKey is null) return false;
byte[] dataBytes = Encoding.Unicode.GetBytes(data); SHA256 sha = SHA256.Create(); byte[] hashedData = sha.ComputeHash(dataBytes); byte[] signatureBytes = FromBase64String(signature); RSA rsa = RSA.Create(); rsa.FromXmlString(PublicKey); return rsa.VerifyHash(hashedData, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }

Wichtig ist, dass nur der öffentliche Teil des Schlüssels für die Verifizierung der Signatur zur Verfügung gestellt wird. Der private Schlüssel bleibt geheim und wird nicht an Dritte weitergegeben.