März 2018

Band 33, Nummer 3

Cutting Edge: REST und Web-API in ASP.NET Core

Von Dino Esposito | März 2018

Dino EspositoIch war noch nie ein Freund von ASP.NET-Web-API als eigenständiges Framework, und ich kann mich kaum an ein Projekt erinnern, in dem ich es verwendet habe. Damit meine ich nicht, dass das Framework an sich fehl am Platz oder unnötig wäre. Ich finde nur, dass der Geschäftsnutzen, den es tatsächlich liefert, meistens minimal ist. Andererseits erkenne ich darin einige deutliche Anzeichen für die zugrunde liegenden Bemühungen von Microsoft, die ASP.NET-Runtimepipeline zu erneuern. Insgesamt möchte ich ASP.NET-Web-API als Proof of Concept für das ansehen, was aktuell zu ASP.NET Core und insbesondere zur neuen Laufzeitumgebung von ASP.NET Core geworden ist.

Die Web-API wurde in erster Linie eingeführt, um das Erstellen einer RESTful-API in ASP.NET einfach und komfortabel zu gestalten. In diesem Artikel geht es darum, wie das gleiche Ergebnis (das Erstellen einer RESTful-API) in ASP.NET Core erreicht werden kann.

Die zusätzlichen Kosten von Web-API in klassischem ASP.NET

ASP.NET-Web API basiert auf den Prinzipien der OWIN-Spezifikation (Open Web Interface für .NET), die den Webserver von gehosteten Webanwendungen entkoppeln soll. Im .NET-Umfeld markierte die Einführung von OWIN einen Wendepunkt, an dem die enge Integration von IIS und ASP.NET in Frage gestellt wurde. Diese enge Kopplung wurde in ASP.NET Core vollständig aufgegeben.

Jede Webanwendung, die mit dem ASP.NET-Web-API-Framework erstellt wird, basiert auf einer vollständig neu geschriebenen Pipeline, die die OWIN-Standardschnittstelle für den Dialog mit dem zugrunde liegenden Hostwebserver verwendet. Eine ASP.NET-Web-API ist jedoch keine eigenständige Anwendung. Um für Aufrufer verfügbar zu sein, benötigt sie eine Hostumgebung, die sich um das Lauschen an einem konfigurierten Port kümmert, eingehende Anforderungen erfasst und diese über die Web-API-Pipeline weiterleitet.

Eine Web-API-Anwendung kann in einem Windows-Dienst oder in einer benutzerdefinierten Konsolenanwendung gehostet werden, die die entsprechenden OWIN-Schnittstellen implementiert. Sie kann auch von einer klassischen ASP.NET-Anwendung gehostet werden, und zwar unabhängig davon, ob das Ziel Web Forms oder ASP.NET MVC ist. In den letzten Jahren erwies sich das Hosten von Web-API in einer klassischen ASP.NET MVC-Anwendung als ein sehr häufiges Szenario, jedoch eines der am wenigsten effektiven in Bezug auf die Rohleistung und den Speicherbedarf.

Wie Abbildung 1 zeigt, sind beim Hosten einer Web-API-Anwendung in einer ASP.NET MVC-Anwendung drei Frameworks parallel vorhanden und verarbeiten jede einzelne Web-API-Anforderung. Die ASP.NET MVC-Hostanwendung ist in einem HTTP-Handler gekapselt, der auf „system.web“ (der ursprünglichen ASP.NET-Laufzeitumgebung) aufsetzt. Zu dem allem kommt noch die OWIN-basierte Pipeline von Web-API, die zusätzlichen Speicherplatz beansprucht.

Frameworks, die in eine klassische ASP.NET-Web-API-Anwendung eingebunden sind
Abbildung 1: Frameworks, die in eine klassische ASP.NET-Web-API-Anwendung eingebunden sind

Die Vision der Einführung eines serverunabhängigen Webframeworks wird in diesem Fall durch die Zwänge der Kompatibilität mit der vorhandenen ASP.NET-Pipeline erheblich abgeschwächt. Daher entfaltet der klare und REST-freundliche Entwurf von Web-API aufgrund der Legacyassembly „system.web“ nicht sein volles Potenzial. Aus einer reiner Leistungsperspektive rechtfertigen nur wenige Grenzanwendungsfälle den Einsatz von Web-API.

Effektive Anwendungsfälle für Web-API

Web-API ist das bekannteste Beispiel für die OWIN-Prinzipien in Aktion. Eine Web-API-Bibliothek wird hinter einer Serveranwendung ausgeführt, die eingehende Anforderungen erfasst und weiterleitet. Dieser Host kann eine klassische Webanwendung auf dem Microsoft-Stapel (Web Forms, ASP.NET MVC) oder eine Konsolenanwendung bzw. ein Windows-Dienst sein.

In jedem Fall muss es sich um eine Anwendung handeln, die mit einer dünnen Codeschicht ausgestattet ist, die in der Lage ist, mit dem Web-API-Listener zu kommunizieren.

Das Hosten einer Web-API außerhalb der Webumgebung entfernt jede Abhängigkeit von der Assembly „system.web“ an der Wurzel und erzielt so fast magisch die gewünschte schlanke und effektive Anforderungspipeline.

Dies ist der entscheidende Punkt, der das ASP.NET Core-Team dazu veranlasst hat, die ASP.NET Core-Pipeline zu erstellen. Die idealen Hostingbedingungen für Web-API wurden überarbeitet, um die idealen Hostingbedingungen für nahezu jede ASP.NET Core-Anwendung zu schaffen. Dies ermöglichte eine völlig neue Pipeline ohne Abhängigkeiten von der Assembly „system.web“, die hinter einem eingebetteten HTTP-Server gehostet werden kann, der eine kontrahierte Schnittstelle (die IServer-Schnittstelle) bereitstellt.

Die OWIN-Spezifikation und Katana, die Implementierung dieser Spezifikation für die IIS-/ASP.NET-Umgebung, spielen in ASP.NET Core keine Rolle. Aber die Erfahrung mit diesen Plattformen trug zur Reife der technischen Vision bei (vor allem bei Web-API-Grenzfällen), die durch die beeindruckende neue Pipeline von ASP.NET Core durchscheint.

Das Komische daran ist, dass die gleiche Web-API als separates Framework nicht mehr relevant ist, sobald die gesamte ASP.NET-Pipeline überarbeitet wurde (inspiriert von der idealen Hostingumgebung für Web-API). In der neuen ASP.NET Core-Pipeline wird nur ein einziges Anwendungsmodell benötigt (das MVC Anwendungsmodell), das auf Controllern basiert, und Controllerklassen sind etwas reichhaltiger als im klassischen ASP.NET MVC, wodurch die Funktionen von alten ASP.NET-Controllern und Web-API-Controllern integriert werden.

Erweiterte ASP.NET Core-Controller

In ASP.NET Core arbeiten Sie mit Controllerklassen, und zwar unabhängig davon, ob Sie HTML oder irgendeine andere Art von Antwort (z.B. JSON oder PDF) bereitstellen möchten. Eine Reihe neuer Ergebnistypen für Aktionen wurde hinzugefügt, um die Erstellung von RESTful-Schnittstellen einfach und bequem zu gestalten. Die Aushandlung von Inhalten wird für alle Controllerklassen vollständig unterstützt, und Formatierungshilfsprogramme wurden in die Aktionsaufruferinfrastruktur eingearbeitet. Wenn Sie eine Web-API erstellen möchten, die HTTP-Endpunkte bereitstellt, müssen Sie lediglich eine einfache Controllerklasse wie hier gezeigt erstellen:

public class ApiController : Controller
{
  // Your methods here
}

Der Name der Controllerklasse ist frei wählbar. Obwohl es aus Gründen der Klarheit wünschenswert ist, /api irgendwo in der URL zu verwenden, ist dies keineswegs erforderlich. Sie können /api in der URL verwenden, die aufgerufen wird, wenn Sie konventionelles Routing (eine ApiController-Klasse) verwenden, um URLs Aktionsmethoden zuzuordnen, oder wenn Sie Attributrouting nutzen. Meiner persönlichen Meinung nach ist Attributrouting wahrscheinlich vorzuziehen, weil es Ihnen erlaubt, mehrere Endpunkte mit dem gleichen /api-Element in der URL bereitzustellen, während es in verschiedenen, beliebig benannten Controllerklassen definiert ist.

Die Controllerklasse in ASP.NET Core weist wesentlich mehr Features auf als die Klasse in klassischem ASP.NET MVC, und die meisten Erweiterungen beziehen sich auf das Erstellen einer RESTful-Web-API. Zuerst und vor allem unterstützen alle ASP.NET Core-Controller die Aushandlung von Inhalten. Aushandlung von Inhalten bezeichnet eine stille Aushandlung zwischen dem Aufrufer und der API über das tatsächliche Format der zurückgegebenen Daten.

Die Aushandlung von Inhalten findet nicht immer und für jede Anforderung statt. Sie erfolgt nur, wenn die eingehende Anforderung einen Accept-HTTP-Header enthält, der die MIME-Typen ankündigt, die der Aufrufer verstehen kann. In diesem Fall durchläuft die ASP.NET-Core-Infrastruktur die im Headerinhalt aufgeführten Typen, bis sie einen Typ findet, für den in der aktuellen Konfiguration der Anwendung ein Formatierer vorhanden ist. Wenn in der Liste der Typen kein passender Formatierer gefunden wird, wird der standardmäßige JSON-Formatierer wie hier gezeigt verwendet:

[HttpGet]
public ObjectResult Get(Guid id)
{
  // Do something here to retrieve the resource data
  var data = FindResourceDataInSomeWay(id);
  return Ok(data);
}

Ein weiterer bemerkenswerter Aspekt der Aushandlung von Inhalten ist, dass sie zwar keine Änderung im Serialisierungsprozess ohne Accept-HTTP-Header bewirkt, technisch aber nur dann ausgelöst wird, wenn die Antwort, die vom Controller zurückgesendet wird, vom Typ ObjectResult ist. Der häufigste Weg, einen ObjectResult-Aktionsergebnistyp zurückzugeben, ist die Serialisierung der Antwort über die Ok-Methode. Beachten Sie unbedingt Folgendes: Wenn Sie die Antwort des Controllers z.B. über die Json-Methode serialisieren, findet unabhängig von den gesendeten Headern keine Aushandlung statt. Unterstützung für Ausgabeformatierer kann programmgesteuert über die Optionen der AddMvc-Methode hinzugefügt werden. Im Folgenden finden Sie ein Beispiel:

services.AddMvc(options =>
{
  options.OutputFormatters.Add(new PdfFormatter());
});

In diesem Beispiel enthält die PdfFormatter-Demoklasse intern die Liste der unterstützten MIME-Typen, die verarbeitet werden können.

Beachten Sie, dass Sie (wie hier gezeigt) mit dem Produces-Attribut die Aushandlung von Inhalten überschreiben können:

[Produces("application/json")]
public class ApiController : Controller
{
  // Action methods here
}

Das Produces-Attribut, das Sie auf Controller- oder Methodenebene anwenden können, erzwingt, dass die Ausgabe vom Typ ObjectResult immer in das durch das Attribut angegebene Format serialisiert wird, und zwar unabhängig vom Accept-HTTP-Header.

Weitere Informationen zum Formatieren der Antwort einer Controllermethode finden Sie unter bit.ly/2klDgdY.

REST-orientierte Aktionsergebnistypen

Ob eine Web-API mit einem REST-Entwurf die bessere Wahl ist, ist ein höchst fragwürdiger Punkt. Allgemein lässt sich mit Sicherheit sagen, dass der REST-Ansatz auf einem bekannten Regelwerk basiert und in dieser Hinsicht eher standardisiert ist. Aus diesem Grund wird er üblicherweise für eine öffentliche API empfohlen, die Teil des Unternehmensgeschäfts ist. Wenn die API nur für eine begrenzte Anzahl von Clients vorgesehen ist (größtenteils unter der gleichen Kontrolle der API-Ersteller), gibt es keinen wirklichen Geschäftsunterschied zwischen der Verwendung der REST-Entwurfsroute und einem lockereren Ansatz mit Remoteprozeduraufrufen (RPC).

In ASP.NET Core gibt es kein eigenständiges und dediziertes Web-API-Framework. Es gibt nur Controller mit ihren Aktionsergebnissen und Hilfsmethoden. Wenn Sie eine beliebige Web-API erstellen möchten, geben Sie einfach JSON, XML oder was auch immer zurück. Wenn Sie eine RESTful-API erstellen möchten, machen Sie sich einfach mit einer Reihe von Aktionsergebnissen und Hilfsmethoden vertraut. Abbildung 2 zeigt die neuen Aktionsergebnistypen, die von ASP.NET Core-Controllern zurückgegeben werden können. In ASP.NET Core ist ein Aktionsergebnistyp ein Typ, der die IActionResult-Schnittstelle implementiert. 

Abbildung 2: Ergebnistypen für Web-API-bezogene Aktionen

Typ Beschreibung
AcceptedResult Gibt einen 202-Statuscode zurück. Zusätzlich wird der URI zurückgegeben, um den aktuellen Status der Anforderung zu überprüfen. Der URI wird im Location-Header gespeichert.
BadRequestResult Gibt einen 400-Statuscode zurück.
CreatedResult Gibt einen 201-Statuscode zurück. Außerdem wird der URI der erstellten Ressource zurückgegeben, der im Location-Header gespeichert ist.
NoContentResult Gibt einen 204-Statuscode und Nullinhalt zurück.
OkResult Gibt einen 200-Statuscode zurück.
UnsupportedMediaTypeResult Gibt einen 415-Statuscode zurück.

Beachten Sie, dass einige der Typen in Abbildung 2 durch Buddytypen ergänzt werden, die die gleiche Kernfunktion (jedoch mit einigen kleinen Unterschieden) bieten. Beispielsweise finden Sie neben AcceptedResult und CreatedResult auch die Typen xxxAtActionResult und xxxAtRouteResult. Der Unterschied besteht darin, wie die Typen den URI ausdrücken, um den Status des akzeptierten Vorgangs und den Speicherort der soeben erstellten Ressource zu überwachen. Der xxxAtAtActionResult-Typ drückt den URI als Paar von Controller- und Aktionszeichenfolgen aus, während der xxxAtRouteResult-Typ einen Routennamen verwendet.

OkObjectResult und BadRequestObjectResult verfügen stattdessen über eine xxxObjectResult-Variante. Der Unterschied besteht darin, dass Objektergebnistypen es auch erlauben, ein Objekt an die Antwort anzufügen. OkResult legt also nur einen 200-Statuscode fest, OkObjectResult legt hingegen einen 200-Statuscode fest und fügt ein Objekt Ihrer Wahl an. Eine gängige Methode zur Verwendung dieses Features ist die Rückgabe eines ModelState-Wörterbuchs, das mit dem erkannten Fehler aktualisiert wurde, wenn eine fehlerhafte Anforderung verarbeitet wird.

Eine weitere interessante Unterscheidung besteht zwischen NoContentResult und EmptyResult. Beide geben eine leere Antwort zurück, aber NoContentResult legt einen Statuscode von 204 fest, während EmptyResult einen Statuscode von 200 festlegt. Vor dem Hintergrund des Gesagten ist das Erstellen einer RESTful-API ist eine Frage der Definition der Ressource, für die Aktionen ausgeführt werden, und der Anordnung einer Reihe von Aufrufen mithilfe des HTTP-Verbs, um allgemeine Bearbeitungsvorgänge auszuführen. Sie verwenden GET zum Lesen, PUT zum Aktualisieren, POST zum Erstellen einer neuen Ressource und DELETE zum Entfernen einer vorhandenen Ressource. Abbildung 3 zeigt das Skelett einer RESTful-Schnittstelle um einen Beispielressourcentyp, das sich aus ASP.NET Core-Klassen ergibt.

Abbildung 3: Allgemeines RESTful-Codeskelett

[HttpGet]
public ObjectResult Get(Guid id)
{
  // Do something here to retrieve the resource
  var res = FindResourceInSomeWay(id);
  return Ok(res);
}
[HttpPut]
public AcceptedResult UpdateResource(Guid id, string content)
{
  // Do something here to update the resource
  var res = UpdateResourceInSomeWay(id, content);
  var path = String.Format("/api/resource/{0}", res.Id);
  return Accepted(new Uri(path));  
}
[HttpPost]
public CreatedResult AddNews(MyResource res)
{
  // Do something here to create the resource
  var resId = CreateResourceInSomeWay(res);
  // Returns HTTP 201 and sets the URI to the Location header
  var path = String.Format("/api/resource/{0}", resId);
  return Created(path, res);
}
[HttpDelete]
public NoContentResult DeleteResource(Guid id)
{
  // Do something here to delete the resource
  // ...
  return NoContent();
}

Wenn Sie daran interessiert sind, die Implementierung von ASP.NET Core-Controllern für die Erstellung einer Web-API weiter zu untersuchen, schauen Sie sich den GitHub-Ordner unter bit.ly/2j4nyUe an.

Zusammenfassung

Eine Web-API ist heute in den meisten Anwendungen ein gängiges Element. Sie wird verwendet, um Daten für ein Angular- oder MVC-Front-End sowie Dienste für mobile oder Desktopanwendungen bereitzustellen. Im Kontext von ASP.NET Core erreicht der Begriff „Web-API“ endlich seine eigentliche Bedeutung ohne Zweideutigkeit oder die Notwendigkeit, seine Konturen weiter zu erläutern. Eine Web-API ist eine programmgesteuerte Schnittstelle, die eine Reihe öffentlich bereitgestellter HTTP-Endpunkte umfasst, die typischerweise (aber nicht notwendigerweise) JSON- oder XML-Daten an Aufrufer zurückgeben. Die Controllerinfrastruktur in ASP.NET Core unterstützt diese Vision mit einer überarbeiteten Implementierung und neuen Aktionsergebnistypen. Das Erstellen einer RESTful-API in ASP.NET war noch nie so einfach wie heute!


Dino Espositoist Autor von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press, 2014) und „Programming ASP.NET Core“ (Microsoft Press, 2018). Als Pluralsight-Autor und Developer Advocate bei JetBrains teilt er seine Vision von Software auf Twitter: @despos.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Ugo Lattanzi


Diesen Artikel im MSDN Magazine-Forum diskutieren