Eine moderne WebAPI - die OData Basics

Eine moderne WebAPI - die OData Basics

Was ist OData?

Doch zunächst etwas Vorgeschichte

Seit es Schnittstellen (API) zum Austausch von Daten gibt, gibt es die Unsicherheit, welche Daten von einem Webservice zurückgegeben werden – es hat die Semantik, ein Standard gefehlt.

Als es dann die ersten XML-Webservices gab, hatte man über XSD (XML Schema Definition) die Möglichkeit die gelieferten Daten zu validieren. Nun hat man zwar eine Möglichkeit gehabt die Rückgaben als Inhalt zu validieren, aber diese müssen in der Regel in Objekte der jeweiligen Programmiersprache übersetzt – deserialisiert – werden.

Die meisten API-Anbieter haben dann angefangen SDKs – Software Development Kits – anzubieten; meist aber nur für die gängigsten aber oft auch nur für ihr präferierte Programmiersprache. Programmierer, die andere Sprachen genutzt haben, waren oft vor einer undokumentierten API auf sich alleine gestellt. Nutzer des SDKs hatten oft Probleme, dass das SDK nicht mehr aktuell war oder einfach nicht weiterentwickelt wurde.

WSDL

Das W3C-Konsortium hat relativ schnell erkannt, dass es einen Standard geben muss, der Entwicklern das Leben und dem Umgang mit Web-APIs leichter macht: WSDL – Web Service Description Language.

WSDL ist plattform- und programmiersprachen unabhängig und beschreibt die Funktionalität, Daten und Datentypen einer Webschnittstelle. Es wird in der Regel zusammen mit SOAP – einem Protokoll zum Datenaustausch basierend auf XML – verwendet und ist auch heute noch sehr weit – vor allem in der Industrie – verbreitet.

Json

Durch den Triumph von mobilen Plattformen und dem Internet wurden zwei Dinge für Entwickler und Betreiber von Webanwendungen immer wichtiger: Geschwindigkeit und Datenvolumen.

Beide Dinge stehen dabei in engem Zusammenhang: wenn weniger Daten an ein Handy gesendet werden müssen – und das oft über langsame Leitungen zu Zeiten von GRPS und Edge – desto schneller wird die jeweilige Webanwendung auf dem Endgerät geladen.

Aufgrund dieser Tatsache wurden Webschnittstellen vielseitiger und unterstützen neben XML eben auch Json.

Json hat einen viel geringeren Overhead als XML. Es sind viel weniger semantische Zeichen zur Trennung von Nutzdaten notwendig, was das Übertragungsvolumen sehr stark verringert und mit einem sehr geringen Aufwand die Webseite schneller beim Benutzer geladen werden.

Das Problem an dieser Stelle ist jetzt jedoch wieder die Validierung der Daten: es gibt keine Verträge wie SOAP oder XLS mehr – auch der Json-Standard hat dies gar nicht vorgesehen.

RESTful

Um die Übertragung wieder zu standardisieren entstand REST – Representational State Transfer.

Das REST Paradigma hat viele Prinzipien wie Zustandlosigkeit – fokussiere mich an dieser Stelle jedoch auf die Standardisierung der Kommunikation.

Bei REST werden die HTTP-Verben dazu genutzt, Inhalte abzurufen, zu erstellen oder zu manipulieren. Die wichtigsten wären:

  • Mit Hilfe eines GET-Requests wird eine Ressource vom Server angefordert
  • Über POST wird eine neue Ressource erstellt
  • PUT ändert eine vorhandene, vollständige Ressource; kann diese aber auch anlegen, wenn sie nicht existiert
  • PATCH ändert den Teil einer Ressource, sodass nur die Änderung mitgeteilt wird
  • DELETE löscht eine Ressource

Sofern die Ressource über Metadaten verfügt können diese durch META angerufen werden Ab und zu trifft man auf APIs, die manchmal von REST ein bisschen, aber nicht sehr, abweichen. Das betrifft aber i.d.R. nur POST und PUT, die bei manchen APIs genau das gleiche ausführen.

TTP Status Codes

Neben den HTTP Verben spielen auch die Status Codes des HTTP Protokolls mittlerweile eine große Rolle bei Webschnittstellen.

Früher hat man oft einen Teil der Nachricht – egal ob XML oder Json – dazu verwendet dem Aufrufer mitzuteilen, ob die Anfrage erfolgreich war oder nicht. Meist in Form von OKbzw. { status: „succeeded“ }

Davon abgesehen, dass auch das die Nachrichtengröße unnötig aufbläht ist dies unnötig. HTTP Statuscodes sind Bestandteil absolut jeder HTTP Anfrage; also warum nicht einfach diese verwenden?

Die gängigsten Status Codes mit ihrer jeweiligen Verwendung kann zum Beispiel von RESTPatterns.org eingesehen werden.

Open Data Protocol – the best way to REST

Dies ist der Leitspruch von <a href="http://www.odata.org/" target"_blank">OData.

OData ist – wie der Name schon sagt – ein offenes Format für den Austausch von Daten – und basiert auf all den schönen Vorteilen und strengeren Regeln von REST; inklusive der HTTP Verben, Statuscodes sowie Plattform- und Technologie-/Programmiersprachenunabhängigkeit.

Es wird der Aufbau sowie das Kommunizieren und Austauschen von Daten vollständig definiert und ist aktuell in der Version 4 veröffentlicht.

Durch die strengen Regeln von OData, die sicherlich nicht allen Entwicklern gefallen, sind OData-Schnittstellen jedoch immer gleich aufgebaut, können auf die gleiche Art angesprochen und verwendet werden. Dies erhöht die Wiederverwendbarkeit von Code enorm, führt zu einem allgemein gültigen Verständnis und senkt auch den Aufwand der Dokumentationserstellung und des Lesens dessen.

Metadata

OData arbeitet auf der Basis von Entitäten. Also definierten, typisierten Klassen mit Eigenschaften. Wo bei herkömmlichen APIs eine externe Dokumentation notwendig war, sodass man sehen konnte, was die API bei gewissen Abfragen zurückgibt, gibt es bei OData Metadaten.

<?xml version="1.0" encoding="UTF-8"?>
<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:m="http://docs.oasis-open.org/odata/ns/metadata" xml:base="http://services.odata.org/V4/OData/OData.svc/" m:context="http://services.odata.org/V4/OData/OData.svc/$metadata">
   <workspace>
      <atom:title type="text">Default</atom:title>
      <collection href="Products">
         <atom:title type="text">Products</atom:title>
      </collection>
      <collection href="ProductDetails">
         <atom:title type="text">ProductDetails</atom:title>
      </collection>
      <collection href="Categories">
         <atom:title type="text">Categories</atom:title>
      </collection>
      <collection href="Suppliers">
         <atom:title type="text">Suppliers</atom:title>
      </collection>
      <collection href="Persons">
         <atom:title type="text">Persons</atom:title>
      </collection>
      <collection href="PersonDetails">
         <atom:title type="text">PersonDetails</atom:title>
      </collection>
      <collection href="Advertisements">
         <atom:title type="text">Advertisements</atom:title>
      </collection>
   </workspace>
</service>
<?xml version="1.0" encoding="UTF-8"?>
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
   <edmx:DataServices>
      <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="ODataDemo">
         <EntityType Name="Product">
            <Key>
               <PropertyRef Name="ID" />
            </Key>
            <Property Name="ID" Type="Edm.Int32" Nullable="false" />
            <Property Name="Name" Type="Edm.String" />
            <Property Name="Description" Type="Edm.String" />
            <Property Name="ReleaseDate" Type="Edm.DateTimeOffset" Nullable="false" />
            <Property Name="DiscontinuedDate" Type="Edm.DateTimeOffset" />
            <Property Name="Rating" Type="Edm.Int16" Nullable="false" />
            <Property Name="Price" Type="Edm.Double" Nullable="false" />
            <NavigationProperty Name="Categories" Type="Collection(ODataDemo.Category)" Partner="Products" />
            <NavigationProperty Name="Supplier" Type="ODataDemo.Supplier" Partner="Products" />
            <NavigationProperty Name="ProductDetail" Type="ODataDemo.ProductDetail" Partner="Product" />
         </EntityType>
         <EntityType Name="FeaturedProduct" BaseType="ODataDemo.Product">
            <NavigationProperty Name="Advertisement" Type="ODataDemo.Advertisement" Partner="FeaturedProduct" />
         </EntityType>
         <EntityType Name="ProductDetail">
            <Key>
               <PropertyRef Name="ProductID" />
            </Key>
            <Property Name="ProductID" Type="Edm.Int32" Nullable="false" />
            <Property Name="Details" Type="Edm.String" />
            <NavigationProperty Name="Product" Type="ODataDemo.Product" Partner="ProductDetail" />
         </EntityType>
         <EntityType Name="Category" OpenType="true">
            <Key>
               <PropertyRef Name="ID" />
            </Key>
            <Property Name="ID" Type="Edm.Int32" Nullable="false" />
            <Property Name="Name" Type="Edm.String" />
            <NavigationProperty Name="Products" Type="Collection(ODataDemo.Product)" Partner="Categories" />
         </EntityType>
         <EntityType Name="Supplier">
            <Key>
               <PropertyRef Name="ID" />
            </Key>
            <Property Name="ID" Type="Edm.Int32" Nullable="false" />
            <Property Name="Name" Type="Edm.String" />
            <Property Name="Address" Type="ODataDemo.Address" />
            <Property Name="Location" Type="Edm.GeographyPoint" SRID="Variable" />
            <Property Name="Concurrency" Type="Edm.Int32" ConcurrencyMode="Fixed" Nullable="false" />
            <NavigationProperty Name="Products" Type="Collection(ODataDemo.Product)" Partner="Supplier" />
         </EntityType>
         <ComplexType Name="Address">
            <Property Name="Street" Type="Edm.String" />
            <Property Name="City" Type="Edm.String" />
            <Property Name="State" Type="Edm.String" />
            <Property Name="ZipCode" Type="Edm.String" />
            <Property Name="Country" Type="Edm.String" />
         </ComplexType>
         <EntityType Name="Person">
            <Key>
               <PropertyRef Name="ID" />
            </Key>
            <Property Name="ID" Type="Edm.Int32" Nullable="false" />
            <Property Name="Name" Type="Edm.String" />
            <NavigationProperty Name="PersonDetail" Type="ODataDemo.PersonDetail" Partner="Person" />
         </EntityType>
         <EntityType Name="Customer" BaseType="ODataDemo.Person">
            <Property Name="TotalExpense" Type="Edm.Decimal" Nullable="false" />
         </EntityType>
         <EntityType Name="Employee" BaseType="ODataDemo.Person">
            <Property Name="EmployeeID" Type="Edm.Int64" Nullable="false" />
            <Property Name="HireDate" Type="Edm.DateTimeOffset" Nullable="false" />
            <Property Name="Salary" Type="Edm.Single" Nullable="false" />
         </EntityType>
         <EntityType Name="PersonDetail">
            <Key>
               <PropertyRef Name="PersonID" />
            </Key>
            <Property Name="PersonID" Type="Edm.Int32" Nullable="false" />
            <Property Name="Age" Type="Edm.Byte" Nullable="false" />
            <Property Name="Gender" Type="Edm.Boolean" Nullable="false" />
            <Property Name="Phone" Type="Edm.String" />
            <Property Name="Address" Type="ODataDemo.Address" />
            <Property Name="Photo" Type="Edm.Stream" Nullable="false" />
            <NavigationProperty Name="Person" Type="ODataDemo.Person" Partner="PersonDetail" />
         </EntityType>
         <EntityType Name="Advertisement" HasStream="true">
            <Key>
               <PropertyRef Name="ID" />
            </Key>
            <Property Name="ID" Type="Edm.Guid" Nullable="false" />
            <Property Name="Name" Type="Edm.String" />
            <Property Name="AirDate" Type="Edm.DateTimeOffset" Nullable="false" />
            <NavigationProperty Name="FeaturedProduct" Type="ODataDemo.FeaturedProduct" Partner="Advertisement" />
         </EntityType>
         <EntityContainer Name="DemoService">
            <EntitySet Name="Products" EntityType="ODataDemo.Product">
               <NavigationPropertyBinding Path="ODataDemo.FeaturedProduct/Advertisement" Target="Advertisements" />
               <NavigationPropertyBinding Path="Categories" Target="Categories" />
               <NavigationPropertyBinding Path="Supplier" Target="Suppliers" />
               <NavigationPropertyBinding Path="ProductDetail" Target="ProductDetails" />
            </EntitySet>
            <EntitySet Name="ProductDetails" EntityType="ODataDemo.ProductDetail">
               <NavigationPropertyBinding Path="Product" Target="Products" />
            </EntitySet>
            <EntitySet Name="Categories" EntityType="ODataDemo.Category">
               <NavigationPropertyBinding Path="Products" Target="Products" />
            </EntitySet>
            <EntitySet Name="Suppliers" EntityType="ODataDemo.Supplier">
               <NavigationPropertyBinding Path="Products" Target="Products" />
            </EntitySet>
            <EntitySet Name="Persons" EntityType="ODataDemo.Person">
               <NavigationPropertyBinding Path="PersonDetail" Target="PersonDetails" />
            </EntitySet>
            <EntitySet Name="PersonDetails" EntityType="ODataDemo.PersonDetail">
               <NavigationPropertyBinding Path="Person" Target="Persons" />
            </EntitySet>
            <EntitySet Name="Advertisements" EntityType="ODataDemo.Advertisement">
               <NavigationPropertyBinding Path="FeaturedProduct" Target="Products" />
            </EntitySet>
         </EntityContainer>
         <Annotations Target="ODataDemo.DemoService">
            <Annotation Term="Org.OData.Display.V1.Description" String="This is a sample OData service with vocabularies" />
         </Annotations>
         <Annotations Target="ODataDemo.Product">
            <Annotation Term="Org.OData.Display.V1.Description" String="All Products available in the online store" />
         </Annotations>
         <Annotations Target="ODataDemo.Product/Name">
            <Annotation Term="Org.OData.Display.V1.DisplayName" String="Product Name" />
         </Annotations>
         <Annotations Target="ODataDemo.DemoService/Suppliers">
            <Annotation Term="Org.OData.Publication.V1.PublisherName" String="Microsoft Corp." />
            <Annotation Term="Org.OData.Publication.V1.PublisherId" String="MSFT" />
            <Annotation Term="Org.OData.Publication.V1.Keywords" String="Inventory, Supplier, Advertisers, Sales, Finance" />
            <Annotation Term="Org.OData.Publication.V1.AttributionUrl" String="http://www.odata.org/" />
            <Annotation Term="Org.OData.Publication.V1.AttributionDescription" String="All rights reserved" />
            <Annotation Term="Org.OData.Publication.V1.DocumentationUrl " String="http://www.odata.org/" />
            <Annotation Term="Org.OData.Publication.V1.TermsOfUseUrl" String="All rights reserved" />
            <Annotation Term="Org.OData.Publication.V1.PrivacyPolicyUrl" String="http://www.odata.org/" />
            <Annotation Term="Org.OData.Publication.V1.LastModified" String="4/2/2013" />
            <Annotation Term="Org.OData.Publication.V1.ImageUrl " String="http://www.odata.org/" />
         </Annotations>
      </Schema>
   </edmx:DataServices>
</edmx:Edmx>

Wie hier im offiziellen Beispiel von odata.org zu sehen ist (korrekt, das sind Informationen als XML!), können das komplexe Typen sein, wie man sie von C# oder Java kennt.

Dank der Metadaten können Entwicklungsumgebungen nun automatisch die Klassen generieren, die in den Metadaten beschrieben sind. Für Visual Studio ist das zB. der <a href="https://visualstudiogallery.msdn.microsoft.com/9b786c0e-79d1-4a50-89a5-125e57475937" target"_blank">OData v4 Client Code Generator. Aber es existieren auch generische Open Source Clients für C#, AngularJS, Java, Node…

Dies führt dazu, dass kein externes SDK bzw. für jede Zielsprache mehr erstellt werden muss. Der Entwickler kann sich selbst die Klassen generieren und dank der Plattform- und Technologieunabhängigkeit die Sprache seiner Wahl verwenden.

Dokumentation

Die Metadaten sind Fluch und Segen zugleich, wobei in meinen Augen definitiv der Segen überwiegt.

Swagger.io hat sich als das Tool etabliert, um APIs zu dokumentieren – oder wie Swagger es ermöglicht: automatisch zu dokumentieren.

Das Problem aktuell bei Swagger ist: es kann mit OData APIs noch nicht offiziell umgehen. Da Swagger jedoch Open Source, auf GitHub verfügbar und die Community sehr sehr fleißig ist, gibt es dafür schon entsprechende Lösungen in Form von Swashbuckle.OData. Eine entsprechende Demo liegt auf Azure des dahinterstehenden Entwicklers.

Filter

Eine API ist aus Sicht von konsumierenden Anwendungen nichts anderes als ein Datenpool – wie auch eine Datenbank. Damit dieser Datenpool performant – es sollen nur die Elemente geladen werden, die auch wirklich benötigt werden – sowie flexibel abgefragt werden kann – eine öffentliche API kennt schließlich nicht die Bedürfnisse der konsumierenden Anwendung – enthält der OData Standard Filter.

Die Filter sind hier nichts unübliches; man kennt die Schlüsselwörter aus SQL oder .NET Welt:

  • orderby
  • top
  • skip
  • filter
  • format
  • select

Die Filter sind dabei so gedacht, dass sie durch die URL übergeben und bis zur Datenbank durchgereicht werden. Damit keine bösen Abfragen von bösen Buben direkt an die Datenbank kommen – oder vielleicht will man auch nicht jeden Filter auf eine Datenbank zulassen, das ist legitim – können Filter einzeln oder gesamt abgeschalten oder eingeschränkt werden – pro Abfrage oder Global.

Ebenso werden zum Beispiel mathematische Operationen wie Floor, Ceiling, Round oder Vergleiche wie Contains, StartsWith, EndsWith… unterstützt. Alles, was man von einer Abfrage-Engine eines Datenpools erwarten kann.

Niemand würde alle Daten aus einer Datenbank ziehen und dann im Client filtern. Wieso das also in einer API machen, bei sich die Menge der Daten und dadurch die Geschwindigkeit viel negativer auf die Anwendung auswirkt?!

URL-Aufbau

Der URL Aufbau von OData gehört zum Regelwerk; die sogenannten URL Convensions. Hinter jeder URL steht eine fixe Methode wie Get, GetAll, Add, Update, Delete, wobei eigene Methoden ebenfalls registriert werden können.

Beispiele:

BeschreibungURL
Alle Personenhttp://host/service/Persons
Alle Personen, sortiert nach Namehttp://host/service/Persons?$orderBy=Name
Alle Personen, aber nur die Ids ,die Namen und das Geburtsdatumhttp://host/service/Persons?$select=Id,Name,DateOfBirth
Alle Personen, die am 1. eines Monats geboren sindhttp://host/service/Persons?$filter=day(DateOfBirth) eq 1
Alle Personen, die Emma im Namen habenhttp://host/service/Persons?$filter=contains(Name, Emma)
Person mit der Id 31http://host/service/Persons(31)
Name der Person mit der Id 31http://host/service/Persons(31)?$select=Name

Beispiele des OData Sample Services

OData.org hat stellt vier Services Online für die Version 4 von OData zur Verfügung, mit denen man ein wenig spielen kann. Zwei davon sind Read-Only und zwei lassen sich auch mit Write-Anfragen manipulieren.

Abfragen der Anzahl von Produkten

http://services.odata.org/V4/OData/OData.svc/Products/$count Das Resultat an dieser Stelle ist einfach nur: 11 Mehr gibt der Server hier nicht zurück. Dass der Request überhaupt funktioniert hat erkennen wir, dass der Server mit einem HTTP Statuscode 200 antwortet.|

Abfragen aller Produkte als Entitäten

http://services.odata.org/V4/OData/OData.svc/Products

{
   "@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Products",
   "value":[
      {
         "ID":0,
         "Name":"Bread",
         "Description":"Whole grain bread",
         "ReleaseDate":"1992-01-01T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":4,
         "Price":2.5
      },
      {
         "ID":1,
         "Name":"Milk",
         "Description":"Low fat milk",
         "ReleaseDate":"1995-10-01T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":3,
         "Price":3.5
      },
      {
         "ID":2,
         "Name":"Vint soda",
         "Description":"Americana Variety - Mix of 6 flavors",
         "ReleaseDate":"2000-10-01T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":3,
         "Price":20.9
      },
      {
         "ID":3,
         "Name":"Havina Cola",
         "Description":"The Original Key Lime Cola",
         "ReleaseDate":"2005-10-01T00:00:00Z",
         "DiscontinuedDate":"2006-10-01T00:00:00Z",
         "Rating":3,
         "Price":19.9
      },
      {
         "ID":4,
         "Name":"Fruit Punch",
         "Description":"Mango flavor, 8.3 Ounce Cans (Pack of 24)",
         "ReleaseDate":"2003-01-05T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":3,
         "Price":22.99
      },
      {
         "ID":5,
         "Name":"Cranberry Juice",
         "Description":"16-Ounce Plastic Bottles (Pack of 12)",
         "ReleaseDate":"2006-08-04T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":3,
         "Price":22.8
      },
      {
         "ID":6,
         "Name":"Pink Lemonade",
         "Description":"36 Ounce Cans (Pack of 3)",
         "ReleaseDate":"2006-11-05T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":3,
         "Price":18.8
      },
      {
         "ID":7,
         "Name":"DVD Player",
         "Description":"1080P Upconversion DVD Player",
         "ReleaseDate":"2006-11-15T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":5,
         "Price":35.88
      },
      {
         "ID":8,
         "Name":"LCD HDTV",
         "Description":"42 inch 1080p LCD with Built-in Blu-ray Disc Player",
         "ReleaseDate":"2008-05-08T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":3,
         "Price":1088.8
      },
      {
         "@odata.type":"#ODataDemo.FeaturedProduct",
         "ID":9,
         "Name":"Lemonade",
         "Description":"Classic, refreshing lemonade (Single bottle)",
         "ReleaseDate":"1970-01-01T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":7,
         "Price":1.01
      },
      {
         "@odata.type":"#ODataDemo.FeaturedProduct",
         "ID":10,
         "Name":"Coffee",
         "Description":"Bulk size can of instant coffee",
         "ReleaseDate":"1982-12-31T00:00:00Z",
         "DiscontinuedDate":null,
         "Rating":1,
         "Price":6.99
      }
   ]
}

Wir sehen hier direkt im ersten Beispiel, dass OData doch ein wenig mehr Overhead hat. So ergibt sich jeder Return aus dem aktuellen Kontext. Hinter dem Kontext befinden sich die Metainformationen für die aktuelle Rückgabe; also welcher Typ mit welchen Eigenschaften. In diesem Fall eben vom Typ Products.

Alle Rückgaben von Entitäten basieren auf diesem Format.

Abfragen IDs und der Namen aller Produkte

http://services.odata.org/V4/OData/OData.svc/Products?$select=ID,Name

{
   "@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Products(ID,Name)",
   "value":[
      {
         "ID":0,
         "Name":"Bread"
      },
      {
         "ID":1,
         "Name":"Milk"
      },
      {
         "ID":2,
         "Name":"Vint soda"
      },
      {
         "ID":3,
         "Name":"Havina Cola"
      },
      {
         "ID":4,
         "Name":"Fruit Punch"
      },
      {
         "ID":5,
         "Name":"Cranberry Juice"
      },
      {
         "ID":6,
         "Name":"Pink Lemonade"
      },
      {
         "ID":7,
         "Name":"DVD Player"
      },
      {
         "ID":8,
         "Name":"LCD HDTV"
      },
      {
         "@odata.type":"#ODataDemo.FeaturedProduct",
         "ID":9,
         "Name":"Lemonade"
      },
      {
         "@odata.type":"#ODataDemo.FeaturedProduct",
         "ID":10,
         "Name":"Coffee"
      }
   ]
}

Hier auch wieder die Angabe des Kontext; jedoch beschränkt auf die Eigenschaften ID und Name des Typs Product.

Alle Produkte, die weniger als 5 Dollar kostet und hierbei nur ID, Name und Preis ermitteln

http://services.odata.org/V4/OData/OData.svc/Products?$filter=Price lt 5&$select=ID,Name,Price

{
   "@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Products(ID,Name,Price)",
   "value":[
      {
         "ID":0,
         "Name":"Bread",
         "Price":2.5
      },
      {
         "ID":1,
         "Name":"Milk",
         "Price":3.5
      },
      {
         "@odata.type":"#ODataDemo.FeaturedProduct",
         "ID":9,
         "Name":"Lemonade",
         "Price":1.01
      }
   ]
}

Im letzten Fall sehen wir, dass nicht nur die ID, den Name und den Preis des Produkts zurück gegeben wurden, sondern auch in einem Fall eine Typbeschreibung: @odata.type„:„#ODataDemo.FeaturedProduct

OData zeigt hier auf, dass es sich um ein besonderen Typen handelt. FeaturedProduct erbt nämlich von Product! Es ist also auch für jeden Entwickler zu sehen, dass er es hier mit einer Objekthierarchie zutun hat!

Dies ist auch der Metadata zu entnehmen:

<EntityType Name="FeaturedProduct" BaseType="ODataDemo.Product">
   <NavigationProperty Name="Advertisement" Type="ODataDemo.Advertisement" Partner="FeaturedProduct"/>
</EntityType>

Es ist nichts anderes als eine Vererbung, wobei FeaturedProduct eben noch eine zusätzliche Eigenschaft hat.

OData verhält sich per default so, dass eingebettete Typen nicht in der Abfrage enthalten sind. Das ist eben wieder ein Nebeneffekt, dass nur das geladen wird, was wirlich geladen werden soll. Das mag vielleicht am Anfang etwas lästig sein; wirkt sich aber sehr positiv für die Performance aus!

Damit ich nun an die zusätzlichen Eigenschaften des FreaturedProduct komme, muss ich der API mitteilen. Einen direkten Zugriff auf das Set der FeaturesProduct gibt es nicht, also frage ich direkt das Product 9 ab und gebe an, dass ich den Typ FeaturedProduct haben möche:

http://services.odata.org/V4/OData/OData.svc/Products(9)/ODataDemo.FeaturedProduct

{
   "@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Products/ODataDemo.FeaturedProduct/$entity",
   "@odata.type":"#ODataDemo.FeaturedProduct",
   "ID":9,
   "Name":"Lemonade",
   "Description":"Classic, refreshing lemonade (Single bottle)",
   "ReleaseDate":"1970-01-01T00:00:00Z",
   "DiscontinuedDate":null,
   "Rating":7,
   "Price":1.01
}

Hier sehe ich nun wieder den Context, also welchen Typ ich zurück erhalten habe, nochmals den Namespace des Typs und alle darin enthaltenen Informationen. Perfekt! Im Vergleich: bei einer herkömmlichen WebAPI, die nur XML oder Json versteht, aber kein OData implementiert, müsste ich für jeden Filtern und jede Filter-Parameter eigene Schnittstellen definieren. Oder ich müsste dem Client alle Informationen bieten und dieser würde auf seiner Seite filtern.

Nochmal: wer würde genau das bei einer Datenbak machen? ;-)

OData auf GitHub

OData ist offen – und wird auf GitHub auch offen gelebt. Die meisten Teile von OData stehen unter MIT Lizenz zur Verfügung, zB. OData.NET

Persönliche Erfahrung

OData hat Einstiegshürden durch die strengen Regeln. Die Regeln haben auch mich am Anfang dazu gezwungen, dass ich meine APIs ordentlicher gestalte – aber das ist ja nicht gegen den Entwickler, sondern für die Einheitlichkeit. Am Ende also wieder für den Entwickler.

Politisch gesehen habe ich mit OData aber schon viel negative Erfahrungen gemacht. Entwickler lassen sich nicht gerne etwas vorschreiben und und versuchen Dinge so einfach wie möglich umzusetzen; aber auch hier muss man sich an Regeln halten. Und genau diese Regeln – dass eben URLs so und so aussehen müssen – führten zu unzähligen Diskussionen, bei denen der Mehrwert nicht eingesehen werden wollte (oder konnte).

Trotz allem bleibe aber dabei, dass eine gute API immer auch eine direkte Möglichkeit haben muss, die Rückgaben einheitlich und allgemein gültig auszuwerten – und dies ist Stand heute OData.

Alleine die Punkte Metadaten und Filter werden jedem Entwickler geschenkt. Zwei Punkte, die eine enorm enorm enorm enorm hohe Gewichtung – natürlich neben der strengen Standardisierung – für eine moderne, flexible, schnelle API sind und nie und nimmer verdrängt werden sollten!

OData ist auch in fast allen Microsoft Produkten mittlerweile präsent oder steht zur Verfügung:

  • Excel kann von OData Feeds Daten lesen
  • Azure bietet zahlreiche APIs wie Storage Table via OData an
  • Office 365 API ist OData
  • PowerBI bietet eine Menge OData Feeds

Auch Facebook bietet in Form der Insights API Daten via OData Feed an.

Fazit

Auch wenn OData erst am Anfang der Verbreitung ist, löst es so ziemlich alle Probleme, die man in den letzten Jahren mit Webschnittstellen hatte:

  • Dank Plattform- und Technologieunabhängigkeit keine Einschränkungen beim Client
  • Dank Metadaten kann auf SDKs verzichtet werden
  • Die Metadaten helfen auch dabei genau zu erfahren, was eine API für ein Resultat bietet
  • Filter helfen dabei die Performance von APIs zu optimieren und nur das zu laden, was der Client wirklich benötigt
  • Der Overhead ist so gering wie derzeit möglich
  • REST wird vollständig unterstützt und sogar forciert
  • Klarer aufbau der URLs durch URL Conventions
  • Eigene Implementierungen weiterhin problemlos möglich

OData hat zugegeben auch einen riesen Nachteil für Entwickler: er hat nicht mehr die Möglichkeit zu „schlampern“ und muss sich nun wirklich bemühen eine gute, konsistente API zu entwickeln ;-)

Am Ende ist es also nur noch Kopfsache des Entwicklers, wenn OData scheitert.