In der objektorientierten Programmierung, speziell bei der Verwendung von Entity Framework Core (EF Core), wird oft ein Modell benötigt, das die Beziehung zwischen einer .NET-Anwendung und einer relationalen Datenbank abbildet. Dieser Prozess umfasst mehrere Schritte, von denen jeder eine präzise Abbildung und eine korrekte Konfiguration der Datenstruktur erfordert. Eine wichtige Komponente dieses Prozesses ist das Definieren und Anpassen von Datenmodellen, die in der Anwendung verwendet werden, um mit der zugrundeliegenden Datenbank zu interagieren.

Im Allgemeinen orientiert sich EF Core an einer Reihe von Konventionen, die automatisch angewendet werden, um die Datenmodelle mit der Datenbank zu synchronisieren. Eine dieser grundlegenden Konventionen besagt, dass jede Eigenschaft eines Entitätstyps, die einen Wert vom Typ int oder Guid enthält, standardmäßig als Identitätsspalte in der Datenbank betrachtet wird, das heißt, sie wird automatisch mit einem Wert versehen, wenn ein neuer Datensatz eingefügt wird. Diese Konventionen können durch spezifische Attribute oder die Fluent API von EF Core weiter verfeinert und angepasst werden.

Ein einfaches Beispiel, um ein Datenmodell in EF Core zu definieren, ist die Verwendung von Annotationsattributen, die direkt in den Entitätsklassen angebracht werden. Diese Attribute geben dem Framework zusätzliche Informationen darüber, wie die Eigenschaften des Modells in der Datenbank dargestellt werden sollen. Zu den gängigsten Attributen gehören:

  • [Required] – stellt sicher, dass der Wert einer Eigenschaft nicht null ist.

  • [StringLength(50)] – definiert die maximale Länge eines Strings.

  • [RegularExpression(expression)] – erzwingt eine Übereinstimmung mit einem bestimmten regulären Ausdruck.

  • [Column(TypeName = "money", Name = "UnitPrice")] – legt den Datentyp und den Namen der Spalte in der Datenbank fest.

Diese Attribute ermöglichen eine detailliertere Kontrolle über die Modellierung der Daten und die Erstellung der entsprechenden Tabellen. So kann beispielsweise die Eigenschaft ProductName einer Produktklasse durch folgende Attribute definiert werden:

csharp
[Required] [StringLength(40)] public string ProductName { get; set; }

Ein weiteres Beispiel wäre die Verwendung von [Column(TypeName = "money")], um den Typ einer Eigenschaft wie UnitPrice anzupassen, da die Datenbank den Typ money verwendet, während .NET keinen entsprechenden Typ besitzt. Hier wird stattdessen der decimal-Typ verwendet:

csharp
[Column(TypeName = "money")] public decimal? UnitPrice { get; set; }

Ein Aspekt, den Entwickler berücksichtigen sollten, ist, dass, wenn Nullwertüberprüfungen in der Anwendung aktiviert sind, nicht jede Referenzeigenschaft mit dem [Required]-Attribut versehen werden muss, wenn der Typ bereits nicht-nullbar ist. In diesen Fällen wird das Nullwertverhalten automatisch durch C#-Nullbarkeitseinstellungen gesteuert, was die Notwendigkeit der Verwendung von Attributen reduziert.

Ein weiterer wichtiger Mechanismus in EF Core ist die Verwendung der Fluent API. Diese ermöglicht es, die Modellierung und Konfiguration der Entitäten durch Code in einer zentralen Methode zu definieren, ohne auf Attribute angewiesen zu sein. Dies führt zu einer saubereren und flexibleren Codebasis. Beispielsweise könnte die Konfiguration des ProductName-Attributs mit Fluent API so aussehen:

csharp
modelBuilder.Entity<Product>() .Property(product => product.ProductName) .IsRequired() .HasMaxLength(40);

Dies ist besonders nützlich, wenn eine größere Kontrolle über das Modell erforderlich ist oder wenn viele Eigenschaften in einer einzigen Methode konfiguriert werden sollen, anstatt einzelne Attribute in den Klassen zu setzen.

Ein weiteres nützliches Feature der Fluent API ist das sogenannte "Data Seeding". Dies erlaubt es, die Datenbank beim Erstellen einer neuen Instanz mit vordefinierten Datensätzen zu befüllen. So kann beispielsweise sichergestellt werden, dass die Tabelle für Produkte mit mindestens einem Datensatz initialisiert wird:

csharp
modelBuilder.Entity<Product>() .HasData(new Product { ProductId = 1, ProductName = "Chai", UnitPrice = 8.99M });

Die Verwendung von Data Seeding ist besonders hilfreich in Entwicklungs- und Testumgebungen, da es eine schnelle und zuverlässige Methode bietet, um eine Datenbank mit standardisierten Werten zu füllen.

Darüber hinaus muss der DbContext, eine zentrale Klasse in EF Core, die Kommunikation mit der Datenbank ermöglichen. Diese Klasse erbt von DbContext und stellt sicher, dass SQL-Abfragen dynamisch generiert werden, um mit der Datenbank zu interagieren. Der DbContext benötigt mindestens eine DbSet-Eigenschaft, die eine Tabelle in der Datenbank repräsentiert. Jede DbSet-Eigenschaft ist dabei ein generisches Objekt, das den Entitätstyp enthält und die Datenzeilen der entsprechenden Tabelle abbildet.

In der Praxis könnte ein einfaches Beispiel für den DbContext folgendermaßen aussehen:

csharp
public class NorthwindContext : DbContext { public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("your_connection_string"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>() .Property(p => p.ProductName) .IsRequired() .HasMaxLength(40); } }

Es gibt noch viele weitere Feinheiten und Anpassungen, die beim Umgang mit EF Core berücksichtigt werden müssen, aber es ist wichtig zu verstehen, dass sowohl Annotations als auch die Fluent API ergänzende Werkzeuge sind, die zusammen verwendet werden können, um eine präzise und flexible Datenmodellierung zu gewährleisten.

Endtext

Wie erstellt und verwaltet man Azure Cosmos DB Ressourcen programmatisch mit der Core (SQL) API?

Das Erstellen und Verwalten von Azure Cosmos DB-Ressourcen, wie Datenbanken und Containern, kann sowohl lokal über den Emulator als auch in der Cloud erfolgen. Entscheidend dabei ist die Nutzung der Klasse CosmosClient aus dem Namespace Microsoft.Azure.Cosmos. Um eine Datenbank namens "Northwind" und einen Container "Products" anzulegen, benötigt man als Grundvoraussetzung die URI des Endpunkts sowie den primären Schlüssel zur Authentifizierung. Im lokalen Emulator sind diese Werte standardisiert und für alle gleich, während sie in der Cloud für jedes Konto individuell sind.

Die Methode CreateDatabaseIfNotExistsAsync ermöglicht die idempotente Erstellung einer Datenbank mit definierter Durchsatzkapazität in Request Units (RU/s). Das Resultat liefert nicht nur Informationen über den Status der Operation, etwa ob die Datenbank bereits existierte oder neu erstellt wurde, sondern auch die Datenbank-ID. Beim Anlegen eines Containers wird neben dem Namen zwingend ein Partitionierungsschlüssel definiert, hier beispielsweise /productId. Dieser Schlüssel ist essenziell für die effiziente Verteilung und Abfrage der Daten im Cosmos DB-System. Zudem lässt sich eine Indexierungsstrategie bestimmen, die standardmäßig auf konsistenter Indizierung basiert und alle Pfade inkludiert, sofern keine explizite Ausnahme definiert ist.

Die Indexierungsrichtlinie beeinflusst maßgeblich die Performance bei Abfragen, da sie festlegt, welche Daten indiziert werden und wie aktuell diese Indizes sind. Die konsistente Indizierung garantiert, dass alle Änderungen sofort in den Indizes reflektiert werden, was insbesondere für Echtzeitanwendungen relevant ist. Über die API erhält man Zugriff auf diese Metadaten wie den zuletzt geänderten Zeitstempel des Containers oder die aktuell konfigurierte Indexierungsstrategie.

Bei der Programmierung ist es wichtig, Fehler robust abzufangen, zum Beispiel indem man überprüft, ob der Emulator aktiv ist oder ob Netzwerkprobleme vorliegen. Die Rückgabe der HTTP-Statuscodes wie 200 (OK) oder 201 (Created) erlaubt eine differenzierte Behandlung von bestehenden versus neu erstellten Ressourcen.

Für die tägliche Arbeit mit Daten in Cosmos DB stellt die Core (SQL) API eine intuitive Schnittstelle zur Verfügung. CRUD-Operationen (Create, Read, Update, Delete) werden über Methoden der Container-Klasse ausgeführt, wobei jede Methode das Item selbst sowie Metadaten wie den verbrauchten Request Charge (RU) und den HTTP-Statuscode zurückliefert. So lassen sich einzelne Dokumente gezielt lesen oder schreiben, mehrere Items gleichzeitig abrufen, oder auch differenzierte Änderungen via Patch-Operationen vornehmen, ohne das gesamte Dokument neu zu speichern.

Wesentlich für die effiziente Nutzung der Core (SQL) API ist das Verständnis der Partitionierung. Die Partitionierungsschlüssel ermöglichen nicht nur Skalierung, sondern auch konsistente Leistung bei Zugriffen. Falsche oder fehlende Partitionierung kann zu Hotspots und Performance-Einbrüchen führen. Ebenso sollte bedacht werden, dass jede Operation Request Units kostet, deren Verbrauch je nach Komplexität und Umfang der Operation variiert. Daher ist es ratsam, die RU-Kosten im Auge zu behalten und bei Bedarf die Durchsatzkapazität anzupassen.

Weiterhin ist zu beachten, dass die Daten in Cosmos DB als JSON-Dokumente verwaltet werden, was Flexibilität bei der Modellierung von Datenstrukturen erlaubt, aber auch spezielle Überlegungen bei der Abfrage und Indexierung erfordert. Die Indexierung von verschachtelten Objekten oder Arrays kann beispielsweise die Query-Performance erheblich verbessern, wenn sie passend konfiguriert wird.

Das Wissen um diese Grundlagen ermöglicht es, Cosmos DB nicht nur als einfachen NoSQL-Speicher zu nutzen, sondern als skalierbare, performante Datenbanklösung mit differenzierten Möglichkeiten der Datenverwaltung und Abfrageoptimierung. Die Fähigkeit, Ressourcen programmgesteuert zu erstellen und zu verwalten, bildet die Basis für die Automatisierung und Integration in moderne DevOps-Prozesse.

Wie man AutoMapper und FluentAssertions für Unit-Tests effektiv einsetzt

Die Verwendung von AutoMapper zur Abbildung von Datenmodellen ist eine bewährte Methode, um Daten effizient zwischen verschiedenen Schichten einer Anwendung zu übertragen. In vielen Fällen, wie beispielsweise bei der Konvertierung eines Einkaufswagens in ein Zusammenfassungsmodell, müssen Entwickler sicherstellen, dass diese Abbildungen korrekt konfiguriert sind. Dabei kann es jedoch zu Fehlern kommen, insbesondere wenn Felder vergessen oder fehlerhaft abgebildet werden. Ein solches Szenario tritt häufig auf, wenn ein Test fehlschlägt, weil ein Feld, wie etwa das Total-Feld in einem Zusammenfassungsmodell, nicht korrekt gemappt wurde.

Ein solches Problem tritt oft auf, wenn die Mapping-Konfiguration nicht alle erforderlichen Felder abdeckt. Ein Beispiel zeigt, dass beim Testen der Mapping-Konfiguration die Fehlermeldung erscheint, dass das Total-Feld im Zielmodell nicht abgebildet ist. Um dies zu beheben, wird das Total-Feld zur Mapper-Konfiguration hinzugefügt, um sicherzustellen, dass die Summe aller Artikel im Warenkorb korrekt berechnet wird. Dies könnte folgendermaßen aussehen:

csharp
MapperConfiguration config = new(cfg => { cfg.Internal().MethodMappingEnabled = false; // Fix für .NET 7 cfg.CreateMap<Cart, Summary>() .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.Customer.FirstName} {src.Customer.LastName}")) .ForMember(dest => dest.Total, opt => opt.MapFrom(src => src.Items.Sum(item => item.UnitPrice * item.Quantity))); });

Mit dieser Änderung wird nun auch das Total korrekt aus den einzelnen Artikelpreisen und deren Mengen berechnet. Der nächste Schritt ist, den Test erneut auszuführen. Dies führt zu einem erfolgreichen Ergebnis, wenn die Konfiguration jetzt korrekt ist und alle Felder abgebildet werden.

Sobald das Mapping bestätigt wurde, kann die Abbildung in einer realen Anwendung eingesetzt werden. Dies umfasst die Erstellung eines Beispiels eines Kundenmodells mit einem Warenkorb, der mehrere Artikel enthält. Dieses Modell wird dann auf ein ViewModel abgebildet, das für die Anzeige im Frontend verwendet wird. Ein typisches Beispiel für eine solche Anwendung könnte wie folgt aussehen:

csharp
Cart cart = new(Customer: new(FirstName: "John", LastName: "Smith"), Items: new() { new(ProductName: "Äpfel", UnitPrice: 0.49M, Quantity: 10), new(ProductName: "Bananen", UnitPrice: 0.99M, Quantity: 4) }); Console.WriteLine($"{cart.Customer}"); foreach (var item in cart.Items) { Console.WriteLine($" {item}"); } MapperConfiguration config = CartToSummaryMapper.GetMapperConfiguration(); IMapper mapper = config.CreateMapper(); Summary summary = mapper.Map<Summary>(cart); Console.WriteLine($"Zusammenfassung: {summary.FullName} hat {summary.Total} ausgegeben.");

Das resultierende Konsolenergebnis zeigt dann die Kundeninformationen und eine prägnante Zusammenfassung der Gesamtausgaben:

nginx
Kunde { Vorname = John, Nachname = Smith } Artikel { Produktname = Äpfel, Preis pro Einheit = 0.49, Menge = 10 } Artikel { Produktname = Bananen, Preis pro Einheit = 0.99, Menge = 4 } Zusammenfassung: John Smith hat 8.86 ausgegeben.

Dies verdeutlicht die Leistungsfähigkeit von AutoMapper in der Praxis. Die korrekte Verwendung der Konfiguration gewährleistet, dass alle relevanten Daten abgebildet und korrekt zusammengefasst werden. Es ist jedoch wichtig zu beachten, dass bei der Verwendung von AutoMapper auch häufige Fallstricke auftreten können. Eine der wichtigsten Herausforderungen ist, dass Änderungen im Datenmodell, wie das Hinzufügen neuer Felder, häufig auch eine Anpassung der Mapping-Konfiguration erfordern. Daher sollte jede Änderung in den Modellen sofort auf die Mapper-Konfiguration überprüft werden.

Ein weiteres nützliches Werkzeug, das häufig zusammen mit AutoMapper verwendet wird, ist FluentAssertions. Diese Bibliothek hilft dabei, Unit-Tests lesbarer zu gestalten, indem sie eine flüssige, englischähnliche Syntax für Assertions bietet. Dies erleichtert nicht nur das Schreiben von Tests, sondern macht auch Fehlermeldungen klarer. Zum Beispiel könnte man mit FluentAssertions eine einfache Zeichenketten-Assertion wie folgt durchführen:

csharp
string city = "London"; city.Should().StartWith("Lo") .And.EndWith("on") .And.Contain("do") .And.HaveLength(6);

Dies zeigt, wie durch einfache und ausdrucksstarke Tests die Lesbarkeit und Verständlichkeit von Code erheblich verbessert wird. FluentAssertions kann nicht nur für einfache Datentypen wie Strings verwendet werden, sondern auch für komplexe Datenstrukturen wie Arrays und Sammlungen. Ein Beispiel für die Überprüfung von Arrays könnte so aussehen:

csharp
string[] names = new[] { "Alice", "Bob", "Charlie" }; names.Should().HaveCountLessThan(4, "Weil die maximale Anzahl von Elementen 3 oder weniger betragen sollte."); names.Should().OnlyContain(name => name.Length <= 6);

Dieses Beispiel zeigt, wie einfach es ist, zu überprüfen, ob die Elemente einer Sammlung bestimmten Bedingungen entsprechen. Wenn die Tests fehlschlagen, liefert FluentAssertions eine verständliche Fehlermeldung, die die Ursache des Problems aufzeigt, sodass Entwickler schnell reagieren können.

Bei der Arbeit mit Daten und Modellen in realen Anwendungen ist es entscheidend, dass nicht nur die Konfigurationen und Mappings korrekt sind, sondern auch, dass die Tests so gestaltet sind, dass sie mögliche Fehler schnell und klar identifizieren. AutoMapper und FluentAssertions sind leistungsstarke Werkzeuge, die Entwicklern dabei helfen können, diese Ziele zu erreichen, indem sie die Arbeit mit Datenmodellen und Unit-Tests erheblich vereinfachen und optimieren.