Eine großartige Anleitung zum Erstellen von RESTful-APIs mit ASP.NET Core

Eine schrittweise Anleitung zur Implementierung sauberer, wartungsfähiger RESTful-APIs

Foto von Jefferson Santos, veröffentlicht bei Unsplash

Überblick

RESTful ist kein neuer Begriff. Es bezieht sich auf einen Architekturstil, bei dem Webdienste Daten von und zu Client-Apps empfangen und senden. Ziel dieser Anwendungen ist es, Daten zu zentralisieren, die von verschiedenen Client-Apps verwendet werden.

Die Auswahl der richtigen Tools zum Schreiben von RESTful-Services ist von entscheidender Bedeutung, da die Skalierbarkeit, Wartung, Dokumentation und alle anderen relevanten Aspekte berücksichtigt werden müssen. Der ASP.NET Core bietet uns eine leistungsstarke, benutzerfreundliche API, mit der diese Ziele erreicht werden können.

In diesem Artikel zeige ich Ihnen, wie Sie mit dem ASP.NET Core-Framework eine gut strukturierte RESTful-API für ein "fast" reales Szenario schreiben. Ich werde allgemeine Muster und Strategien beschreiben, um den Entwicklungsprozess zu vereinfachen.

Ich zeige Ihnen auch, wie Sie allgemeine Frameworks und Bibliotheken wie Entity Framework Core und AutoMapper integrieren, um die erforderlichen Funktionen bereitzustellen.

Voraussetzungen

Ich erwarte von Ihnen Kenntnisse über objektorientierte Programmierkonzepte.

Obwohl ich viele Details der C # -Programmiersprache behandeln werde, empfehle ich Ihnen, Grundkenntnisse in diesem Thema zu haben.

Ich gehe auch davon aus, dass Sie wissen, was REST ist, wie das HTTP-Protokoll funktioniert, was API-Endpunkte sind und was JSON ist. Hier ist ein großartiges Einführungs-Tutorial zu diesem Thema. Die letzte Voraussetzung ist, dass Sie verstehen, wie relationale Datenbanken funktionieren.

Um mit mir zusammen programmieren zu können, müssen Sie .NET Core 2.2 und Postman installieren, das Tool, mit dem ich die API testen werde. Ich empfehle Ihnen, einen Code-Editor wie Visual Studio Code zu verwenden, um die API zu entwickeln. Wählen Sie den gewünschten Code-Editor. Wenn Sie diesen Code-Editor auswählen, empfehle ich Ihnen, die C # -Erweiterung zu installieren, um eine bessere Code-Hervorhebung zu erzielen.

Sie finden am Ende dieses Artikels einen Link zum Github-Repository der API, um das Endergebnis zu überprüfen.

Der Geltungsbereich

Schreiben wir eine fiktive Web-API für einen Supermarkt. Stellen wir uns vor, wir müssen den folgenden Geltungsbereich implementieren:

  • Erstellen Sie einen RESTful-Service, mit dem Kundenanwendungen den Produktkatalog des Supermarkts verwalten können. Endpunkte müssen verfügbar gemacht werden, um Produktkategorien wie Milchprodukte und Kosmetika zu erstellen, zu lesen, zu bearbeiten und zu löschen sowie Produkte dieser Kategorien zu verwalten.
  • Für Kategorien müssen wir ihre Namen speichern. Bei Produkten müssen die Namen, die Maßeinheit (z. B. KG für Produkte, die nach Gewicht gemessen werden), die Menge in der Verpackung (z. B. 10, wenn es sich bei dem Produkt um eine Kekspackung handelt) und die jeweiligen Kategorien gespeichert werden.

Um das Beispiel zu vereinfachen, werde ich keine Produkte auf Lager, Produktversand, Sicherheit und andere Funktionen behandeln. Der angegebene Umfang reicht aus, um Ihnen die Funktionsweise von ASP.NET Core zu zeigen.

Um diesen Service zu entwickeln, benötigen wir grundsätzlich zwei API-Endpunkte: einen zum Verwalten von Kategorien und einen zum Verwalten von Produkten. In Bezug auf die JSON-Kommunikation können wir uns die folgenden Antworten vorstellen:

API-Endpunkt: / api / categories

JSON-Antwort (für GET-Anforderungen):

{
  [
    {"id": 1, "name": "Obst und Gemüse"},
    {"id": 2, "name": "Breads"},
    … // Andere Kategorien
  ]
}

API-Endpunkt: / api / products

JSON-Antwort (für GET-Anforderungen):

{
  [
    {
      "id": 1,
      "Name": "Zucker",
      "QuantityInPackage": 1,
      "unitOfMeasurement": "KG"
      "Kategorie": {
        "id": 3,
        "Name": "Zucker"
      }
    },
    … // Andere Produkte
  ]
}

Beginnen wir mit dem Schreiben der Anwendung.

Schritt 1 - API erstellen

Zunächst müssen wir die Ordnerstruktur für den Webdienst erstellen und dann mit den .NET CLI-Tools eine grundlegende Web-API erstellen. Öffnen Sie das Terminal oder die Eingabeaufforderung (dies hängt vom verwendeten Betriebssystem ab) und geben Sie die folgenden Befehle nacheinander ein:

mkdir src / Supermarket.API
cd src / supermarkt.api
Dotnet neue Webapi

Mit den ersten beiden Befehlen wird einfach ein neues Verzeichnis für die API erstellt und der aktuelle Speicherort in den neuen Ordner geändert. Das letzte generiert ein neues Projekt, das der Web-API-Vorlage folgt. Dies ist die Art von Anwendung, die wir entwickeln. Weitere Informationen zu diesem Befehl und zu anderen Projektvorlagen, die Sie über diesen Link generieren können, finden Sie hier.

Das neue Verzeichnis hat nun folgende Struktur:

Projektstruktur

Strukturübersicht

Eine ASP.NET Core-Anwendung besteht aus einer Gruppe von Middlewares (kleine Teile der Anwendung, die an die Anwendungspipeline angehängt sind und Anforderungen und Antworten verarbeiten), die in der Startup-Klasse konfiguriert sind. Wenn Sie bereits mit Frameworks wie Express.js gearbeitet haben, ist dieses Konzept für Sie nicht neu.

Wenn die Anwendung gestartet wird, wird die Main-Methode aus der Program-Klasse aufgerufen. Mithilfe der Startkonfiguration wird ein Standardwebhost erstellt, der die Anwendung über HTTP über einen bestimmten Port verfügbar macht (standardmäßig Port 5000 für HTTP und 5001 für HTTPS).

Sehen Sie sich die ValuesController-Klasse im Ordner Controllers an. Es macht Methoden verfügbar, die aufgerufen werden, wenn die API Anforderungen über route / api / values ​​empfängt.

Machen Sie sich keine Sorgen, wenn Sie einen Teil dieses Codes nicht verstehen. Ich werde bei der Entwicklung der erforderlichen API-Endpunkte auf jedes Detail eingehen. Löschen Sie vorerst einfach diese Klasse, da wir sie nicht verwenden werden.

Schritt 2 - Erstellen der Domänenmodelle

Ich werde einige Designkonzepte anwenden, mit denen die Anwendung einfach und leicht zu warten bleibt.

Das Schreiben von Code, der von Ihnen selbst verstanden und gepflegt werden kann, ist nicht so schwierig. Sie müssen jedoch bedenken, dass Sie als Teil eines Teams arbeiten werden. Wenn Sie sich nicht darum kümmern, wie Sie Ihren Code schreiben, wird das Ergebnis ein Monster sein, das Ihnen und Ihren Teamkollegen ständige Kopfschmerzen bereitet. Es klingt extrem, oder? Aber glauben Sie mir, das ist die Wahrheit.

wtf - Code-Qualitätsmessung von smitty42 ist unter CC-BY-ND 2.0 lizenziert

Beginnen wir mit dem Schreiben der Domänenschicht. Diese Ebene enthält unsere Modellklassen, die Klassen, die unsere Produkte und Kategorien darstellen, sowie Repositorys und Serviceschnittstellen. Ich werde diese beiden letzten Konzepte gleich erläutern.

Erstellen Sie im Verzeichnis Supermarket.API einen neuen Ordner mit dem Namen Domain. Erstellen Sie im neuen Domänenordner einen weiteren Ordner mit dem Namen Models. Das erste Modell, das wir diesem Ordner hinzufügen müssen, ist die Kategorie. Anfänglich handelt es sich um eine einfache POCO-Klasse (Plain Old CLR Object). Dies bedeutet, dass die Klasse nur Eigenschaften hat, um ihre Basisinformationen zu beschreiben.

Die Klasse verfügt über eine Id-Eigenschaft zum Identifizieren der Kategorie und eine Name-Eigenschaft. Wir haben auch eine Produkteigenschaft. Letzteres wird von Entity Framework Core verwendet, dem ORM, mit dem die meisten ASP.NET Core-Anwendungen Daten in einer Datenbank speichern und die Beziehung zwischen Kategorien und Produkten abbilden. Es ist auch sinnvoll, im Sinne einer objektorientierten Programmierung zu denken, da eine Kategorie viele verwandte Produkte enthält.

Wir müssen auch das Produktmodell erstellen. Fügen Sie im selben Ordner eine neue Produktklasse hinzu.

Das Produkt verfügt auch über Eigenschaften für die ID und den Namen. Das ist auch eine Eigenschaft QuantityInPackage, die angibt, wie viele Einheiten des Produkts in einer Packung enthalten sind (denken Sie an das Keksbeispiel des Anwendungsbereichs) und eine UnitOfMeasurement-Eigenschaft. Dieser wird durch einen Aufzählungstyp dargestellt, der eine Aufzählung möglicher Maßeinheiten darstellt. Die letzten beiden Eigenschaften CategoryId und Category werden vom ORM verwendet, um die Beziehung zwischen Produkten und Kategorien abzubilden. Es gibt an, dass ein Produkt nur eine Kategorie hat.

Definieren wir den letzten Teil unserer Domain-Modelle, die EUnitOfMeasurement-Enumeration.

Konventionell müssen Enums nicht mit einem "E" vor ihrem Namen beginnen, aber in einigen Bibliotheken und Frameworks finden Sie dieses Präfix, um Enums von Interfaces und Klassen zu unterscheiden.

Der Code ist wirklich einfach. Hier haben wir nur eine Handvoll Möglichkeiten für Maßeinheiten definiert. In einem realen Supermarktsystem haben Sie jedoch möglicherweise viele andere Maßeinheiten und möglicherweise ein separates Modell dafür.

Beachten Sie das Description-Attribut, das für jede Aufzählungsmöglichkeit angewendet wird. Ein Attribut ist eine Möglichkeit, Metadaten über Klassen, Schnittstellen, Eigenschaften und andere Komponenten der C # -Sprache zu definieren. In diesem Fall werden wir es verwenden, um die Antworten des Produkt-API-Endpunkts zu vereinfachen, aber Sie müssen sich im Moment nicht darum kümmern. Wir kommen später wieder.

Unsere Basismodelle sind einsatzbereit. Jetzt können wir mit dem Schreiben des API-Endpunkts beginnen, der alle Kategorien verwalten soll.

Schritt 3 - Die Categories-API

Fügen Sie im Ordner "Controller" eine neue Klasse mit dem Namen "CategoriesController" hinzu.

Gemäß der Konvention werden alle Klassen in diesem Ordner, die mit dem Suffix „Controller“ enden, zu Controllern unserer Anwendung. Dies bedeutet, dass sie Anfragen und Antworten bearbeiten werden. Sie müssen diese Klasse von der Controller-Klasse erben, die im Namespace Microsoft.AspNetCore.Mvc definiert ist.

Ein Namespace besteht aus einer Gruppe zusammengehöriger Klassen, Schnittstellen, Aufzählungen und Strukturen. Sie können sich das als etwas ähnliches wie Module der Javascript-Sprache oder Pakete aus Java vorstellen.

Der neue Controller sollte über route / api / categories antworten. Dies erreichen wir, indem wir das Route-Attribut über dem Klassennamen hinzufügen und einen Platzhalter angeben, der angibt, dass die Route den Klassennamen gemäß Konvention ohne das Controller-Suffix verwenden soll.

Beginnen wir mit der Bearbeitung von GET-Anfragen. Wenn jemand Daten von / api / categories über das GET-Verb anfordert, muss die API zunächst alle Kategorien zurückgeben. Zu diesem Zweck können wir einen Kategoriedienst erstellen.

Konzeptionell ist ein Service im Grunde eine Klasse oder Schnittstelle, die Methoden definiert, um eine Geschäftslogik zu handhaben. In vielen verschiedenen Programmiersprachen ist es üblich, Dienste für die Geschäftslogik zu erstellen, z. B. Authentifizierung und Autorisierung, Zahlungen, komplexe Datenflüsse, Zwischenspeicherung und Aufgaben, die eine gewisse Interaktion zwischen anderen Diensten oder Modellen erfordern.

Mithilfe von Diensten können wir die Verarbeitung von Anforderungen und Antworten von der eigentlichen Logik isolieren, die zum Ausführen von Aufgaben erforderlich ist.

Der Service, den wir zu Beginn erstellen, definiert ein einzelnes Verhalten oder eine einzelne Methode: eine Auflistungsmethode. Wir erwarten, dass diese Methode alle vorhandenen Kategorien in der Datenbank zurückgibt.

Der Einfachheit halber werden wir uns in diesem Fall nicht mit der Paginierung oder Filterung von Daten befassen. Ich werde in Zukunft einen Artikel schreiben, der zeigt, wie diese Funktionen einfach zu handhaben sind.

Um ein erwartetes Verhalten für etwas in C # (und in anderen objektorientierten Sprachen wie beispielsweise Java) zu definieren, definieren wir eine Schnittstelle. Eine Schnittstelle gibt an, wie etwas funktionieren soll, implementiert jedoch nicht die eigentliche Logik für das Verhalten. Die Logik wird in Klassen implementiert, die die Schnittstelle implementieren. Wenn Ihnen dieses Konzept nicht klar ist, machen Sie sich keine Sorgen. Du wirst es in einer Weile verstehen.

Erstellen Sie im Ordner Domain ein neues Verzeichnis mit dem Namen Services. Fügen Sie dort eine Schnittstelle mit dem Namen ICategoryService hinzu. Standardmäßig sollten alle Schnittstellen mit dem Großbuchstaben "I" in C # beginnen. Definieren Sie den Schnittstellencode wie folgt:

Die Implementierungen der ListAsync-Methode müssen asynchron eine Aufzählung von Kategorien zurückgeben.

Die Task-Klasse, die die Rückgabe einkapselt, zeigt Asynchronität an. Wir müssen uns eine asynchrone Methode überlegen, da wir warten müssen, bis die Datenbank einige Vorgänge abgeschlossen hat, um die Daten zurückzugeben. Dieser Vorgang kann eine Weile dauern. Beachten Sie auch das Suffix "async". Diese Konvention besagt, dass unsere Methode asynchron ausgeführt werden sollte.

Wir haben viele Konventionen, richtig? Ich persönlich mag es, weil es die Lesbarkeit von Anwendungen verbessert, auch wenn Sie noch kein Neuling in einem Unternehmen sind, das .NET-Technologie einsetzt.

„- Ok, wir haben diese Schnittstelle definiert, aber sie macht nichts. Wie kann es nützlich sein? "

Wenn Sie aus einer Sprache wie Javascript oder einer anderen nicht stark typisierten Sprache stammen, kann dieses Konzept seltsam erscheinen.

Schnittstellen ermöglichen es uns, das gewünschte Verhalten von der tatsächlichen Implementierung zu abstrahieren. Mithilfe eines Mechanismus, der als Abhängigkeitsinjektion bezeichnet wird, können wir diese Schnittstellen implementieren und von anderen Komponenten isolieren.

Grundsätzlich definieren Sie bei Verwendung der Abhängigkeitsinjektion einige Verhaltensweisen mithilfe einer Schnittstelle. Anschließend erstellen Sie eine Klasse, die die Schnittstelle implementiert. Zuletzt binden Sie die Referenzen von der Schnittstelle an die von Ihnen erstellte Klasse.

„- Es klingt wirklich verwirrend. Können wir nicht einfach eine Klasse schaffen, die diese Dinge für uns erledigt? "

Lassen Sie uns mit der Implementierung unserer API fortfahren, und Sie werden verstehen, warum Sie diesen Ansatz verwenden sollten.

Ändern Sie den CategoriesController-Code wie folgt:

Ich habe eine Konstruktorfunktion für unseren Controller definiert (ein Konstruktor wird aufgerufen, wenn eine neue Instanz einer Klasse erstellt wird) und er empfängt eine Instanz von ICategoryService. Dies bedeutet, dass die Instanz alles sein kann, was die Serviceschnittstelle implementiert. Ich speichere diese Instanz in einem privaten schreibgeschützten Feld _categoryService. In diesem Feld greifen wir auf die Methoden unserer Kategorie-Service-Implementierung zu.

Übrigens ist das Unterstrich-Präfix eine weitere gebräuchliche Konvention, um ein Feld zu bezeichnen. Insbesondere diese Konvention wird in der offiziellen Richtlinie für Namenskonventionen von .NET nicht empfohlen. Es ist jedoch eine weit verbreitete Praxis, um die Verwendung des Schlüsselworts "this" zur Unterscheidung von Klassenfeldern von lokalen Variablen zu vermeiden. Ich persönlich finde es viel sauberer zu lesen und viele Frameworks und Bibliotheken verwenden diese Konvention.

Unterhalb des Konstruktors habe ich die Methode definiert, die Anforderungen für / api / categories verarbeiten soll. Das HttpGet-Attribut weist die ASP.NET Core-Pipeline an, es zur Verarbeitung von GET-Anforderungen zu verwenden (dieses Attribut kann weggelassen werden, es ist jedoch besser, es zu schreiben, um die Lesbarkeit zu verbessern).

Die Methode verwendet unsere Category-Service-Instanz, um alle Kategorien aufzulisten, und gibt die Kategorien dann an den Client zurück. Die Framework-Pipeline verwaltet die Serialisierung von Daten in ein JSON-Objekt. Der IEnumerable -Typ teilt dem Framework mit, dass eine Aufzählung von Kategorien zurückgegeben werden soll, und der Task-Typ mit dem vorangestellten Schlüsselwort async teilt der Pipeline mit, dass diese Methode asynchron ausgeführt werden soll. Wenn wir schließlich eine asynchrone Methode definieren, müssen wir das Schlüsselwort await für Aufgaben verwenden, die eine Weile dauern können.

Ok, wir haben die anfängliche Struktur unserer API definiert. Jetzt ist es notwendig, den Kategoriedienst wirklich zu implementieren.

Schritt 4 - Implementierung des Categories-Service

Erstellen Sie im Stammordner der API (dem Ordner "Supermarket.API") einen neuen Ordner mit dem Namen "Services". Hier werden alle Service-Implementierungen aufgeführt. Fügen Sie im neuen Ordner eine neue Klasse mit dem Namen CategoryService hinzu. Ändern Sie den Code wie folgt:

Es ist einfach der grundlegende Code für die Schnittstellenimplementierung, aber wir behandeln immer noch keine Logik. Überlegen wir uns, wie die Auflistungsmethode funktionieren soll.

Wir müssen auf die Datenbank zugreifen und alle Kategorien zurückgeben, dann müssen wir diese Daten an den Client zurückgeben.

Eine Serviceklasse ist keine Klasse, die den Datenzugriff handhaben soll. Es gibt ein Muster namens Repository-Muster, mit dem Daten aus Datenbanken verwaltet werden.

Bei Verwendung des Repository-Musters definieren wir Repository-Klassen, die im Grunde die gesamte Logik für den Datenzugriff kapseln. Diese Repositorys bieten Methoden zum Auflisten, Erstellen, Bearbeiten und Löschen von Objekten eines bestimmten Modells, auf dieselbe Weise, wie Sie Sammlungen bearbeiten können. Intern kommunizieren diese Methoden mit der Datenbank, um CRUD-Vorgänge auszuführen und den Datenbankzugriff vom Rest der Anwendung zu isolieren.

Unser Service muss mit einem Kategorie-Repository sprechen, um die Liste der Objekte zu erhalten.

Konzeptionell kann ein Dienst mit einem oder mehreren Repositorys oder anderen Diensten "kommunizieren", um Vorgänge auszuführen.

Es mag überflüssig erscheinen, eine neue Definition für die Handhabung der Datenzugriffslogik zu erstellen, aber Sie werden mit der Zeit feststellen, dass es wirklich vorteilhaft ist, diese Logik von der Serviceklasse zu isolieren.

Erstellen wir ein Repository, das für die Vermittlung der Datenbankkommunikation zuständig ist, um Kategorien beizubehalten.

Schritt 5 - Das Categories-Repository und die Persistenzschicht

Erstellen Sie im Ordner Domain ein neues Verzeichnis mit dem Namen Repositories. Fügen Sie dann eine neue Schnittstelle mit dem Namen ICategoryRespository hinzu. Definieren Sie die Schnittstelle wie folgt:

Der Initialcode ist grundsätzlich identisch mit dem Code der Service-Schnittstelle.

Nachdem Sie die Schnittstelle definiert haben, kehren Sie zur Serviceklasse zurück und beenden die Implementierung der Auflistungsmethode. Verwenden Sie dazu eine Instanz von ICategoryRepository, um die Daten zurückzugeben.

Jetzt müssen wir die reale Logik des Category Repositorys implementieren. Bevor wir dies tun, müssen wir uns überlegen, wie wir auf die Datenbank zugreifen.

Übrigens haben wir noch keine Datenbank!

Wir verwenden den Entity Framework Core (der Einfachheit halber nenne ich ihn EF Core) als Datenbank-ORM. Dieses Framework wird mit ASP.NET Core als Standard-ORM geliefert und stellt eine benutzerfreundliche API bereit, mit der wir Klassen unserer Anwendungen Datenbanktabellen zuordnen können.

Mit dem EF Core können wir auch zuerst unsere Anwendung entwerfen und dann eine Datenbank gemäß den Definitionen in unserem Code generieren. Diese Technik wird zuerst als Code bezeichnet. Wir werden den Code-First-Ansatz verwenden, um eine Datenbank zu generieren (in diesem Beispiel werde ich tatsächlich eine In-Memory-Datenbank verwenden, aber Sie können diese problemlos in eine SQL Server- oder MySQL-Server-Instanz ändern. zum Beispiel).

Erstellen Sie im Stammverzeichnis der API ein neues Verzeichnis mit dem Namen Persistence. Dieses Verzeichnis wird alles enthalten, was wir für den Zugriff auf die Datenbank benötigen, z. B. Repository-Implementierungen.

Erstellen Sie in dem neuen Ordner ein neues Verzeichnis mit Name Contexts und fügen Sie dann eine neue Klasse hinzu, die als AppDbContext bezeichnet wird. Diese Klasse muss DbContext erben, eine Klasse, mit der EF Core Ihre Modelle Datenbanktabellen zuordnet. Ändern Sie den Code folgendermaßen:

Der Konstruktor, den wir dieser Klasse hinzugefügt haben, ist dafür verantwortlich, die Datenbankkonfiguration durch Abhängigkeitsinjektion an die Basisklasse zu übergeben. Sie werden gleich sehen, wie das funktioniert.

Jetzt müssen wir zwei DbSet-Eigenschaften erstellen. Diese Eigenschaften sind Mengen (Sammlungen eindeutiger Objekte), die Modelle Datenbanktabellen zuordnen.

Außerdem müssen wir die Modelleigenschaften den entsprechenden Tabellenspalten zuordnen und angeben, welche Eigenschaften Primärschlüssel, welche Fremdschlüssel, Spaltentypen usw. sind. Dies kann über die Methode OnModelCreating mit einer Funktion namens Fluent API to erfolgen Geben Sie die Datenbankzuordnung an. Ändern Sie die AppDbContext-Klasse wie folgt:

Der Code ist intuitiv.

Wir legen fest, auf welche Tabellen unsere Modelle abgebildet werden sollen. Außerdem setzen wir die Primärschlüssel mit der Methode HasKey, die Tabellenspalten mit der Property-Methode und einige Einschränkungen wie IsRequired, HasMaxLength und ValueGeneratedOnAdd, alles mit Lambda-Ausdrücken auf "flüssige Weise" (Verkettungsmethoden).

Sehen Sie sich den folgenden Code an:

builder.Entity  ()
       .HasMany (p => p.Products)
       .WithOne (p => p.Category)
       .HasForeignKey (p => p.CategoryId);

Hier geben wir eine Beziehung zwischen Tabellen an. Wir sagen, dass eine Kategorie viele Produkte enthält, und wir legen die Eigenschaften fest, die diese Beziehung abbilden (Produkte aus Kategorieklasse und Kategorie aus Produktklasse). Wir setzen auch den Fremdschlüssel (CategoryId).

Schauen Sie sich dieses Tutorial an, um zu erfahren, wie Sie mit EF Core Eins-zu-Eins- und Viele-zu-Viele-Beziehungen konfigurieren und wie Sie es als Ganzes verwenden.

Es gibt auch eine Konfiguration für das Seeding von Daten mit der Methode HasData:

builder.Entity  (). HasData
(
  neue Kategorie {Id = 100, Name = "Obst und Gemüse"},
  neue Kategorie {Id = 101, Name = "Dairy"}
);

Hier fügen wir einfach standardmäßig zwei Beispielkategorien hinzu. Dies ist erforderlich, um unseren API-Endpunkt nach dessen Fertigstellung zu testen.

Hinweis: Die ID-Eigenschaften werden hier manuell festgelegt, da der In-Memory-Anbieter dies erfordert. Ich setze die Bezeichner auf große Zahlen, um eine Kollision zwischen automatisch generierten Bezeichnern und Startdaten zu vermeiden.
Diese Einschränkung gilt nicht für echte relationale Datenbankanbieter. Wenn Sie also beispielsweise eine Datenbank wie SQL Server verwenden möchten, müssen Sie diese Bezeichner nicht angeben. Überprüfen Sie dieses Github-Problem, wenn Sie dieses Verhalten verstehen möchten.

Nachdem wir die Datenbankkontextklasse implementiert haben, können wir das Categories-Repository implementieren. Fügen Sie im Ordner Persistenz einen neuen Ordner mit dem Namen Repositorys hinzu, und fügen Sie dann eine neue Klasse mit dem Namen BaseRepository hinzu.

Diese Klasse ist nur eine abstrakte Klasse, die alle unsere Repositorys übernehmen. Eine abstrakte Klasse ist eine Klasse, die keine direkten Instanzen hat. Sie müssen direkte Klassen erstellen, um die Instanzen zu erstellen.

Das BaseRepository empfängt eine Instanz unseres AppDbContext durch Abhängigkeitsinjektion und legt eine geschützte Eigenschaft (eine Eigenschaft, auf die nur die untergeordneten Klassen zugreifen können) mit dem Namen _context offen, mit der auf alle Methoden zugegriffen werden kann, die zur Verarbeitung von Datenbankvorgängen erforderlich sind.

Fügen Sie eine neue Klasse im selben Ordner mit dem Namen CategoryRepository hinzu. Jetzt werden wir die Repository-Logik wirklich implementieren:

Das Repository erbt das BaseRepository und implementiert ICategoryRepository.

Beachten Sie, wie einfach es ist, die Listungsmethode zu implementieren. Wir verwenden den Categories-Datenbanksatz, um auf die Categories-Tabelle zuzugreifen, und rufen dann die Erweiterungsmethode ToListAsync auf, die dafür verantwortlich ist, das Ergebnis einer Abfrage in eine Auflistung von Kategorien umzuwandeln.

Der EF Core übersetzt unseren Methodenaufruf so effizient wie möglich in eine SQL-Abfrage. Die Abfrage wird nur ausgeführt, wenn Sie eine Methode aufrufen, mit der Ihre Daten in eine Auflistung umgewandelt werden, oder wenn Sie eine Methode zum Aufnehmen bestimmter Daten verwenden.

Wir haben jetzt eine saubere Implementierung der Kategorien Controller, Service und Repository.

Wir haben Bedenken getrennt und Klassen geschaffen, die nur das tun, was sie tun sollen.

Der letzte Schritt vor dem Testen der Anwendung besteht darin, unsere Schnittstellen mit dem ASP.NET Core-Mechanismus für das Einfügen von Abhängigkeiten an die jeweiligen Klassen zu binden.

Schritt 6 - Konfigurieren der Abhängigkeitsinjektion

Es ist Zeit, dass Sie endlich verstehen, wie dieses Konzept funktioniert.

Öffnen Sie im Stammordner der Anwendung die Startup-Klasse. Diese Klasse ist für die Konfiguration aller Arten von Konfigurationen verantwortlich, wenn die Anwendung gestartet wird.

Die Methoden ConfigureServices und Configure werden zur Laufzeit von der Framework-Pipeline aufgerufen, um zu konfigurieren, wie die Anwendung funktionieren soll und welche Komponenten verwendet werden müssen.

Schauen Sie sich die ConfigureServices-Methode an. Hier haben wir nur eine Zeile, die die Anwendung für die Verwendung der MVC-Pipeline konfiguriert, was im Grunde bedeutet, dass die Anwendung Anforderungen und Antworten mithilfe von Controller-Klassen verarbeitet zur Zeit).

Mit der ConfigureServices-Methode, die auf den services-Parameter zugreift, können wir unsere Abhängigkeitsbindungen konfigurieren. Bereinigen Sie den Klassencode, indem Sie alle Kommentare entfernen und den Code wie folgt ändern:

Sehen Sie sich diesen Code an:

services.AddDbContext  (options => {
  options.UseInMemoryDatabase ("supermarket-api-in-memory");
});

Hier konfigurieren wir den Datenbankkontext. Wir weisen ASP.NET Core an, unseren AppDbContext mit einer speicherinternen Datenbankimplementierung zu verwenden, die durch die Zeichenfolge identifiziert wird, die als Argument an unsere Methode übergeben wird. Normalerweise wird der In-Memory-Provider verwendet, wenn wir Integrationstests schreiben. Der Einfachheit halber verwende ich ihn hier. Auf diese Weise müssen wir keine Verbindung zu einer echten Datenbank herstellen, um die Anwendung zu testen.

Die Konfiguration dieser Zeilen konfiguriert intern unseren Datenbankkontext für die Abhängigkeitsinjektion unter Verwendung einer Lebensdauer mit Gültigkeitsbereich.

Die Lebensdauer des Gültigkeitsbereichs teilt der ASP.NET Core-Pipeline mit, dass jedes Mal, wenn eine Klasse aufgelöst werden muss, die eine Instanz von AppDbContext als Konstruktorargument empfängt, dieselbe Instanz der Klasse verwendet werden sollte. Befindet sich keine Instanz im Speicher, erstellt die Pipeline eine neue Instanz und verwendet sie während einer bestimmten Anforderung in allen Klassen, die sie benötigen, erneut. Auf diese Weise müssen Sie die Klasseninstanz nicht manuell erstellen, wenn Sie sie verwenden möchten.

Es gibt noch weitere Gültigkeitsbereiche, die Sie in der offiziellen Dokumentation nachlesen können.

Die Abhängigkeitsinjektionstechnik bietet uns viele Vorteile, wie zum Beispiel:

  • Wiederverwendbarkeit des Codes;
  • Höhere Produktivität, da wir uns, wenn wir die Implementierung ändern müssen, nicht die Mühe machen müssen, hundert Stellen zu ändern, an denen Sie diese Funktion verwenden.
  • Sie können die Anwendung einfach testen, da wir das, was wir testen müssen, mithilfe von Mocks (gefälschte Implementierung von Klassen) isolieren können, bei denen wir Schnittstellen als Konstruktorargumente übergeben müssen.
  • Wenn eine Klasse mehr Abhängigkeiten über einen Konstruktor erhalten muss, müssen Sie nicht alle Stellen, an denen die Instanzen erstellt werden, manuell ändern (das ist großartig!).

Nach der Konfiguration des Datenbankkontexts binden wir unseren Service und unser Repository an die jeweiligen Klassen.

services.AddScoped  ();
services.AddScoped  ();

Hier verwenden wir auch eine Lebensdauer mit Gültigkeitsbereich, da diese Klassen intern die Datenbankkontextklasse verwenden müssen. In diesem Fall ist es sinnvoll, den gleichen Gültigkeitsbereich anzugeben.

Nachdem wir nun unsere Abhängigkeitsbindungen konfiguriert haben, müssen wir in der Program-Klasse eine kleine Änderung vornehmen, damit die Datenbank unsere anfänglichen Daten korrekt ausgibt. Dieser Schritt ist nur bei Verwendung des In-Memory-Datenbankanbieters erforderlich (siehe dieses Github-Problem, um zu verstehen, warum).

Die Main-Methode musste geändert werden, um zu gewährleisten, dass unsere Datenbank beim Start der Anwendung "erstellt" wird, da wir einen In-Memory-Provider verwenden. Ohne diese Änderung werden die Kategorien, die wir festlegen möchten, nicht erstellt.

Nachdem alle grundlegenden Funktionen implementiert wurden, ist es an der Zeit, unseren API-Endpunkt zu testen.

Schritt 7 - Testen der Categories-API

Öffnen Sie das Terminal oder die Eingabeaufforderung im API-Stammordner und geben Sie den folgenden Befehl ein:

dotnet laufen

Der obige Befehl startet die Anwendung. Die Konsole wird eine Ausgabe ähnlich der folgenden anzeigen:

info: Microsoft.EntityFrameworkCore.Infrastructure [10403]
Entity Framework Core 2.2.0-rtm-35687 hat "AppDbContext" mit dem Anbieter "Microsoft.EntityFrameworkCore.InMemory" mit den folgenden Optionen initialisiert: StoreName = supermarket-api-in-memory
info: Microsoft.EntityFrameworkCore.Update [30100]
2 Entitäten im In-Memory-Speicher gespeichert.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager [0]
Benutzerprofil ist verfügbar. Verwenden von "C: \ Users \ evgomes \ AppData \ Local \ ASP.NET \ DataProtection-Keys" als Schlüsselrepository und Windows DPAPI zum Verschlüsseln ruhender Schlüssel.
Hosting-Umgebung: Entwicklung
Inhaltsstammpfad: C: \ Users \ evgomes \ Desktop \ Tutorials \ src \ Supermarket.API
Hören Sie jetzt auf: https: // localhost: 5001
Hören Sie jetzt auf: http: // localhost: 5000
Anwendung gestartet. Drücken Sie zum Herunterfahren Strg + C.

Sie können sehen, dass EF Core aufgerufen wurde, um die Datenbank zu initialisieren. In den letzten Zeilen wird angezeigt, an welchen Ports die Anwendung ausgeführt wird.

Öffnen Sie einen Browser und navigieren Sie zu http: // localhost: 5000 / api / categories (oder zu der in der Konsolenausgabe angezeigten URL). Wenn aufgrund von HTTPS ein Sicherheitsfehler auftritt, fügen Sie einfach eine Ausnahme für die Anwendung hinzu.

Der Browser zeigt die folgenden JSON-Daten als Ausgabe an:

[
  {
     "id": 100,
     "Name": "Obst und Gemüse",
     "Produkte": []
  },
  {
     "id": 101,
     "name": "Dairy",
     "Produkte": []
  }
]

Hier sehen wir die Daten, die wir der Datenbank hinzugefügt haben, als wir den Datenbankkontext konfiguriert haben. Diese Ausgabe bestätigt, dass unser Code funktioniert.

Sie haben einen GET-API-Endpunkt mit wirklich wenigen Codezeilen erstellt und verfügen über eine Codestruktur, die aufgrund der API-Architektur sehr einfach zu ändern ist.

Jetzt ist es an der Zeit, Ihnen zu zeigen, wie einfach es ist, diesen Code zu ändern, wenn Sie ihn an geschäftliche Anforderungen anpassen müssen.

Schritt 8 - Erstellen einer Kategorieressource

Wenn Sie sich an die Spezifikation des API-Endpunkts erinnern, haben Sie festgestellt, dass unsere tatsächliche JSON-Antwort eine zusätzliche Eigenschaft aufweist: eine Reihe von Produkten. Schauen Sie sich das Beispiel der gewünschten Antwort an:

{
  [
    {"id": 1, "name": "Obst und Gemüse"},
    {"id": 2, "name": "Breads"},
    … // Andere Kategorien
  ]
}

Das Produktarray ist in unserer aktuellen JSON-Antwort enthalten, da unser Kategoriemodell über eine Products-Eigenschaft verfügt, die von EF Core zum Korrigieren der Zuordnung der Produkte einer bestimmten Kategorie benötigt wird.

Wir möchten diese Eigenschaft in unserer Antwort nicht, können jedoch die Modellklasse nicht ändern, um diese Eigenschaft auszuschließen. Wenn wir versuchen, Kategoriedaten zu verwalten, führt dies zu Fehlern in EF Core. Außerdem führt dies zu einem Bruch des Domain-Modells, da es keinen Sinn macht, eine Produktkategorie zu haben, die keine Produkte enthält.

Um JSON-Daten zurückzugeben, die nur die Bezeichner und Namen der Supermarktkategorien enthalten, müssen Sie eine Ressourcenklasse erstellen.

Eine Ressourcenklasse ist eine Klasse, die nur grundlegende Informationen enthält, die zwischen Clientanwendungen und API-Endpunkten ausgetauscht werden, im Allgemeinen in Form von JSON-Daten, um bestimmte Informationen darzustellen.

Alle Antworten von API-Endpunkten müssen eine Ressource zurückgeben.

Es ist eine schlechte Praxis, die Repräsentation des realen Modells als Antwort zurückzugeben, da sie Informationen enthalten kann, die die Clientanwendung nicht benötigt oder für die sie keine Berechtigung hat (z. B. könnte ein Benutzermodell Informationen des Benutzerkennworts zurückgeben , was ein großes Sicherheitsproblem sein würde).

Wir benötigen eine Ressource, die nur unsere Kategorien ohne die Produkte darstellt.

Nachdem Sie nun wissen, was eine Ressource ist, implementieren wir sie. Beenden Sie zunächst die laufende Anwendung, indem Sie in der Befehlszeile Strg + C drücken. Erstellen Sie im Stammordner der Anwendung einen neuen Ordner mit dem Namen Resources. Fügen Sie dort eine neue Klasse mit dem Namen CategoryResource hinzu.

Wir müssen unsere Sammlung von Kategoriemodellen, die von unserem Kategoriedienst bereitgestellt wird, einer Sammlung von Kategorieressourcen zuordnen.

Wir werden eine Bibliothek namens AutoMapper verwenden, um die Zuordnung zwischen Objekten zu handhaben. AutoMapper ist eine sehr beliebte Bibliothek in der .NET-Welt und wird in vielen kommerziellen und Open-Source-Projekten verwendet.

Geben Sie die folgenden Zeilen in die Befehlszeile ein, um AutoMapper zu unserer Anwendung hinzuzufügen:

dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

Um AutoMapper zu verwenden, müssen wir zwei Dinge tun:

  • Registrieren Sie es für die Abhängigkeitsinjektion.
  • Erstellen Sie eine Klasse, die AutoMapper mitteilt, wie die Klassenzuordnung behandelt wird.

Öffnen Sie zunächst die Startup-Klasse. Fügen Sie in der ConfigureServices-Methode nach der letzten Zeile den folgenden Code hinzu:

services.AddAutoMapper ();

In dieser Zeile werden alle erforderlichen AutoMapper-Konfigurationen behandelt, z. B. die Registrierung für die Abhängigkeitsinjektion und das Scannen der Anwendung beim Start, um die Zuordnungsprofile zu konfigurieren.

Fügen Sie nun im Stammverzeichnis einen neuen Ordner mit dem Namen Mapping hinzu und fügen Sie dann eine Klasse mit dem Namen ModelToResourceProfile hinzu. Ändern Sie den Code folgendermaßen:

Die Klasse erbt Profile, einen Klassentyp, mit dem AutoMapper überprüft, wie unsere Zuordnungen funktionieren. Auf dem Konstruktor erstellen wir eine Zuordnung zwischen der Category-Modellklasse und der CategoryResource-Klasse. Da die Eigenschaften der Klassen dieselben Namen und Typen haben, müssen wir für sie keine spezielle Konfiguration verwenden.

Der letzte Schritt besteht darin, den Kategorieregler so zu ändern, dass er AutoMapper für die Objektzuordnung verwendet.

Ich habe den Konstruktor geändert, um eine Instanz der IMapper-Implementierung zu erhalten. Mit diesen Schnittstellenmethoden können Sie AutoMapper-Zuordnungsmethoden verwenden.

Ich habe auch die GetAllAsync-Methode geändert, um unsere Aufzählung von Kategorien mithilfe der Map-Methode einer Aufzählung von Ressourcen zuzuordnen. Diese Methode empfängt eine Instanz der zuzuordnenden Klasse oder Auflistung und definiert über generische Typdefinitionen, welcher Typ von Klasse oder Auflistung zugeordnet werden muss.

Beachten Sie, dass wir die Implementierung problemlos ändern können, ohne die Serviceklasse oder das Repository anpassen zu müssen, indem Sie dem Konstruktor einfach eine neue Abhängigkeit (IMapper) hinzufügen.

Die Abhängigkeitsinjektion macht Ihre Anwendung wartbar und einfach zu ändern, da Sie nicht die gesamte Codeimplementierung unterbrechen müssen, um Features hinzuzufügen oder zu entfernen.

Sie haben wahrscheinlich festgestellt, dass nicht nur die Controller-Klasse, sondern alle Klassen, die Abhängigkeiten erhalten (einschließlich der Abhängigkeiten selbst), automatisch aufgelöst wurden, um die richtigen Klassen gemäß den Bindungskonfigurationen zu erhalten.

Die Abhängigkeitsinjektion ist erstaunlich, nicht wahr?

Starten Sie nun die API erneut mit dem Befehl dotnet run und rufen Sie http: // localhost: 5000 / api / categories auf, um die neue JSON-Antwort anzuzeigen.

Dies sind die Antwortdaten, die Sie sehen sollten

Wir haben bereits unseren GET-Endpunkt. Jetzt erstellen wir einen neuen Endpunkt für POST-Kategorien (Erstellen).

Schritt 9 - Neue Kategorien erstellen

Bei der Erstellung von Ressourcen müssen wir uns um viele Dinge kümmern, wie zum Beispiel:

  • Datenvalidierung und Datenintegrität;
  • Berechtigung zum Anlegen von Ressourcen;
  • Fehlerbehandlung;
  • Protokollierung.

Ich werde in diesem Tutorial nicht zeigen, wie man mit Authentifizierung und Autorisierung umgeht, aber Sie können sehen, wie Sie diese Funktionen einfach implementieren können, wenn Sie mein Tutorial zur JSON-Web-Token-Authentifizierung lesen.

Außerdem gibt es ein sehr beliebtes Framework namens ASP.NET Identity, das integrierte Lösungen für die Sicherheit und Benutzerregistrierung bietet, die Sie in Ihren Anwendungen verwenden können. Es enthält Anbieter für die Arbeit mit EF Core, z. B. einen integrierten IdentityDbContext, den Sie verwenden können. Mehr dazu erfahren Sie hier.

Erstellen wir einen HTTP-POST-Endpunkt, der die anderen Szenarien abdeckt (mit Ausnahme der Protokollierung, die sich je nach Umfang und Tool ändern kann).

Vor dem Erstellen des neuen Endpunkts benötigen wir eine neue Ressource. Diese Ressource ordnet Daten, die Clientanwendungen an diesen Endpunkt senden (in diesem Fall den Kategorienamen), einer Klasse unserer Anwendung zu.

Da wir eine neue Kategorie erstellen, haben wir noch keine ID. Dies bedeutet, dass wir eine Ressource benötigen, die eine Kategorie darstellt, die nur ihren Namen enthält.

Fügen Sie im Ordner Resources eine neue Klasse mit dem Namen SaveCategoryResource hinzu:

Beachten Sie die Attribute Required und MaxLength, die auf die Eigenschaft Name angewendet werden. Diese Attribute werden als Datenanmerkungen bezeichnet. Die ASP.NET Core-Pipeline verwendet diese Metadaten, um Anforderungen und Antworten zu überprüfen. Wie der Name schon sagt, ist der Kategoriename erforderlich und darf maximal 30 Zeichen lang sein.

Definieren wir nun die Form des neuen API-Endpunkts. Fügen Sie dem Categories-Controller den folgenden Code hinzu:

Wir teilen dem Framework mit, dass dies ein HTTP-POST-Endpunkt ist, der das HttpPost-Attribut verwendet.

Beachten Sie den Antworttyp dieser Methode, Task . In Controller-Klassen vorhandene Methoden werden als Aktionen bezeichnet und haben diese Signatur, da mehr als ein mögliches Ergebnis zurückgegeben werden kann, nachdem die Anwendung die Aktion ausgeführt hat.

In diesem Fall müssen wir, wenn der Kategoriename ungültig ist oder ein Fehler auftritt, eine 400-Code-Antwort (ungültige Anforderung) zurückgeben, die im Allgemeinen eine Fehlermeldung enthält, die Client-Apps zur Behebung des Problems verwenden können 200 Rückmeldungen (Erfolg) mit Daten, wenn alles in Ordnung ist.

Es gibt viele Arten von Aktionstypen, die Sie als Antwort verwenden können. Im Allgemeinen können wir diese Schnittstelle verwenden, und ASP.NET Core verwendet hierfür eine Standardklasse.

Das FromBody-Attribut weist ASP.NET Core an, die Anforderungshauptteildaten in unserer neuen Ressourcenklasse zu analysieren. Dies bedeutet, dass wenn ein JSON mit dem Kategorienamen an unsere Anwendung gesendet wird, das Framework ihn automatisch in unsere neue Klasse analysiert.

Lassen Sie uns nun unsere Routenlogik implementieren. Wir müssen einige Schritte ausführen, um eine neue Kategorie erfolgreich zu erstellen:

  • Zuerst müssen wir die eingehende Anfrage validieren. Wenn die Anforderung ungültig ist, müssen wir eine ungültige Anforderungsantwort mit den Fehlermeldungen zurückgeben.
  • Wenn die Anforderung gültig ist, müssen wir unsere neue Ressource mithilfe von AutoMapper unserer Kategoriemodellklasse zuordnen.
  • Wir müssen jetzt unseren Service anrufen und ihm mitteilen, dass unsere neue Kategorie gespeichert werden soll. Wenn die Speicherlogik ohne Probleme ausgeführt wird, sollte sie eine Antwort mit unseren neuen Kategoriedaten zurückgeben. Wenn nicht, sollte dies einen Hinweis darauf geben, dass der Prozess fehlgeschlagen ist, und eine potenzielle Fehlermeldung anzeigen.
  • Wenn ein Fehler auftritt, wird eine ungültige Anforderung zurückgegeben. Andernfalls ordnen wir unser neues Kategoriemodell einer Kategorieressource zu und geben eine Erfolgsantwort an den Client zurück, die die neuen Kategoriedaten enthält.

Es scheint kompliziert zu sein, aber es ist wirklich einfach, diese Logik mit der Service-Architektur zu implementieren, die wir für unsere API strukturiert haben.

Beginnen wir mit der Validierung der eingehenden Anfrage.

Schritt 10 - Überprüfen des Anforderungshauptteils anhand des Modellstatus

ASP.NET Core-Controller haben eine Eigenschaft namens ModelState. Diese Eigenschaft wird während der Ausführung der Anforderung gefüllt, bevor unsere Aktionsausführung erreicht wird. Es handelt sich um eine Instanz von ModelStateDictionary, einer Klasse, die Informationen enthält, z. B. ob die Anforderung gültig ist und mögliche Überprüfungsfehlermeldungen.

Ändern Sie den Endpunktcode wie folgt:

Der Code überprüft, ob der Modellstatus (in diesem Fall die im Anforderungshauptteil gesendeten Daten) ungültig ist, und überprüft dabei unsere Datenanmerkungen. Ist dies nicht der Fall, gibt die API eine ungültige Anforderung (mit 400-Statuscode) und die Standardfehlermeldungen zurück, die von unseren Anmerkungsmetadaten bereitgestellt wurden.

Die ModelState.GetErrorMessages () -Methode ist noch nicht implementiert. Es handelt sich um eine Erweiterungsmethode (eine Methode, die die Funktionalität einer bereits vorhandenen Klasse oder Schnittstelle erweitert), die ich implementieren möchte, um die Überprüfungsfehler in einfache Zeichenfolgen umzuwandeln und an den Client zurückzugeben.

Fügen Sie im Stammverzeichnis unserer API eine neue Ordnererweiterung hinzu und fügen Sie dann eine neue Klasse ModelStateExtensions hinzu.

Alle Erweiterungsmethoden sowie die Klassen, in denen sie deklariert sind, sollten statisch sein. Dies bedeutet, dass sie keine bestimmten Instanzdaten verarbeiten und beim Starten der Anwendung nur einmal geladen werden.

Das Schlüsselwort this vor der Parameterdeklaration weist den C # -Compiler an, es als Erweiterungsmethode zu behandeln. Das Ergebnis ist, dass wir es wie eine normale Methode dieser Klasse aufrufen können, da wir die entsprechende using-Direktive einfügen, in der wir die Erweiterung verwenden möchten.

Die Erweiterung verwendet LINQ-Abfragen, eine sehr nützliche Funktion von .NET, mit der wir Daten mithilfe verkettbarer Ausdrücke abfragen und transformieren können. Die Ausdrücke hier transformieren die Validierungsfehlermethoden in eine Liste von Zeichenfolgen, die die Fehlermeldungen enthalten.

Importieren Sie den Namespace Supermarket.API.Extensions in den Categories-Controller, bevor Sie mit dem nächsten Schritt fortfahren.

Verwenden von Supermarket.API.Extensions;

Setzen wir die Implementierung unserer Endpunktlogik fort, indem wir unsere neue Ressource einer Kategoriemodellklasse zuordnen.

Schritt 11 - Zuordnen der neuen Ressource

Wir haben bereits ein Mapping-Profil definiert, um Modelle in Ressourcen umzuwandeln. Jetzt brauchen wir ein neues Profil, das die Umkehrung vornimmt.

Fügen Sie dem Mapping-Ordner eine neue Klasse ResourceToModelProfile hinzu:

Hier gibt es nichts Neues. Dank der Magie der Abhängigkeitsinjektion registriert AutoMapper dieses Profil automatisch, wenn die Anwendung gestartet wird, und wir müssen keinen anderen Ort ändern, um sie zu verwenden.

Jetzt können wir unsere neue Ressource der jeweiligen Modellklasse zuordnen:

Schritt 12 - Anwenden des Request-Response-Musters zur Verarbeitung der Speicherlogik

Jetzt müssen wir die interessanteste Logik implementieren: eine neue Kategorie speichern. Wir erwarten, dass unser Service dies tut.

Die Speicherlogik schlägt möglicherweise aufgrund von Problemen beim Herstellen einer Verbindung zur Datenbank fehl oder weil eine interne Geschäftsregel unsere Daten ungültig macht.

Wenn etwas schief geht, können wir nicht einfach einen Fehler auslösen, da dies die API stoppen könnte und die Client-Anwendung nicht weiß, wie das Problem zu behandeln ist. Außerdem verfügen wir möglicherweise über einen Protokollierungsmechanismus, der den Fehler protokolliert.

Der Vertrag der Speichermethode, dh die Signatur der Methode und des Antworttyps, muss uns anzeigen, ob der Prozess korrekt ausgeführt wurde. Wenn der Vorgang erfolgreich ist, erhalten wir die Kategoriedaten. Wenn nicht, müssen wir zumindest eine Fehlermeldung erhalten, die angibt, warum der Prozess fehlgeschlagen ist.

Wir können diese Funktion implementieren, indem wir das Anforderungs-Antwort-Muster anwenden. Dieses Enterprise-Design-Muster kapselt unsere Anforderungs- und Antwortparameter in Klassen, um Informationen zu kapseln, die unsere Services zur Verarbeitung bestimmter Aufgaben verwenden, und um Informationen an die Klasse zurückzugeben, die den Service verwendet.

Dieses Muster gibt uns einige Vorteile, wie zum Beispiel:

  • Wenn wir unseren Service ändern müssen, um weitere Parameter zu erhalten, müssen wir seine Signatur nicht brechen.
  • Wir können einen Standardvertrag für unsere Anfrage und / oder Antworten definieren.
  • Wir können mit Geschäftslogik und potenziellen Ausfällen umgehen, ohne den Anwendungsprozess anzuhalten, und wir müssen keine Tonnen von Try-Catch-Blöcken verwenden.

Erstellen wir einen Standardantworttyp für unsere Dienstmethoden, der Datenänderungen verarbeitet. Für jede Anfrage dieses Typs möchten wir wissen, ob die Anfrage ohne Probleme ausgeführt wird. Wenn dies fehlschlägt, möchten wir eine Fehlermeldung an den Client zurücksenden.

Fügen Sie im Ordner Domain unter Services ein neues Verzeichnis mit dem Namen Communication hinzu. Fügen Sie dort eine neue Klasse namens BaseResponse hinzu.

Dies ist eine abstrakte Klasse, die unsere Antworttypen übernehmen.

Die Abstraktion definiert eine Success-Eigenschaft, die angibt, ob Anforderungen erfolgreich abgeschlossen wurden, und eine Message-Eigenschaft, die die Fehlermeldung anzeigt, wenn ein Fehler auftritt.

Beachten Sie, dass diese Eigenschaften erforderlich sind und nur geerbte Klassen diese Daten festlegen können, da untergeordnete Klassen diese Informationen über die Konstruktorfunktion übergeben müssen.

Tipp: Es ist keine gute Vorgehensweise, Basisklassen für alles zu definieren, da Basisklassen Ihren Code koppeln und verhindern, dass Sie ihn leicht ändern können. Verwenden Sie die Komposition lieber als die Vererbung.
Für den Umfang dieser API ist die Verwendung von Basisklassen kein wirkliches Problem, da unsere Services nicht sehr umfangreich werden. Wenn Sie feststellen, dass ein Dienst oder eine Anwendung häufig wächst und sich ändert, vermeiden Sie die Verwendung einer Basisklasse.

Fügen Sie nun im selben Ordner eine neue Klasse mit dem Namen SaveCategoryResponse hinzu.

Der Antworttyp legt auch eine Category-Eigenschaft fest, die unsere Kategoriedaten enthält, wenn die Anforderung erfolgreich abgeschlossen wird.

Beachten Sie, dass ich drei verschiedene Konstruktoren für diese Klasse definiert habe:

  • Ein privates Objekt, das die Erfolgs- und Nachrichtenparameter an die Basisklasse übergibt und außerdem die Category-Eigenschaft festlegt.
  • Ein Konstruktor, der nur die Kategorie als Parameter empfängt. Dieser Befehl erstellt eine erfolgreiche Antwort und ruft den privaten Konstruktor auf, um die entsprechenden Eigenschaften festzulegen.
  • Ein dritter Konstruktor, der nur die Nachricht angibt. Dieser wird verwendet, um eine Fehlerreaktion zu erstellen.

Da C # mehrere Konstruktoren unterstützt, haben wir die Antworterstellung vereinfacht, ohne dafür eine andere Methode zu definieren, nur indem wir verschiedene Konstruktoren verwendet haben.

Jetzt können wir unsere Serviceschnittstelle ändern, um den neuen Speichermethodenvertrag hinzuzufügen.

Ändern Sie die ICategoryService-Schnittstelle wie folgt:

Wir übergeben dieser Methode einfach eine Kategorie. Sie verarbeitet die gesamte Logik, die zum Speichern der Modelldaten erforderlich ist, und orchestriert dazu Repositorys und andere erforderliche Services.

Beachten Sie, dass ich hier keine bestimmte Anforderungsklasse erstelle, da wir keine anderen Parameter benötigen, um diese Aufgabe auszuführen. In der Computerprogrammierung gibt es ein Konzept namens KISS - kurz für Keep it Simple, Stupid. Grundsätzlich heißt es, dass Sie Ihre Bewerbung so einfach wie möglich halten sollten.

Beachten Sie Folgendes beim Entwerfen Ihrer Anwendungen: Wenden Sie nur das an, was Sie zur Lösung eines Problems benötigen. Überarbeiten Sie Ihre Anwendung nicht.

Jetzt können wir unsere Endpunktlogik beenden:

Nachdem wir die Anforderungsdaten validiert und die Ressource unserem Modell zugeordnet haben, übergeben wir sie an unseren Service, um die Daten beizubehalten.

Wenn etwas fehlschlägt, gibt die API eine ungültige Anforderung zurück. Andernfalls ordnet die API die neue Kategorie (jetzt einschließlich Daten wie der neuen ID) unserer zuvor erstellten CategoryResource zu und sendet sie an den Client.

Lassen Sie uns nun die eigentliche Logik für den Service implementieren.

Schritt 13 - Die Datenbanklogik und das Muster der Arbeitseinheit

Da die Daten in der Datenbank erhalten bleiben sollen, benötigen wir eine neue Methode in unserem Repository.

Fügen Sie der ICategoryRepository-Schnittstelle eine neue AddAsync-Methode hinzu:

Implementieren wir diese Methode jetzt in unserer realen Repository-Klasse:

Hier fügen wir einfach eine neue Kategorie zu unserem Set hinzu.

Wenn wir einem DBSet <> eine Klasse hinzufügen, beginnt EF Core, alle Änderungen zu verfolgen, die an unserem Modell vorgenommen wurden, und verwendet diese Daten im aktuellen Status, um Abfragen zum Einfügen, Aktualisieren oder Löschen von Modellen zu generieren.

Die aktuelle Implementierung fügt das Modell einfach zu unserem Set hinzu, aber unsere Daten werden immer noch nicht gespeichert.

In der Kontextklasse ist eine Methode namens SaveChanges vorhanden, die aufgerufen werden muss, um die Abfragen in der Datenbank tatsächlich auszuführen. Ich habe es hier nicht aufgerufen, da ein Repository keine Daten beibehalten soll, sondern nur eine speicherinterne Sammlung von Objekten.

Dieses Thema ist selbst unter erfahrenen .NET-Entwicklern sehr umstritten. Lassen Sie mich jedoch erklären, warum Sie SaveChanges in Repository-Klassen nicht aufrufen sollten.

Wir können uns ein Repository konzeptionell als jede andere Sammlung vorstellen, die im .NET Framework vorhanden ist. Wenn Sie mit einer Sammlung in .NET (und vielen anderen Programmiersprachen wie Javascript und Java) arbeiten, können Sie im Allgemeinen:

  • Fügen Sie neue Elemente hinzu (z. B. wenn Sie Daten in Listen, Arrays und Wörterbücher verschieben).
  • Elemente suchen oder filtern;
  • Entfernen Sie ein Objekt aus der Sammlung.
  • Ersetzen Sie ein bestimmtes Element oder aktualisieren Sie es.

Denken Sie an eine Liste aus der realen Welt. Stellen Sie sich vor, Sie schreiben eine Einkaufsliste, um im Supermarkt einzukaufen (was für ein Zufall, nein?).

In die Liste schreiben Sie alle Früchte, die Sie kaufen müssen. Sie können dieser Liste Früchte hinzufügen, Früchte entfernen, wenn Sie den Kauf aufgeben, oder den Namen einer Frucht ersetzen. Sie können jedoch keine Früchte in der Liste speichern. Es macht keinen Sinn, so etwas in einfachem Englisch zu sagen.

Tipp: Verwenden Sie beim Entwerfen von Klassen und Schnittstellen in objektorientierten Programmiersprachen die natürliche Sprache, um zu überprüfen, ob Ihre Vorgehensweise korrekt zu sein scheint.
Es ist zum Beispiel sinnvoll zu sagen, dass ein Mann eine Personenschnittstelle implementiert, aber es ist nicht sinnvoll zu sagen, dass ein Mann einen Account implementiert.

Wenn Sie die Obstlisten „speichern“ möchten (in diesem Fall, um alle Früchte zu kaufen), bezahlen Sie diese und der Supermarkt verarbeitet die Bestandsdaten, um zu prüfen, ob sie mehr Früchte von einem Anbieter kaufen müssen oder nicht.

Dieselbe Logik kann beim Programmieren angewendet werden. Repositorys sollten keine Daten speichern, aktualisieren oder löschen. Stattdessen sollten sie es an eine andere Klasse delegieren, um diese Logik zu handhaben.

Beim direkten Speichern von Daten in ein Repository tritt ein anderes Problem auf: Sie können keine Transaktionen verwenden.

Stellen Sie sich vor, unsere Anwendung verfügt über einen Protokollierungsmechanismus, der einen bestimmten Benutzernamen und die Aktion speichert, die bei jeder Änderung an den API-Daten ausgeführt wird.

Stellen Sie sich nun vor, Sie haben aus irgendeinem Grund einen Anruf bei einem Dienst, der den Benutzernamen aktualisiert (dies ist kein allgemeines Szenario, aber wir sollten es in Betracht ziehen).

Sie stimmen zu, dass Sie, um den Benutzernamen in einer fiktiven Benutzertabelle zu ändern, zuerst alle Protokolle aktualisieren müssen, um zu erkennen, wer diese Operation durchgeführt hat, oder?

Stellen Sie sich vor, wir haben die Aktualisierungsmethode für Benutzer und Protokolle in verschiedenen Repositorys implementiert, und beide rufen SaveChanges auf. Was passiert, wenn eine dieser Methoden während des Aktualisierungsprozesses fehlschlägt? Es kommt zu Dateninkonsistenzen.

Wir sollten unsere Änderungen erst in der Datenbank speichern, wenn alles fertig ist. Dazu müssen wir eine Transaktion verwenden, eine Funktion, die die meisten Datenbanken implementieren, um Daten erst nach Abschluss einer komplexen Operation zu speichern.

"- Ok, wenn wir hier nichts speichern können, wo sollen wir es dann tun?"

Ein gängiges Muster zur Behebung dieses Problems ist das Muster der Arbeitseinheit. Dieses Muster besteht aus einer Klasse, die unsere AppDbContext-Instanz als Abhängigkeit empfängt und Methoden zum Starten, Abschließen oder Abbrechen von Transaktionen bereitstellt.

Wir werden eine einfache Implementierung einer Arbeitseinheit verwenden, um unser Problem hier anzugehen.

Fügen Sie im Ordner "Repositories" der Domänenebene eine neue Schnittstelle mit dem Namen "IUnitOfWork" hinzu:

Wie Sie sehen, wird nur eine Methode verfügbar gemacht, mit der Datenverwaltungsvorgänge asynchron abgeschlossen werden.

Fügen wir jetzt die eigentliche Implementierung hinzu.

Fügen Sie im RepositoriesRepositories-Ordner der Persistence-Ebene eine neue Klasse mit dem Namen UnitOfWork hinzu:

Dies ist eine einfache, saubere Implementierung, bei der alle Änderungen erst in der Datenbank gespeichert werden, nachdem Sie sie mithilfe Ihrer Repositorys geändert haben.

Wenn Sie nach Implementierungen des Musters "Unit of Work" suchen, werden Sie komplexere finden, die Rollback-Vorgänge implementieren.

Da EF Core das Repository-Muster und die Arbeitseinheit bereits im Hintergrund implementiert, müssen wir uns nicht um eine Rollback-Methode kümmern.

„- Was? Warum müssen wir all diese Schnittstellen und Klassen erstellen? “

Die Trennung der Persistenzlogik von den Geschäftsregeln bietet viele Vorteile hinsichtlich der Wiederverwendbarkeit und Wartung des Codes. Wenn wir EF Core direkt verwenden, werden wir komplexere Klassen haben, die nicht so einfach zu ändern sind.

Stellen Sie sich vor, Sie möchten in Zukunft das ORM-Framework auf ein anderes ändern, z. B. Dapper, oder Sie müssen aus Gründen der Leistung einfache SQL-Abfragen implementieren. Wenn Sie Ihre Abfragelogik mit Ihren Diensten koppeln, ist es schwierig, die Logik zu ändern, da Sie dies in vielen Klassen tun müssen.

Mithilfe des Repository-Musters können Sie einfach eine neue Repository-Klasse implementieren und mithilfe der Abhängigkeitsinjektion binden.

Wenn Sie EF Core direkt in Ihre Dienste einbinden und etwas ändern müssen, erhalten Sie im Grunde Folgendes:

Wie ich bereits sagte, implementiert EF Core die Muster "Unit of Work" und "Repository" hinter den Kulissen. Wir können unsere DbSet <> -Eigenschaften als Repositorys betrachten. Außerdem speichert SaveChanges die Daten nur im Erfolgsfall für alle Datenbankoperationen.

Nachdem Sie nun wissen, was eine Arbeitseinheit ist und warum Sie sie mit Repositorys verwenden müssen, implementieren wir die Logik des eigentlichen Service.

Dank unserer entkoppelten Architektur können wir eine Instanz von UnitOfWork einfach als Abhängigkeit für diese Klasse übergeben.

Unsere Geschäftslogik ist ziemlich einfach.

Zuerst wird versucht, die neue Kategorie zur Datenbank hinzuzufügen, und dann wird von der API versucht, sie zu speichern. Dabei wird alles in einen Try-Catch-Block eingeschlossen.

Wenn etwas fehlschlägt, ruft die API einen fiktiven Protokollierungsdienst auf und gibt eine Antwort zurück, die auf einen Fehler hinweist.

Wenn der Vorgang ohne Probleme abgeschlossen wird, gibt die Anwendung eine Erfolgsantwort zurück und sendet unsere Kategoriedaten. Einfach, richtig?

Tipp: In realen Anwendungen sollten Sie nicht alles in einen generischen Try-Catch-Block einschließen, sondern alle möglichen Fehler separat behandeln.
Durch einfaches Hinzufügen eines Try-Catch-Blocks werden die meisten möglichen Fehlerszenarien nicht abgedeckt. Achten Sie darauf, die Fehlerbehandlung des Geräts zu korrigieren.

Der letzte Schritt vor dem Testen unserer API besteht darin, die Unit-of-Work-Schnittstelle an die jeweilige Klasse zu binden.

Fügen Sie der ConfigureServices-Methode der Startup-Klasse diese neue Zeile hinzu:

services.AddScoped  ();

Jetzt testen wir es!

Schritt 14 - Testen unseres POST-Endpunkts mit Postman

Starten Sie unsere Anwendung erneut mit dotnet run.

Wir können den Browser nicht zum Testen eines POST-Endpunkts verwenden. Verwenden wir Postman, um unsere Endpunkte zu testen. Es ist ein sehr nützliches Tool zum Testen von RESTful-APIs.

Öffnen Sie Postman und schließen Sie die Intronachrichten. Sie sehen einen Bildschirm wie diesen:

Bildschirm mit Optionen zum Testen von Endpunkten

Ändern Sie das standardmäßig ausgewählte GET in das Auswahlfeld POST.

Geben Sie die API-Adresse in das Feld Anforderungs-URL eingeben ein.

Wir müssen die Daten des Anfragetexts bereitstellen, um sie an unsere API senden zu können. Klicken Sie auf den Menüpunkt Body und ändern Sie die darunter angezeigte Option in raw.

Der Postbote zeigt rechts eine Textoption an. Ändern Sie es in JSON (application / json) und fügen Sie die folgenden JSON-Daten ein:

{
  "Name": ""
}
Bildschirm kurz vor dem Senden einer Anfrage

Wie Sie sehen, senden wir eine leere Namenszeichenfolge an unseren neuen Endpunkt.

Klicken Sie auf die Schaltfläche Senden. Sie erhalten eine Ausgabe wie die folgende:

Wie Sie sehen, funktioniert unsere Validierungslogik!

Erinnern Sie sich an die Validierungslogik, die wir für den Endpunkt erstellt haben? Diese Ausgabe ist der Beweis, dass es funktioniert!

Beachten Sie auch den 400-Statuscode, der rechts angezeigt wird. Das Ergebnis von BadRequest fügt diesen Statuscode automatisch der Antwort hinzu.

Ändern wir nun die JSON-Daten in gültige, um die neue Antwort anzuzeigen:

Endlich das Ergebnis, das wir erwartet hatten

Die API hat unsere neue Ressource korrekt erstellt.

Bisher kann unsere API Kategorien auflisten und erstellen. Sie haben viel über die C # -Sprache, das ASP.NET Core-Framework und die gängigen Entwurfsansätze zum Strukturieren Ihrer APIs gelernt.

Fahren wir mit unserer Kategorien-API fort und erstellen den Endpunkt zum Aktualisieren von Kategorien.

Von nun an werde ich, da ich Ihnen die meisten Konzepte erklärt habe, die Erklärungen beschleunigen und mich auf neue Themen konzentrieren, um Ihre Zeit nicht zu verschwenden. Lass uns gehen!

Schritt 15 - Kategorien aktualisieren

Zum Aktualisieren von Kategorien benötigen wir einen HTTP-PUT-Endpunkt.

Die Logik, die wir codieren müssen, ist der POST-Logik sehr ähnlich:

  • Zuerst müssen wir die eingehende Anfrage mit ModelState validieren.
  • Wenn die Anforderung gültig ist, sollte die API die eingehende Ressource mithilfe von AutoMapper einer Modellklasse zuordnen.
  • Dann müssen wir unseren Service anrufen und ihm mitteilen, dass die Kategorie aktualisiert werden soll, wobei die entsprechende Kategorie-ID und die aktualisierten Daten anzugeben sind.
  • Wenn in der Datenbank keine Kategorie mit der angegebenen ID vorhanden ist, wird eine ungültige Anforderung zurückgegeben. Wir könnten stattdessen ein NotFound-Ergebnis verwenden, aber das spielt für diesen Bereich keine Rolle, da wir den Clientanwendungen eine Fehlermeldung zur Verfügung stellen.
  • Wenn die Speicherlogik korrekt ausgeführt wird, muss der Service eine Antwort mit den aktualisierten Kategoriedaten zurückgeben. Wenn nicht, sollte dies einen Hinweis darauf geben, dass der Prozess fehlgeschlagen ist, sowie eine Meldung, warum.
  • Wenn schließlich ein Fehler auftritt, gibt die API eine ungültige Anforderung zurück. Wenn nicht, ordnet es das aktualisierte Kategoriemodell einer Kategorieressource zu und gibt eine Erfolgsantwort an die Clientanwendung zurück.

Fügen wir der Controller-Klasse die neue PutAsync-Methode hinzu:

Wenn Sie es mit der POST-Logik vergleichen, werden Sie feststellen, dass wir hier nur einen Unterschied haben: Das HttPut-Attribut gibt einen Parameter an, den die angegebene Route erhalten soll.

Wir nennen diesen Endpunkt und geben die Kategorie-ID als letztes URL-Fragment an, z. B. / api / categories / 1. Die ASP.NET Core-Pipeline analysiert dieses Fragment auf den gleichnamigen Parameter.

Jetzt müssen wir die UpdateAsync-Methodensignatur in der ICategoryService-Schnittstelle definieren:

Gehen wir nun zur eigentlichen Logik über.

Schritt 16 - Die Aktualisierungslogik

Um unsere Kategorie zu aktualisieren, müssen wir zuerst die aktuellen Daten aus der Datenbank zurückgeben, falls vorhanden. Wir müssen es auch in unserem DBSet <> aktualisieren.

Fügen wir unserer ICategoryService-Schnittstelle zwei neue Methodenverträge hinzu:

Wir haben die FindByIdAsync-Methode, die asynchron eine Kategorie aus der Datenbank zurückgibt, und die Update-Methode definiert. Beachten Sie, dass die Update-Methode nicht asynchron ist, da die EF Core-API keine asynchrone Methode zum Aktualisieren von Modellen erfordert.

Implementieren wir nun die echte Logik in die CategoryRepository-Klasse:

Schließlich können wir die Servicelogik codieren:

Die API versucht, die Kategorie aus der Datenbank abzurufen. Wenn das Ergebnis null ist, geben wir eine Antwort zurück, die besagt, dass die Kategorie nicht existiert. Wenn die Kategorie existiert, müssen wir ihren neuen Namen festlegen.

Die API versucht dann, Änderungen zu speichern, beispielsweise wenn wir eine neue Kategorie erstellen. Wenn der Prozess abgeschlossen ist, gibt der Service eine Erfolgsantwort zurück. Wenn nicht, wird die Protokollierungslogik ausgeführt und der Endpunkt erhält eine Antwort mit einer Fehlermeldung.

Jetzt testen wir es. Fügen Sie zunächst eine neue Kategorie hinzu, um eine gültige ID zu verwenden. Wir könnten die Bezeichner der Kategorien verwenden, die wir unserer Datenbank zuordnen, aber ich möchte dies auf diese Weise tun, um Ihnen zu zeigen, dass unsere API die richtige Ressource aktualisieren wird.

Führen Sie die Anwendung erneut aus und POSTEN Sie mit Postman eine neue Kategorie in der Datenbank:

Hinzufügen einer neuen Kategorie, um sie später zu aktualisieren

Wenn Sie eine gültige ID in Händen haben, ändern Sie die Option POST in PUT im Auswahlfeld und fügen Sie den ID-Wert am Ende der URL hinzu. Ändern Sie die Namenseigenschaft in einen anderen Namen und senden Sie die Anfrage, um das Ergebnis zu überprüfen:

Die Kategoriedaten wurden erfolgreich aktualisiert

Sie können eine GET-Anforderung an den API-Endpunkt senden, um sicherzustellen, dass Sie den Kategorienamen korrekt bearbeitet haben:

Das ist das Ergebnis einer GET-Anfrage

Die letzte Operation, die wir für Kategorien implementieren müssen, ist das Ausschließen von Kategorien. Lassen Sie uns einen HTTP Delete-Endpunkt erstellen.

Schritt 17 - Löschen von Kategorien

Die Logik zum Löschen von Kategorien ist sehr einfach zu implementieren, da die meisten Methoden, die wir benötigen, zuvor erstellt wurden.

Dies sind die notwendigen Schritte für unseren Weg zur Arbeit:

  • Die API muss unseren Service anrufen und ihn anweisen, unsere Kategorie zu löschen und die entsprechende ID anzugeben.
  • Wenn in der Datenbank keine Kategorie mit der angegebenen ID vorhanden ist, sollte der Dienst eine Nachricht zurückgeben, die darauf hinweist.
  • Wenn die Löschlogik ohne Probleme ausgeführt wird, sollte der Service eine Antwort mit unseren gelöschten Kategoriedaten zurückgeben. Wenn nicht, sollte dies einen Hinweis darauf geben, dass der Prozess fehlgeschlagen ist, und eine potenzielle Fehlermeldung anzeigen.
  • Wenn schließlich ein Fehler auftritt, gibt die API eine ungültige Anforderung zurück. Wenn nicht, ordnet die API die aktualisierte Kategorie einer Ressource zu und gibt eine Erfolgsantwort an den Client zurück.

Beginnen wir mit dem Hinzufügen der neuen Endpunktlogik:

Das HttpDelete-Attribut definiert auch eine ID-Vorlage.

Bevor wir die DeleteAsync-Signatur zu unserer ICategoryService-Schnittstelle hinzufügen, müssen wir ein kleines Refactoring durchführen.

Die neue Dienstmethode muss eine Antwort mit den Kategoriedaten zurückgeben, genau wie bei den PostAsync- und UpdateAsync-Methoden. Wir könnten die SaveCategoryResponse für diesen Zweck wiederverwenden, aber in diesem Fall speichern wir keine Daten.

Um zu vermeiden, dass eine neue Klasse mit derselben Form erstellt wird, um diese Anforderung zu erfüllen, können wir unsere SaveCategoryResponse einfach in CategoryResponse umbenennen.

Wenn Sie Visual Studio Code verwenden, können Sie die SaveCategoryResponse-Klasse öffnen, den Mauszeiger über den Klassennamen setzen und die Option "Alle Vorkommen ändern" verwenden, um die Klasse umzubenennen:

Einfache Möglichkeit, den Namen in allen Dateien zu ändern

Vergessen Sie nicht, den Dateinamen ebenfalls umzubenennen.

Fügen Sie der ICategoryService-Schnittstelle die Signatur der DeleteAsync-Methode hinzu:

Vor der Implementierung der Löschlogik benötigen wir eine neue Methode in unserem Repository.

Fügen Sie der ICategoryRepository-Schnittstelle die Remove-Methodensignatur hinzu:

nichtig entfernen (Kategorie Kategorie);

Fügen Sie nun die eigentliche Implementierung für die Repository-Klasse hinzu:

EF Core erfordert, dass die Instanz unseres Modells an die Remove-Methode übergeben wird, um zu verstehen, welches Modell gelöscht wird, anstatt einfach eine ID zu übergeben.

Zum Schluss implementieren wir die Logik für die CategoryService-Klasse:

Hier gibt es nichts Neues. Der Service versucht die Kategorie anhand der ID zu finden und ruft dann unser Repository auf, um die Kategorie zu löschen. Schließlich schließt die Arbeitseinheit die Transaktion ab, die die reale Operation in der Datenbank ausführt.

„- Hey, aber wie steht es mit den Produkten jeder Kategorie? Müssen Sie nicht zuerst ein Repository erstellen und die Produkte löschen, um Fehler zu vermeiden? "

Die Antwort ist nein. Dank des EF Core-Verfolgungsmechanismus weiß das Framework beim Laden eines Modells aus der Datenbank, welche Beziehungen das Modell hat. Wenn wir es löschen, weiß EF Core, dass alle zugehörigen Modelle zuerst rekursiv gelöscht werden sollten.

Diese Funktion kann deaktiviert werden, wenn unsere Klassen Datenbanktabellen zugeordnet werden. Sie ist jedoch für dieses Lernprogramm nicht verfügbar. Werfen Sie einen Blick hier, wenn Sie mehr über diese Funktion erfahren möchten.

Jetzt ist es Zeit, unseren neuen Endpunkt zu testen. Führen Sie die Anwendung erneut aus und senden Sie eine DELETE-Anforderung mit Postman wie folgt:

Wie Sie sehen, hat die API die vorhandene Kategorie ohne Probleme gelöscht

Wir können überprüfen, ob unsere API korrekt funktioniert, indem wir eine GET-Anfrage senden:

Jetzt erhalten wir nur noch eine Kategorie

Wir haben die Categories-API abgeschlossen. Jetzt ist es an der Zeit, zur Produkt-API zu wechseln.

Schritt 18 - Die Produkt-API

Bisher haben Sie gelernt, wie Sie alle grundlegenden HTTP-Verben implementieren, um CRUD-Vorgänge mit ASP.NET Core zu verarbeiten. Gehen wir zur nächsten Stufe der Implementierung unserer Produkt-API.

Ich werde nicht noch einmal alle HTTP-Verben detaillieren, da dies erschöpfend wäre. Im letzten Teil dieses Lernprogramms werde ich nur die GET-Anforderung behandeln, um zu zeigen, wie verwandte Entitäten bei der Abfrage von Daten aus der Datenbank einbezogen werden und wie die von uns für die EUnitOfMeasurement-Aufzählungswerte definierten Beschreibungsattribute verwendet werden.

Fügen Sie im Ordner "Controllers" einen neuen Controller mit dem Namen "ProductsController" hinzu.

Bevor wir hier etwas codieren, müssen wir die Produktressource erstellen.

Lassen Sie mich Ihr Gedächtnis auffrischen und zeigen, wie unsere Ressource aussehen sollte:

{
 [
  {
   "id": 1,
   "Name": "Zucker",
   "QuantityInPackage": 1,
   "unitOfMeasurement": "KG"
   "Kategorie": {
   "id": 3,
   "Name": "Zucker"
   }
  },
  … // Andere Produkte
 ]
}

Wir möchten ein JSON-Array, das alle Produkte aus der Datenbank enthält.

Die JSON-Daten unterscheiden sich vom Produktmodell in zwei Punkten:

  • Die Maßeinheit wird kürzer angezeigt und zeigt nur die Abkürzung an.
  • Wir geben die Kategoriedaten ohne die CategoryId-Eigenschaft aus.

Zur Darstellung der Maßeinheit können wir anstelle eines Aufzählungstyps eine einfache Zeichenfolgeneigenschaft verwenden (für JSON-Daten haben wir übrigens keinen Standardaufzählungstyp, daher müssen wir ihn in einen anderen Typ umwandeln).

Nachdem wir nun wissen, wie Sie die neue Ressource gestalten, erstellen wir sie. Fügen Sie eine neue Klasse ProductResource in den Ordner Resources ein:

Jetzt müssen wir die Zuordnung zwischen der Modellklasse und unserer neuen Ressourcenklasse konfigurieren.

Die Zuordnungskonfiguration ist nahezu identisch mit der Konfiguration für andere Zuordnungen, hier müssen wir jedoch die Umwandlung unserer EUnitOfMeasurement-Enumeration in eine Zeichenfolge vornehmen.

Erinnern Sie sich an das StringValue-Attribut, das auf die Aufzählungstypen angewendet wurde? Jetzt zeige ich Ihnen, wie Sie diese Informationen mithilfe einer leistungsstarken Funktion von .NET Framework extrahieren: der Reflection-API.

Die Reflection-API ist ein leistungsstarker Satz von Ressourcen, mit denen wir Metadaten extrahieren und bearbeiten können. Viele Frameworks und Bibliotheken (einschließlich ASP.NET Core selbst) verwenden diese Ressourcen, um viele Dinge hinter den Kulissen zu erledigen.

Nun wollen wir sehen, wie es in der Praxis funktioniert. Fügen Sie dem Extensions-Ordner eine neue Klasse mit dem Namen EnumExtensions hinzu.

Es mag erschreckend erscheinen, wenn Sie sich den Code zum ersten Mal ansehen, aber es ist nicht so komplex. Lassen Sie uns die Codedefinition aufschlüsseln, um zu verstehen, wie es funktioniert.

Zuerst haben wir eine generische Methode definiert (eine Methode, die mehr als einen Argumenttyp empfangen kann, in diesem Fall dargestellt durch die TEnum-Deklaration), die eine bestimmte Aufzählung als Argument empfängt.

Da enum in C # ein reserviertes Schlüsselwort ist, haben wir vor dem Namen des Parameters ein @ eingefügt, um ihn zu einem gültigen Namen zu machen.

Der erste Ausführungsschritt dieser Methode besteht darin, die Typinformationen (Klasse, Schnittstelle, Aufzählung oder Strukturdefinition) des Parameters mithilfe der GetType-Methode abzurufen.

Anschließend ruft die Methode den spezifischen Aufzählungswert (z. B. Kilogramm) mit GetField (@ enum.ToString ()) ab.

In der nächsten Zeile werden alle Beschreibungsattribute aufgeführt, die auf den Aufzählungswert angewendet wurden, und ihre Daten werden in einem Array gespeichert (in einigen Fällen können mehrere Attribute für dieselbe Eigenschaft angegeben werden).

In der letzten Zeile wird eine kürzere Syntax verwendet, um zu überprüfen, ob mindestens ein Beschreibungsattribut für den Aufzählungstyp vorhanden ist. Wenn dies der Fall ist, geben wir den von diesem Attribut bereitgestellten Wert Description zurück. Wenn nicht, geben wir die Aufzählung als Zeichenfolge zurück, wobei das Standard-Casting verwendet wird.

Das ?. operator (ein nullbedingter Operator) prüft, ob der Wert null ist, bevor auf seine Eigenschaft zugegriffen wird.

Das ?? operator (ein Null-Koaleszenz-Operator) weist die Anwendung an, den Wert auf der linken Seite zurückzugeben, wenn er nicht leer ist, oder den Wert auf der rechten Seite, wenn er nicht leer ist.

Nachdem wir nun eine Erweiterungsmethode zum Extrahieren von Beschreibungen haben, konfigurieren wir unsere Zuordnung zwischen Modell und Ressource. Dank AutoMapper können wir dies mit nur einer zusätzlichen Zeile tun.

Öffnen Sie die ModelToResourceProfile-Klasse und ändern Sie den Code folgendermaßen:

Diese Syntax weist AutoMapper an, die neue Erweiterungsmethode zu verwenden, um unseren EUnitOfMeasurement-Wert in eine Zeichenfolge mit seiner Beschreibung zu konvertieren. Einfach, richtig? Sie können die offizielle Dokumentation lesen, um die vollständige Syntax zu verstehen.

Beachten Sie, dass für die Kategorieeigenschaft keine Zuordnungskonfiguration definiert wurde. Da wir die Zuordnung zuvor für Kategorien konfiguriert haben und das Produktmodell eine Kategorieeigenschaft des gleichen Typs und Namens hat, weiß AutoMapper implizit, dass es sie mit der entsprechenden Konfiguration zuordnen sollte.

Fügen wir nun den Endpunktcode hinzu. Ändern Sie den ProductsController-Code:

Grundsätzlich wird die gleiche Struktur für den Categories Controller definiert.

Gehen wir zum Serviceteil. Fügen Sie eine neue IProductService-Schnittstelle in den Ordner "Services" ein, der auf der Domänenebene vorhanden ist:

Sie hätten erkennen müssen, dass wir ein Repository benötigen, bevor der neue Service wirklich implementiert werden kann.

Fügen Sie eine neue Schnittstelle namens IProductRepository in den entsprechenden Ordner ein:

Implementieren wir nun das Repository. Wir müssen es fast genauso implementieren, wie wir es für das Categories-Repository getan haben, außer dass wir die jeweiligen Kategoriedaten jedes Produkts zurückgeben müssen, wenn wir Daten abfragen.

EF Core enthält standardmäßig keine verwandten Entitäten zu Ihren Modellen, wenn Sie Daten abfragen, da dies sehr langsam sein kann (stellen Sie sich ein Modell mit zehn verwandten Entitäten vor, wobei alle verwandten Entitäten ihre eigenen Beziehungen haben).

Um die Kategoriedaten aufzunehmen, benötigen wir nur eine zusätzliche Zeile:

Beachten Sie den Aufruf von Include (p => p.Category). Wir können diese Syntax verketten, um beim Abfragen von Daten so viele Entitäten wie nötig einzuschließen. EF Core übersetzt es bei der Auswahl in einen Join.

Jetzt können wir die ProductService-Klasse genauso implementieren wie für Kategorien:

Binden wir die neuen Abhängigkeiten, indem wir die Startup-Klasse ändern:

Ändern Sie vor dem Testen der API die AppDbContext-Klasse so, dass beim Initialisieren der Anwendung einige Produkte einbezogen werden, damit die Ergebnisse angezeigt werden:

Ich habe zwei fiktive Produkte hinzugefügt, die sie den Kategorien zuordnen, die wir beim Initialisieren der Anwendung festgelegt haben.

Zeit zum Testen! Führen Sie die API erneut aus und senden Sie mit Postman eine GET-Anfrage an / api / products:

Voilà! Hier sind unsere Produkte

Und das ist es! Herzliche Glückwünsche!

Jetzt haben Sie eine Grundlage zum Erstellen einer RESTful-API mit ASP.NET Core unter Verwendung einer entkoppelten Architektur. Sie haben viel über das .NET Core-Framework gelernt, wie man mit C # arbeitet, die Grundlagen von EF Core und AutoMapper und viele nützliche Muster, die Sie beim Entwerfen Ihrer Anwendungen verwenden können.

Sie können die vollständige Implementierung der API überprüfen, die die anderen HTTP-Verben für Produkte enthält, und das Github-Repository überprüfen:

Fazit

ASP.NET Core ist ein hervorragendes Framework zum Erstellen von Webanwendungen. Es enthält viele nützliche APIs, mit denen Sie saubere, wartbare Anwendungen erstellen können. Betrachten Sie es als eine Option, wenn Sie professionelle Anwendungen erstellen.

In diesem Artikel wurden nicht alle Aspekte einer professionellen API behandelt, aber Sie haben alle Grundlagen erlernt. Sie haben auch viele nützliche Muster gelernt, um Muster zu lösen, mit denen wir täglich konfrontiert sind.

Ich hoffe, Ihnen hat dieser Artikel gefallen und ich hoffe, er hat Ihnen geholfen. Ich freue mich über Ihr Feedback, um zu verstehen, wie ich dies verbessern kann.

Hinweise zum Weiterlernen

.NET Core-Lernprogramme - Microsoft Docs

ASP.NET-Kerndokumentation - Microsoft Docs