Innovation

Content Negotiation und die Web-API für ASP.NET MVC-Entwickler

Dino Esposito

Dino EspositoEiner der größten Vorteile, den ich bei ASP.NET MVC sehe, ist die Bereitstellung von Methoden, die von HTTP-Clients ganz einfach aufgerufen werden können, wie zum Beispiel jQuery-basierte Seiten, mobile Apps und einfache C#-Back-Ends. Lange Zeit fand die Erstellung dieser Dienstschicht innerhalb WCF(Windows Communication Foundation)-Dienste statt. Es wurde zwar versucht, WCF für HTTP zu spezialisieren, zum Beispiel durch den webHttpBinding-Mechanismus und durch Frameworks wie dem inzwischen eingestellten REST Starter Kit. Keiner dieser Versuche konnte jedoch die Hürden vollständig ausräumen, auf die die Entwickler regelmäßig stießen, wie z. B. die notorische Überkonfiguration von WCF, der übermäßige Gebrauch von Attributen und eine Struktur, die nicht speziell zum Testen ausgelegt war. Dann kam die Web-API – ein neues schlankes, testfähiges, von der Hostingumgebung (z. B. IIS) unabhängiges Framework, das HTTP-zentriert ist.

Die Programmierschnittstelle der Web-API ähnelt meiner Ansicht nach allerdings sehr stark (fast zu stark) dem ASP.NET MVC-Framework. Das ist aber nicht negativ gemeint, da ASP.NET MVC eine saubere und gut definierte Programmierschnittstelle aufweist. Am Anfang ähnelte das Programmiermodell der Web-API eher WCF und begann sich dann immer mehr in Richtung ASP.NET MVC zu entwickeln.

In diesem Artikel möchte ich die Web-API aus der Perspektive eines normalen ASP.NET MVC-Entwicklers betrachten und den Fokus auf einen Funktionsbereich der Web-API legen, der Vorteile gegenüber dem einfachen ASP.NET MVC-Framework bietet: die Content Negotiation.

Die Web-API auf einen Blick

Die Web-API ist ein Framework, mit dem Sie eine Bibliothek von Klassen erstellen können, die HTTP-Anforderungen verarbeiten. Die erstellte Bibliothek kann mit einigen anfänglichen Konfigurationseinstellungen in einer Laufzeitumgebung gehostet und von Aufrufern über HTTP verwendet werden. Öffentliche Methoden in Controllerklassen werden zu HTTP-Endpunkten. Konfigurierbare Routingregeln helfen bei der Definition der URL-Formate, mit denen auf spezifische Methoden zugegriffen wird. Mit Ausnahme des Routings wird die Standardform der URL-Verarbeitung in der Web-API jedoch eher durch Konvention als durch Konfiguration bestimmt.

Wenn Sie ASP.NET MVC-Entwickler sind, fragen Sie sich an dieser Stelle vielleicht, wieso Sie denn ein neues Framework verwenden sollten, das anscheinend das gleiche Controllerkonzept wie ASP.NET MVC aufweist.

Die kurze Antwort darauf ist: Sie benötigen wahrscheinlich in der Tat keine Web-API in ASP.NET MVC, da Sie fast die gleiche Funktionalität auch über einfache Controller erzielen können. Sie können zum Beispiel ganz einfach Daten als formatierte JSON- oder XML-Zeichenfolgen zurückgeben. Sie können auch ganz leicht binäre Daten oder Nur-Text zurückgeben. Und Sie können die URL-Vorlagen so gestalten, wie Sie dies bevorzugen.

Sie können die gleiche Controllerklasse für JSON-Daten oder eine HTML-Ansicht verwenden, und Sie können Controller, die HTML zurückgeben, ganz einfach von Controllern trennen, die nur Daten zurückgeben. Es ist in der Tat eine übliche Praxis, in Projekten eine ApiController-Klasse zu verwenden, der alle Endpunkte zugeordnet werden, die reine Daten zurückgeben. Im Folgenden finden Sie ein Beispiel:

public class ApiController : Controller {
public ActionResult Customers()
{
  var data = _repository.GetAllCustomers();
  return Json(data, JsonRequestBehavior.AllowGet);  }
  …
}

Die Web-API nutzt das Beste der ASP.NET MVC-Architektur und verbessert sie in zwei wichtigen Bereichen. Zunächst einmal führt sie eine neue logische Schicht, die als Content Negotiation bezeichnet wird, mit einem Standardsatz von Regeln ein, um Daten in einem bestimmten Format anzufordern, zum Beispiel JSON, XML oder ein anderes Format. Außerdem weist die Web-API keinerlei Abhängigkeiten von ASP.NET und IIS auf: Genauer gesagt hängt sie nicht von der system.web.dll-Bibliothek ab. Natürlich kann sie aber in einer ASP.NET-Anwendung unter IIS gehostet werden. Dies bleibt vermutlich das häufigste Szenario, jedoch kann eine Web-API-Bibliothek auch in jeder anderen Anwendung gehostet werden, die eine Ad-hoc-Hostingumgebung bietet, wie zum Beispiel ein Windows-Dienst, eine WPF(Windows Presentation Foundation)-Anwendung oder eine Konsolenanwendung.

Außerdem werden Sie, wenn Sie ein erfahrener ASP.NET MVC-Entwickler sind, mit den Web-API-Konzepten wie Controller, Modellbindung, Routing und Aktionsfiltern vertraut sein.

Darum lieben Entwickler von Web Forms die Web-API

Wenn Sie ein ASP.NET MVC-Entwickler sind, sind Sie anfangs vielleicht verwundert über die Vorteile der Web-API, da ihr Programmierungsmodell sehr stark dem ASP.NET MVC-Framework ähnelt. Als Entwickler von Web Forms werden Sie davon jedoch begeistert sein. Mit der Web-API ist die Bereitstellung von HTTP-Endpunkten innerhalb einer Web Forms-Anwendung ein Kinderspiel. Sie müssen nur eine oder mehrere Klassen hinzufügen, die der im Folgenden dargestellten Klasse entsprechen:

public class ValuesController : ApiController
{
  public IEnumerable<string> Get()
  {
    return new string[] { "value1", "value2" };
  }
  public string Get(int id)
  {
    return "value";
  }
}

Denselben Code würden Sie verwenden, um einen Web-API-Controller zu einer ASP.NET MVC-Anwendung hinzuzufügen. Außerdem müssen Sie Routen angeben. Den folgenden Code können Sie beim Anwendungsstart ausführen:

RouteTable.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{id}",
  defaults: new { id = System.Web.Http.RouteParameter.Optional });

Wenn nicht anderweitig durch das NonAction-Attribut angegeben, sind alle öffentlichen Methoden in der Klasse, die den standardmäßigen Benennungs- und Routingkonventionen entsprechen, öffentliche, über HTTP aufrufbare Endpunkte. Sie können von jedem Client aufgerufen werden, ohne dass Proxyklassen, web.config-Verweise oder spezieller Code generiert werden muss.

Die Routingkonventionen der Web-API legen fest, dass die URL mit „/api“ beginnt und danach der Controllername folgt. Beachten Sie, dass kein Aktionsname ausdrücklich genannt wird. Die Aktion wird durch den Anforderungstyp festgelegt, zum Beispiel GET, PUT, POST oder DELETE. Ein Methodenname, der mit „Get“, „Put“, „Post“ oder „Delete“ beginnt, wird ganz konventionell der entsprechenden Aktion zugeordnet. Beispielsweise wird eine GetTasks-Methode eines TaskController-Objekts für jede GET-Anforderung einer URL (wie „/api/task“) aufgerufen.

Trotz der offenkundigen Ähnlichkeit mit ASP.NET MVC in Bezug auf Verhalten und Klassennamen befindet sich die Web-API in einem vollständig separaten Satz von Assemblys und verwendet völlig andere Typen – „System.Net.Http“ ist die primäre Assembly.

Genauere Betrachtung der Content Negotiation bei der Web-API

Mit „Content Negotiation“ wird oft der Prozess beschrieben, mit dem die Struktur einer eingehenden HTTP-Anforderung untersucht wird, um das Format zu ermitteln, in dem der Client Antworten empfangen möchte. Technisch betrachtet ist die Content Negotiation jedoch der Prozess, in dem Client und Server das bestmögliche Darstellungsformat für ihre Interaktionen vereinbaren oder aushandeln. Das Untersuchen einer Anforderung bedeutet in der Regel, dass die HTTP-Header-Felder „Accept“ und „Content-Type“ geprüft werden. „Content-Type“ wird auf dem Server zum Verarbeiten von POST- und PUT-Anforderungen und auf dem Client zur Auswahl des Formatierers der HTTP-Antworten verwendet. „Content-Type“ wird nicht für GET-Anforderungen verwendet.

Der interne Mechanismus der Content Negotiation ist jedoch wesentlich komplexer. Das zuvor genannte Szenario ist – aufgrund von Standardkonventionen und -implementierungen zwar das üblichste, aber nicht das einzig mögliche.

Die Komponente, die den Aushandlungsprozess in der Web-API regelt, ist die Klasse „DefaultContentNegotiator“. Sie implementiert eine öffentliche Schnittstelle (IContentNegotiator), sodass Sie sie vollständig austauschen können, falls nötig. Intern wendet der standardmäßige Negotiator verschiedene spezielle Kriterien an, um das ideale Format für die Antwort zu ermitteln.

Der Negotiator arbeitet mit einer Liste registrierter Medientypenformatierer – das sind die Komponenten, die Objekte in ein spezifisches Format umwandeln. Der Negotiator durchläuft die Liste mit Formatierern und hält beim ersten Treffer an. Ein Formatierer kann dem Negotiator auf verschiedene Art und Weise mitteilen, dass er die Antwort für die aktuelle Anforderung serialisieren kann.

Zunächst wird der Inhalt der MediaTypeMappings-Auflistung geprüft, die in allen vordefinierten Medientypformatierern standardmäßig leer ist Eine Medientypzuordnung gibt eine Bedingung an, die, wenn sie zutrifft, den Formatierer zum Serialisieren der Antwort auf die laufende Anforderung berechtigt. Es gibt einige vordefinierte Medientypzuordnungen. Eine überprüft einen bestimmten Parameter in der Abfragezeichenfolge. Sie können zum Beispiel die XML-Serialisierung ganz einfach umsetzen, indem Sie festlegen, dass der Abfragenzeichenfolge, mit der die Web-API aufgerufen wird, der Ausdruck „xml=true“ hinzugefügt wird. Hierzu muss sich im Konstruktor Ihres benutzerdefinierten XML-Medientypformatierers folgender Code befinden:

MediaTypeMappings.Add(new QueryStringMapping("xml", "true", "text/xml"));

Aufrufer können in ähnlicher Weise ihre Präferenzen ausdrücken, indem der URL eine Erweiterung hinzugefügt oder ein benutzerdefinierter HTTP-Header eingefügt wird:

MediaTypeMappings.Add(new UriPathExtensionMapping("xml", "text/xml"));
MediaTypeMappings.Add(new RequestHeaderMapping("xml", "true",
  StringComparison.InvariantCultureIgnoreCase, false,"text/xml"));

Bei URL-Pfaderweiterungen würde folgende URL dem XML-Formatierer zugeordnet werden:

http://server/api/news.xml

Damit URL-Pfaderweiterungen funktionieren, muss eine Ad-hoc-Route wie folgende verwendet werden:

config.Routes.MapHttpRoute(
  name: "Url extension",
  routeTemplate: "api/{controller}/{action}.{ext}/{id}",
  defaults: new { id = RouteParameter.Optional }
);

Bei benutzerdefinierten HTTP-Headern akzeptiert der Konstruktor der RequestHeaderMapping-Klasse den Namen des Headers, seinen erwarteten Wert und eine Reihe von zusätzlichen Parametern. Ein optionaler Parameter gibt den gewünschten Modus für den Zeichenfolgenvergleich an, und ein anderer Parameter ist ein boolescher Ausdruck, der angibt, ob der Vergleich mit der gesamten Zeichenfolge durchgeführt wird. Wenn der Negotiator mithilfe der Medientypzuordnung keinen passenden Formatierer findet, überprüft er die standardmäßigen HTTP-Header wie „Accept“ und „Content-Type“. Wird kein Treffer gefunden, durchläuft er erneut die Liste registrierter Formatierer und prüft, ob der Rückgabetyp der Anforderung von einem dieser Formatierer serialisiert werden kann.

Um einen benutzerdefinierten Formatierer hinzuzufügen, fügen Sie Code wie den folgenden beim Anwendungsstart ein (zum Beispiel in der Application_Start-Methode):

config.Formatters.Add(xmlIndex, new NewsXmlFormatter());

Anpassen des Negotiation-Prozesses

Mit den Medientypzuordnungen lassen sich die meisten speziellen Anforderungen an die Serialisierung erfüllen. Sie können den standardmäßigen Negotiator jedoch jederzeit austauschen, indem Sie eine abgeleitete Klasse schreiben und die MatchRequestMediaType-Methode überspringen:

protected override MediaTypeFormatterMatch MatchRequestMediaType(
  HttpRequestMessage request, MediaTypeFormatter formatter)
{
  ...
}

Sie können einen vollständig benutzerdefinierten Content Negotiator mit einer neuen Klasse erstellen, die die IContentNegotiator-Schnittstelle implementiert. Nach der Erstellung eines solchen benutzerdefinierten Content Negotiators registrieren Sie ihn bei der Web-API-Laufzeit:

GlobalConfiguration.Configuration.Services.Replace(
  typeof(IContentNegotiator),
  new YourOwnNegotiator());

Der voranstehende Code kann in der „global.asax“ oder in einem der praktischen Konfigurationshandler verwendet werden, die Visual Studio für Sie in der ASP.NET MVC-Web-API-Projektvorlage erstellt.

Steuern der Inhaltsformatierung über den Client

Das häufigste Szenario für die Content Negotiation in der Web-API ist die Verwendung des Accept-Headers. Bei dieser Methode ist die Inhaltsformatierung vollständig transparent für den Web-API-Code. Der Aufrufer legt den Accept-Header entsprechend fest (z. B. auf „text/xml“), und die Web-API-Infrastruktur verarbeitet sie entsprechend. Im folgenden Code wird gezeigt, wie der Accept-Header in einem jQuery-Aufruf für einen Web-API-Endpunkt festgelegt wird, um XML abzurufen:

$.ajax({
  url: "/api/news/all",
  type: "GET",
  headers: { Accept: "text/xml; charset=utf-8" }
});

Im C#-Code legen Sie den Header wie folgt fest:

var client = new HttpClient();
client.Headers.Add("Accept", "text/xml; charset=utf-8");

Jede HTTP-API in einer Programmierumgebung lässt Sie HTTP-Header festlegen. Wenn Sie absehen können, dass Sie Aufrufer haben werden, bei denen dies problematisch sein könnte, können Sie als Best Practice eine Medientypzuordnung hinzufügen, sodass die URL alle erforderlichen Informationen zur Inhaltsformatierung enthält.

Bedenken Sie, dass die Antwort vollständig von der Struktur der HTTP-Anforderung abhängt. Versuchen Sie, eine Web-API-URL über die Adresszeile von Internet Explorer 10 und Chrome aufzurufen. Seien Sie nicht überrascht, wenn Sie in einem Fall JSON und im anderen Fall XML erhalten. Die standardmäßigen Accept-Header können sich bei den verschiedenen Browsern unterscheiden. Im Allgemeinen gilt: Wenn die API öffentlich ist und von Dritten verwendet werden wird, sollten Sie einen URL-basierten Mechanismus zum Auswählen des Ausgabeformats haben.

Szenarios für die Verwendung der Web-API

Architektonisch gesehen ist die Web-API ein großer Schritt nach vorne. Ihre Bedeutung wird mit dem aktuellen Open Web Interface for .NET (OWIN) NuGet-Paket (Microsoft.AspNet.Web­­Api.Owin) und dem Katana-Projekt, die das Hosting der API in externen Apps durch einen Standardsatz von Schnittstellen vereinfachen, noch vergrößert. Wenn Sie andere Lösungen als ASP.NET MVC-Anwendungen erstellen, ist die Verwendung der Web-API selbstverständlich. Aber welchen Sinn macht die Verwendung der Web-API in einer auf ASP.NET MVC basierenden Weblösung?

Mit ASP.NET MVC können Sie ganz einfach eine HTTP-Fassade erstellen, ohne neue Dinge lernen zu müssen. Sie können Inhalte recht einfach aushandeln, indem Sie kleine Codeelemente in Controllerbasisklassen oder in Methoden verwenden (oder durch die Erstellung eines ausgehandelten ActionResult). Dazu benötigen Sie nur einen zusätzlichen Parameter in der Aktionsmethodensignatur, müssen diesen prüfen und dann die Antwort in XML oder JSON serialisieren. Die Lösung ist einfach und praktisch, solange Sie sich auf die Verwendung von XML oder JSON beschränken. Wenn Sie mehr Formate berücksichtigen müssen, dann sollten Sie die Web-API verwenden.

Wie bereits zuvor erwähnt, kann die Web-API außerhalb von IIS gehostet werden, zum Beispiel in einem Windows-Dienst. Befindet sich die API in einer ASP.NET MVC-Anwendung, sind Sie natürlich an IIS gebunden. Der Hostingtyp hängt daher von den Zielen der API-Schicht ab, die Sie erstellen. Wenn sie nur von der sie umgebenden ASP.NET MVC-Site verwendet wird, benötigen Sie die Web-API wahrscheinlich nicht. Wenn Ihre erstellte API-Schicht aber ein echter „Dienst“ zum Bereitstellen der API in einem Geschäftskontext ist, ist die Verwendung der Web-API in ASP.NET MVC sehr sinnvoll.

Dino Esposito ist der Autor von „Architecting Mobile Solutions for the Enterprise” (Microsoft Press, 2012) sowie des in Kürze erscheinenden „Programming ASP.NET MVC 5” (Microsoft Press). Esposito ist Technical Evangelist für die .NET- und Android-Plattformen bei JetBrains und spricht häufig auf Branchenveranstaltungen weltweit. Auf software2cents.wordpress.com und auf Twitter unter twitter.com/despos lässt er uns wissen, welche Softwarevision er verfolgt.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Howard Dierking (Microsoft)
Howard Dierking ist Programm-Manager für das Team für Windows Azure-Frameworks und -Tools; seine Schwerpunkte sind ASP.NET, NuGet und Web-APIs. Vorher arbeitete er als Chefredakteur des MSDN Magazine und leitete auch das Entwicklerzertifizierungsprogramm für Microsoft Learning. Vor seiner Zeit bei Microsoft arbeitete er zehn Jahre als Entwickler und Anwendungsarchitekt mit dem Schwerpunkt auf verteilte Systeme.