ASP.NET

Erstellen von Hypermedia-Web-APIs mit ASP.NET-Web-API

Pablo Cibraro

Hypermedia, besser bekannt als „Hypermedia as the Engine of Application State“ (HATEOAS), zählt zu den Haupteinschränkungen des REST-Architekturstils (Representational State Transfer). Dahinter steht der Gedanke, dass anhand von Hypermediaartefakten wie Links oder Formularen beschrieben werden kann, wie Clients mit HTTP-Diensten interagieren. Dieses Konzept wurde schnell für die Erstellung entwicklungsfähiger API-Entwürfe interessant. Dabei gibt es keinen Unterschied dazu, wie wir normalerweise mit dem Web interagieren. Für gewöhnlich erinnern wir uns an einen einzelnen Zugangspunkt oder eine URL zu der Startseite einer Website und bewegen uns dann über die Links durch die verschiedenen Bereiche der Website. Wir benutzen auch Formulare, die mit einer vordefinierten Aktion oder URL Daten übermitteln, die von der Website zum Ausführen bestimmter Aktionen benötigt werden.

Entwickler neigen dazu, für alle unterstützten Methoden eines Diensts statische Beschreibungen zu liefern. Das reicht von formalen Verträgen wie Web Services Description Language (WSDL) in SOAP-Diensten bis hin zur einfachen Dokumentation in Web-APIs ohne Hypermedia. Dabei liegt das Hauptproblem darin, dass Clients durch eine statische API-Beschreibung stark an den Server gekoppelt werden. Verkürzt heißt das, die statische Beschreibung verhindert die Entwicklungsfähigkeit, da jede Änderung an der API-Beschreibung zum Abbruch der Anwendung auf allen vorhandenen Clients führen kann.

In Unternehmen stellte dies eine Zeit lang keine Schwierigkeit dar, da die Anzahl der Clientanwendungen kontrollierbar und im Voraus bekannt war. Wenn jedoch die Anzahl der potenziellen Clients exponentiell ansteigt, so wie heutzutage durch Tausende von Drittanbieteranwendungen, die auf verschiedenen Geräten ausgeführt werden, kann diese Bindung ungeeignet sein. Dennoch ist ein einfacher Wechsel von SOAP- zu HTTP-Diensten kein Garant für die Lösung des Problems. Wenn beispielsweise auf den Clients Möglichkeiten zum Verarbeiten von URLs vorhanden sind, bleibt das Problem selbst ohne expliziten Vertrag wie etwa WSDL bestehen. Hier bietet Hypermedia die Möglichkeiten zum Abschirmen von Clients vor Serveränderungen.  

Der Anwendungszustandsworkflow, über den die nächste durch den Client ausführbare Aktion bestimmt wird, sollte sich ebenfalls auf der Serverseite befinden. Angenommen, eine Aktion in einer Ressource ist nur für einen bestimmten Zustand verfügbar, sollte die Logik dann in jedem möglichen API-Client vorhanden sein? Selbstverständlich nicht. Es sollte immer der Server darüber bestimmen, was mit einer Ressource geschehen soll. Wenn beispielsweise eine Bestellung storniert wird, darf es der Clientanwendung nicht möglich sein, die Bestellung abzusenden. Das heißt, in der Antwort an den Client sollte kein Link bzw. Formular zum Absenden der Bestellung enthalten sein.

Hypermedia als Lösung des Problems

Eine Schlüsselkomponente der REST-Architektur war schon immer der Link. Links sind bereits aus Benutzeroberflächen wie etwa in Browsern bekannt. Denken Sie nur an einen Link wie „Mehr Details“, um die Details zu einem bestimmten Produkt in einem Katalog anzuzeigen. Wie ist das jedoch bei der Interaktion zwischen Computern, bei der es keine Benutzeroberfläche bzw. kein Eingreifen durch den Benutzer gibt? Die Idee ist, dass auch in solchen Fällen Hypermediaartefakte eingesetzt werden können. 

Bei dieser neuen Herangehensweise gibt der Server nicht nur Daten zurück, sondern Daten und Hypermediaartefakte. Durch die Hypermediaartefakte kann der Client die verfügbaren Aktionen bestimmen, die auf Grundlage des Zustands des Serveranwendungsworkflows an einem bestimmten Punkt ausgeführt werden können.

In diesem Bereich unterscheidet sich eine reguläre Web-API typischerweise von einer RESTful API. Es gelten jedoch noch weitere Einschränkungen, weshalb sich die Diskussion, ob es sich bei einer API um eine RESTful API handelt oder nicht, in den meisten Fällen erübrigt. Von Relevanz hingegen ist, dass die API HTTP richtig als Anwendungsprotokoll einsetzt und wenn möglich Hypermedia nutzt. Durch das Aktivieren von Hypermedia können selbstauffindbare APIs erstellt werden. Das ist keine Entschuldigung dafür, die Dokumentation zu vernachlässigen, doch die APIs sind in Sachen Aktualisierbarkeit flexibler.

Welche Hypermediaartefakte verfügbar sind, wird hauptsächlich durch die gewählten Medientypen bestimmt. Viele der aktuell verfügbaren Medientypen zum Erstellen von Web-APIs wie JSON oder XML bieten kein integriertes Konzept für die Darstellung von Links oder Formularen, wie es bei HTML der Fall ist. Sie können diese Medientypen dennoch nutzen, indem Sie eine Möglichkeit zum Ausdrücken von Hypermedia definieren. Das erfordert jedoch Clients, die verstehen, wie darüber hinaus die Hypermediasemantik definiert ist. Im Gegensatz dazu unterstützen Medientypen wie XHTML („application/xhtml+xml“) oder ATOM („application/atom+xml“) bereits einige der Hypermediaartefakte wie Links und Formulare.

Bei HTML besteht ein Link aus drei Bestandteilen: Das href-Attribut zeigt auf eine URL, das rel-Attribut beschreibt die Beziehung des Links zur aktuellen Ressource, und das optionale type-Attribut bestimmt den erwarteten Medientyp näher. Wenn Sie beispielsweise eine Produktliste in einem Katalog mit XHTML verfügbar machen möchten, könnte die Ressourcennutzlast der in Abbildung 1 entsprechen.

Abbildung 1: Verfügbarmachen einer Produktliste mit XHTML

    <div id="products">
      <ul class="all">
        <li>
          <span class="product-id">1</span>
          <span class="product-name">Product 1</span>
          <span class="product-price">5.34</span>
          <a rel="add-cart" href="/cart" type="application/xml"/>
        </li>
        <li>
          <span class="product-id">2</span>
          <span class="product-name">Product 2</span>
          <span class="product-price">10</span>
          <a rel="add-cart" href="/cart" type="application/xml"/>
        </li>
      </ul>
    </div>

In diesem Beispiel wird der Produktkatalog mit Standard-HTML-Elementen dargestellt. Trotzdem habe ich XHTML verwendet, da damit das Parsing mit bestehenden XML-Bibliotheken leichter fällt. Zudem wurde als Teil der Nutzlast ein Anchor-Element („a“) eingefügt, das einen Link zum Hinzufügen des Artikels zum aktuellen Warenkorb des Benutzers darstellt. In dem Link kann ein Client über das rel-Attribut dessen Zweck ableiten (neuen Artikel hinzufügen) und über das href-Attribut eine Operation mit der Ressource durchführen („/cart“). Hierbei ist wichtig, dass die Links vom Server auf Grundlage des Geschäftsworkflows erzeugt werden. Auf diese Weise muss der Client keine Hartkodierung für URLs verwenden oder Regeln ableiten. Dadurch ergeben sich auch neue Möglichkeiten, den Workflow während der Laufzeit zu ändern, ohne die vorhandenen Clients zu beeinträchtigen. Wenn ein Produkt aus dem Katalog nicht lieferbar ist, kann der Server einfach den Link zum Hinzufügen des Produkts zum Warenkorb weglassen. Aus Sicht des Clients ist der Link nicht verfügbar, und somit kann das Produkt nicht bestellt werden. Im Zusammenhang mit diesem Workflow können auch komplexere Regeln auf Serverseite angewendet werden. Für den Client ist dies jedoch nicht einsehbar. Für ihn zählt nur, dass der Link nicht vorhanden ist. Dank Hypermedia und den Links ist der Client vom serverseitigen Geschäftsworkflow entkoppelt.

Darüber hinaus kann die Entwicklungsfähigkeit eines API-Entwurfs mit Hypermedia und Links noch verbessert werden. Mit der Entwicklung des Geschäftsworkflows auf dem Server können zusätzliche Links für neue Funktionen angeboten werden. In dem Beispiel mit dem Produktkatalog könnte der Server einen neuen Link zum Markieren eines Produkts als Favorit hinzufügen. Dies sähe wie folgt aus:

    <li>
      <span class="product-id">1</span>
      <span class="product-name">Product 1</span>
      <span class="product-price">5.34</span>
      <a rel="add-cart" href="/cart/1" type="application/xml"/>
      <a rel="favorite" href="/product_favorite/1" 
         type="application/xml"/>
    </li>

Bestehende Clients ignorieren diesen Link womöglich und bieten die Funktion daher nicht an. Neuere Clients jedoch können sie sofort umsetzen. So gesehen wäre es keinesfalls merkwürdig, einen einzelnen Zugangspunkt oder eine Stamm-URL für die Web-API verwenden zu wollen, die Links zum Erkunden der restlichen Funktionen enthält. Beispielsweise können Sie über nur eine URL „/shopping_cart“ die folgende HTML-Darstellung zurückgeben lassen:

    <div class="root">
      <a rel="products" href="/products"/>
      <a rel="cart" href="/cart"/>
      <a rel="favorites" href="/product_favorite"/>
    </div>

Die gleiche Funktionalität findet sich auch in OData-Diensten, bei denen ein einzelnes Dienstdokument in der Stamm-URL verfügbar gemacht wird. Dieses Dienstdokument enthält sämtliche unterstützten Ressourcensätze und Links für die Zuordnung der Daten.

Links eignen sich hervorragend, um Server und Clients zu verbinden, sie bringen jedoch ein nicht zu vernachlässigendes Problem mit sich. Im vorherigen Beispiel mit dem Produktkatalog bietet der Link in HTML nur die Attribute „rel“, „href“ und „type“. Dies erfordert etwas Zusatzwissen darüber, was mit der URL im href-Attribut geschehen soll. Sollte der Client HTTP POST oder HTTP GET verwenden? Welche Daten sollte der Client bei POST in den Anforderungstext aufnehmen? Dieses Wissen könnte zwar dokumentiert werden, aber wäre es nicht viel besser, wenn die Clients diese Funktionalität selbst entdecken könnten? Auf all diese Fragen ist die Verwendung von HTML-Formularen die sinnvolle Antwort.

Formulare im Einsatz

Bei der Interaktion mit dem Web über einen Browser werden Aktionen üblicherweise mit Formularen dargestellt. In dem Beispiel mit dem Produktkatalog entspricht ein Klick auf den Link „Zum Warenkorb hinzufügen“ einer HTTP GET-Nachricht, die an den Server gesendet wird. Zurückgegeben wird ein HTML-Formular, über das das Produkt zum Warenkorb hinzugefügt werden kann. Dieses Formular kann ein action-Attribut mit einer URL, ein method-Attribut für die HTTP-Methode, einige Eingabefelder, bei denen die Benutzereingabe erforderlich ist, sowie einige lesbare Anweisungen zum weiteren Vorgehen enthalten.

Dasselbe ist auch bei der Interaktion zwischen Computern möglich. Anstelle eines Menschen, der mit einem Formular interagiert, könnte eine Anwendung stehen, die mit JavaScript oder C# ausgeführt wird. In dem Produktkatalog wird über eine HTTP GET-Nachricht an den add-cart-Link zum ersten Produkt das folgende in XHTML dargestellte Formular abgerufen:

    <form action="/cart" method="POST">
      <input type="hidden" id="product-id">1</input>
      <input type="hidden" id="product-price">5.34</input>
      <input type="hidden" id="product-quantity" class="required">1</input>
      <input type="hidden" id="___forgeryToken">XXXXXXXX</input>
    </form>

Die Clientanwendung ist jetzt von bestimmten Details entkoppelt, die mit dem Hinzufügen des Produkts zum Warenkorb in Zusammenhang stehen. Die Anwendung muss das Formular nur noch über eine HTTP POST-Nachricht an die URL senden, die im action-Attribut angegeben ist. Dazu kann der Server noch weitere Informationen in das Formular aufnehmen, beispielsweise ein Fälschungstoken zum Vermeiden von websiteübergreifenden Anforderungsfälschungsangriffen (CSRF) oder zum Signieren der für den Server vorausgefüllten Daten.

Bei diesem Modell ist die freie Entwicklung von Web-APIs möglich, indem neue Formulare auf Grundlage verschiedener Faktoren wie Benutzerberechtigungen oder der vom Client gewünschten Version angeboten werden.

Hypermedia für XML und JSON?

Wie bereits erwähnt, verfügen die generischen Medientypen für XML („application/­xml“) und JSON („application/json“) über keine integrierte Unterstützung für Hypermedia-Links oder -Formulare. Es ist zwar möglich, diese Medientypen mit domänenspezifischen Konzepten wie „application/vnd-shoppingcart+xml“ zu erweitern. Das erfordert jedoch, dass neue Clients die gesamte im neuen Typ definierte Semantik verstehen, was wahrscheinlich zu einer starken Zunahme der Medientypen führen würde. Daher ist dieser Ansatz nicht zu empfehlen.

Aus diesem Grund wurde der neue Medientyp „Hypertext Application Language“ (HAL) vorgeschlagen, der XML und JASON mit Linksemantik erweitert. Die Entwurfsversion, in der eine Standardmöglichkeit zum Ausdrücken von Links und eingebetteten Ressourcen (Daten) mit XML und JSON definiert wird, ist unter stateless.co/hal_specification.html verfügbar. Im HAL-Medientyp wird eine Ressource definiert, die eine Gruppe von Einstellungen, eine Gruppe von Links und eine Gruppe von eingebetteten Ressourcen enthält (siehe Abbildung 2).

The HAL Media Type
Abbildung 2: Der HAL-Medientyp

Abbildung 3 zeigt beispielhaft, wie ein Produktkatalog mit HAL in den beiden Darstellungen XML und JSON aussehen würde. Abbildung 4 zeigt die JSON-Darstellung der Beispielressource.

Abbildung 3: Der Produktkatalog in HAL

    <resource href="/products">
      <link rel="next" href="/products?page=2" />
      <link rel="find" href="/products{?id}" templated="true" />
      <resource rel="product" href="/products/1">
        <link rel="add-cart" href="/cart/" />
        <name>Product 1</name>
        <price>5.34</price>
      </resource>
      <resource rel="product" href="/products/2">
        <link rel="add-cart" href="/cart/" />
        <name>Product 2</name>
        <price>10</price>
      </resource>
    </resource>

Abbildung 4: die JSON-Darstellung der Beispielressource

{
  "_links": {
    "self": { "href": "/products" },
    "next": { "href": "/products?page=2" },
    "find": { "href": "/products{?id}", "templated": true }
  },
  "_embedded": {
    "products": [{
      "_links": {
        "self": { "href": "/products/1" },
        "add-cart": { "href": "/cart/" },
      },
      "name": "Product 1",
      "price": 5.34,
    },{
      "_links": {
        "self": { "href": "/products/2" },
        "add-cart": { "href": "/cart/" }
      },
      "name": "Product 2",
      "price": 10
    }]
  }
}

Hypermedia-Unterstützung im ASP.NET-Web-API

Bis zu diesem Punkt wurde die Theorie hinter Hypermedia beim Entwerfen von Web-APIs besprochen. Werfen wir nun einen Blick darauf, wie die Theorie mit ASP.NET-Web-API inklusive aller Erweiterungspunkte und Features des Frameworks umgesetzt werden kann.

Im Kern unterstützt ASP.NET-Web-API das Prinzip der Formatierer. Bei einer Formatiererimplementierung ist bekannt, wie ein bestimmter Medientyp behandelt werden muss und wie er in konkrete .NET-Typen serialisiert bzw. deserialisiert wird. In der Vergangenheit war die Unterstützung neuer Medientypen in ASP.NET MVC sehr eingeschränkt. Lediglich HTML und JSON wurden als First-Class-Objekte behandelt und im gesamten Stapel vollständig unterstützt. Darüber hinaus gab es kein einheitliches Modell für die Unterstützung der Inhaltsaushandlung. Die Unterstützung verschiedener Medientypformate für die Antwortnachrichten war möglich, indem ActionResult benutzerdefiniert implementiert wurde. Es war jedoch nicht klar, wie ein neuer Medientyp für deserialisierte Anforderungsnachrichten eingeführt werden konnte. Für gewöhnlich wurde dies über die modellbindende Infrastruktur mit neuen Modellbindungen oder Wertanbietern gelöst. Glücklicherweise wurde diese Inkonsistenz in ASP.NET-Web-API mit der Einführung der Formatierer gelöst.

Alle Formatierer werden von der Basisklasse System.Net.Http.Formatting.MediaTypeFormatter abgeleitet und überschreiben die Methode CanReadType/ReadFromStreamAsync zur Deserialisierungsunterstützung sowie die Methode CanWriteType/WriteToStreamAsync zur Unterstützung der Serialisierung von .NET-Typen in ein bestimmtes Medientypformat.

Abbildung 5 zeigt die Definition der MediaTypeFormatter-Klasse.

Abbildung 5: Die MediaTypeFormatter-Klasse

public abstract class MediaTypeFormatter
{
  public Collection<Encoding> SupportedEncodings { get; }
  public Collection<MediaTypeHeaderValue> SupportedMediaTypes { get; }
  public abstract bool CanReadType(Type type);
  public abstract bool CanWriteType(Type type);
  public virtual Task<object> ReadFromStreamAsync(Type type, 
    Stream readStream,
    HttpContent content, IFormatterLogger formatterLogger);
  public virtual Task WriteToStreamAsync(Type type, object value,
    Stream writeStream, HttpContent content, 
    TransportContext transportContext);
}

In ASP.NET-Web-API spielen Formatierer eine wichtige Rolle bei der Unterstützung der Inhaltsaushandlung, da jetzt vom Framework der richtige Formatierer gewählt werden kann. Die Wahl erfolgt anhand der Werte, die über die Header „Accept“ und „Content-Type“ der Anforderungsnachricht empfangen wurden.

Die Methoden ReadFromStreamAsync und WriteToStreamAsync sind von der Task Parallel Library (TPL) abhängig, damit diese die asynchrone Verarbeitung übernimmt. Zu diesem Zweck geben die Methoden eine Task-Instanz zurück. Falls Sie explizit auf einer synchronen Verarbeitung bei der Formatiererimplementierung bestehen, übernimmt die Basisklasse BufferedMediaTypeFormatter dies intern für Sie. Diese Basisklasse stellt die beiden Methoden SaveToStream und ReadFromStream bereit, die Sie in einer Implementierung überschreiben können. Bei diesen Methoden handelt es sich um die asynchronen Versionen von SaveToStreamAsync und ReadFromStreamAsync. 

Entwicklung von MediaTypeFormatter für HAL

Da HAL für die Darstellung von Ressourcen und Links eine bestimmte Semantik verwendet, können Sie in einer Web-API-Implementierung nicht jedes beliebige Modell verwenden. Daher werden eine Basisklasse für die Darstellung einer Ressource und eine weitere Basisklasse für eine Ressourcensammlung genutzt, was die Implementierung des Formatierers wesentlich vereinfacht:

public abstract class LinkedResource
{
  public List<Link> Links { get; set; }
  public string HRef { get; set; }
}
public abstract class LinkedResourceCollection<T> : LinkedResource,
  ICollection<T> where T : LinkedResource
{
  // Rest of the collection implementation
}

Von diesen beiden Basisklassen können die realen Modellklassen abgeleitet werden, die von den Web-API-Controllern verwendet werden. So kann beispielsweise ein Produkt oder eine Produktsammlung folgendermaßen implementiert werden:

public class Product : LinkedResource
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal UnitPrice { get; set; }
}
...
public class Products : LinkedResourceCollection<Product>
{
}

Über eine Standardmethode zum Definieren von HAL-Modellen kann jetzt der Formatierer implementiert werden. Am einfachsten lässt sich eine neue Formatiererimplementierung mit einer Ableitung von der Basisklasse MediaTypeFormatter oder von der Basisklasse BufferedMediaTypeFormatter beginnen. Im Beispiel in Abbildung 6 kommt die zweite Basisklasse zum Einsatz.

Abbildung 6: Die Basisklasse BufferedMediaTypeFormatter

public class HalXmlMediaTypeFormatter : BufferedMediaTypeFormatter
{
  public HalXmlMediaTypeFormatter()
    : base()
  {
    this.SupportedMediaTypes.Add(new MediaTypeHeaderValue(
      "application/hal+xml"));
  }
  public override bool CanReadType(Type type)
  {
    return type.BaseType == typeof(LinkedResource) ||
      type.BaseType.GetGenericTypeDefinition() ==
        typeof(LinkedResourceCollection<>);
  }
  public override bool CanWriteType(Type type)
  {
    return type.BaseType == typeof(LinkedResource) ||
     type.BaseType.GetGenericTypeDefinition() ==
       typeof(LinkedResourceCollection<>);
  }
  ...
}

Zunächst werden über den Code im Konstruktor die unterstützten Medientypen für diese Implementierung („application/hal+xml“) definiert und die Methoden CanReadType und CanWriteType überschrieben. So werden die unterstützten .NET-Typen angegeben, die entweder von Linked­Resource oder von LinkedResourceCollection abgeleitet sein müssen. Gemäß der Definition im Konstruktor unterstützt diese Implementierung nur die XML-Variante von HAL. Optional kann ein weiterer Formatierer zur Unterstützung der JSON-Variante implementiert werden.

Die tatsächliche Arbeit wird in den Methoden WriteToStream und ReadFromStream ausgeführt (siehe Abbildung 7). Dabei kommt ein XmlWriter bzw. ein XmlReader zum Schreiben eines Objekts in den Datenstrom bzw. zum Lesen des Objekts aus dem Datenstrom zum Einsatz.

Abbildung 7: Die Methoden WriteToStream und ReadFromStream

public override void WriteToStream(Type type, object value,
  System.IO.Stream writeStream, System.Net.Http.HttpContent content)
{
  var encoding = base.SelectCharacterEncoding(content.Headers);
  var settings = new XmlWriterSettings();
  settings.Encoding = encoding;
  var writer = XmlWriter.Create(writeStream, settings);
  var resource = (LinkedResource)value;
  if (resource is IEnumerable)
  {
    writer.WriteStartElement("resource");
    writer.WriteAttributeString("href", resource.HRef);
    foreach (LinkedResource innerResource in (IEnumerable)resource)
    {
      // Serializes the resource state and links recursively
      SerializeInnerResource(writer, innerResource);
    }
    writer.WriteEndElement();
  }
  else
  {
    // Serializes a single linked resource
    SerializeInnerResource(writer, resource);
  }
  writer.Flush();
  writer.Close();
}
public override object ReadFromStream(Type type,
  System.IO.Stream readStream, System.Net.Http.HttpContent content,
  IFormatterLogger formatterLogger)
{
  if (type != typeof(LinkedResource))
    throw new ArgumentException(
      "Only the LinkedResource type is supported", "type");
  var value = (LinkedResource)Activator.CreateInstance(type);
  var reader = XmlReader.Create(readStream);
  if (value is IEnumerable)
  {
    var collection = (ILinkedResourceCollection)value;
    reader.ReadStartElement("resource");
    value.HRef = reader.GetAttribute("href");
    var innerType = type.BaseType.GetGenericArguments().First();
    while (reader.Read() && reader.LocalName == "resource")
    {
      // Deserializes a linked resource recursively
      var innerResource = DeserializeInnerResource(reader, innerType);
      collection.Add(innerResource);
    }
  }
  else
  {
    // Deserializes a linked resource recursively
    value = DeserializeInnerResource(reader, type);
  }
  reader.Close();
  return value;
}

Der letzte Schritt besteht darin, die Formatiererimplementierung als Teil des Web-API-Hosts zu konfigurieren. Dieser Schritt ist mit der Vorgehensweise in ASP.NET oder ASP.NET Web API Self-Host vergleichbar. Der einzige Unterschied liegt in der benötigten HttpConfiguration-Implementierung. Self-Host verwendet eine HttpSelfHostConfiguration-Instanz, wohingegen ASP.NET üblicherweise die HttpConfiguration-Instanz verwendet, die global in System.Web.Http.GlobalConfiguration.Configuration verfügbar ist. Die HttpConfiguration-Klasse stellt eine Formatters-Sammlung bereit, in die Sie Ihre eigene Formatiererimplementierung einfügen können. So gehen Sie bei ASP.NET vor:

protected void Application_Start()
{
  Register(GlobalConfiguration.Configuration);
}
public static void Register(HttpConfiguration config)
{
  config.Formatters.Add(new HalXmlMediaTypeFormatter());
}

Sobald der Formatierer in der ASP.NET-Web-API-Pipeline konfiguriert ist, kann von jedem Controller einfach eine von LinkedResource abgeleitete Modellklasse zurückgegeben werden, damit sie durch den Formatierer mit HAL serialisiert wird. Im Beispiel mit dem Produktkatalog kann das Produkt von LinkedResource und die Produktsammlung, die den Katalog darstellt, von LinkedResourceCollection abgeleitet werden:

public class Product : LinkedResource
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal UnitPrice { get; set; }
}
public class Products : LinkedResourceCollection<Product>
{
}

Jetzt können vom ProductCatalogController-Controller, der alle Anforderungen für die Produktkatalogressource verarbeitet, für die Get-Methode Instanzen von „Product“ und „Products“ zurückgegeben werden (siehe Abbildung 8).

Abbildung 8: Die ProductCatalogController-Klasse

public class ProductCatalogController : ApiController
{
  public static Products Products = new Products
  {
    new Product
    {
      Id = 1,
      Name = "Product 1",
      UnitPrice = 5.34M,
      Links = new List<Link>
      {
        new Link { Rel = "add-cart", HRef = "/api/cart" },
        new Link { Rel = "self", HRef = "/api/products/1" }
      }
    },
    new Product
    {
      Id = 2,
      Name = "Product 2",
      UnitPrice = 10,
      Links = new List<Link>
      {
        new Link { Rel = "add-cart", HRef = "/cart" },
        new Link { Rel = "self", HRef = "/api/products/2" }
      }
    }
  };
  public Products Get()
  {
    return Products;           
  }
}

In diesem Beispiel wird das HAL-Format verwendet. Sie können aber auch einen ähnlichen Ansatz zum Aufbau eines Formatierers wählen, der Razor und Vorlagen zum Serialisieren von Modellen in XHTML verwendet. Eine konkrete Implementierung von MediaTypeFormatter für Razor finden Sie in RestBugs unter github.com/howarddierking/RestBugs. In dieser Beispielanwendung von Howard Dierking wird demonstriert, wie mit ASP.NET-Web-API Hypermedia-Web-APIs erstellt werden.

Mit Formatierern lässt sich die Web-API einfach durch neue Medientypen erweitern.    

Bessere Linkunterstützung in den Web-API-Controllern

In dem vorhergehenden Beispiel zu ProductCatalog­Controller gibt es ein Problem. Alle Links wurden hartkodiert, was zu erheblichen Schwierigkeiten führen kann, wenn die Routen häufig geändert werden. Die gute Nachricht ist jedoch, dass das Framework eine Helferklasse namens System.Web.Http.Routing.UrlHelper bereitstellt, mit der Links aus der Routingtabelle automatisch abgeleitet werden. Eine Instanz dieser Klasse ist in der Basisklasse ApiController über die Url-Eigenschaft verfügbar. So kann sie einfach in jeder beliebigen Controller-Methode eingesetzt werden. Die UrlHelper-Klassendefinition sieht folgendermaßen aus:

public class UrlHelper
{
  public string Link(string routeName,
    IDictionary<string, object> routeValues);
  public string Link(string routeName, object routeValues);
  public string Route(string routeName,
    IDictionary<string, object> routeValues);
  public string Route(string routeName, object routeValues);
}

Die Route-Methoden geben die relative URL zu einer gegebenen Route zurück (zum Beispiel „/products/1“). Die Links-Methoden geben die absolute URL zurück, die in den Modellen zur Vermeidung von Hartkodierungen eingesetzt werden kann. Die Link-Methode empfängt zwei Argumente: den Routenname und die Werte zum Aufbau der URL.

In Abbildung 9 ist zu sehen, wie in dem vorhergehenden Produktkatalogbeispiel die UrlHelper-Klasse in der Get-Methode genutzt werden kann.

Abbildung 9: Mögliche Nutzung der UrlHelper-Klasse in der Get-Methode

public Products Get()
{
  var products = GetProducts();
  foreach (var product in products)
  {
    var selfLink = new Link
    {
      Rel = "self",
      HRef = Url.Route("API Default",
        new
        {
          controller = "ProductCatalog",
          id = product.Id
        })
    };
product.Links.Add(selfLink);
if(product.IsAvailable)
{
    var addCart = new Link
    {
      Rel = "add-cart",
      HRef = Url.Route("API Default",
        new
        {
          controller = "Cart"
        })
    };
    product.Links.Add(addCart);
  }           
}
  return Products;           
}

Der Link „self“ zum Produkt wurde von der Standardroute mit dem Controllernamen ProductCatalog und der Produktkennung erzeugt. Der Link zum Hinzufügen des Produkts zum Warenkorb wurde ebenfalls von der Standardroute erzeugt, jedoch mit dem Controllernamen „Cart“. Wie in Abbildung 9 zu sehen, ist der Link zum Hinzufügen des Produkts zum Warenkorb mit der Antwort verbunden, die auf der Produktverfügbarkeit (product.IsAvailable) beruht. Die Logik zum Bereitstellen von Links für den Client wird meistens von den Geschäftsregeln abhängen, die für gewöhnlich in den Controllern durchgesetzt werden.    

Zusammenfassung

Hypermedia ist ein leistungsfähiges Feature, das die unabhängige Entwicklung von Clients und Servern ermöglicht. Durch die Verwendung von Links oder anderen Hypermediaartefakten wie Formularen, die vom Server an verschiedenen Stellen bereitgestellt werden, können Clients erfolgreich vom Servergeschäftsworkflow und somit von seiner maßgeblichen Bestimmung der Interaktionen entkoppelt werden.

Pablo Cibraro ist ein international anerkannter Experte mit über 12 Jahren Erfahrung im Entwerfen und Implementieren großer verteilter Systeme mithilfe von Microsoft-Technologien. Er ist Microsoft-MVP für verbundene Systeme. In den letzten neun Jahren hat Cibraro zahlreiche Microsoft-Teams bei der Entwicklung von Tools und Frameworks zum Aufbau von serviceorientierten Anwendungen mit Webdiensten, Windows Communication Foundation, ASP.NET und Windows Azure unterstützt. Er veröffentlicht Blogs unter weblogs.asp.net/cibrax, und Sie können ihm auf Twitter unter twitter.com/cibrax folgen.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Daniel Roth