Februar 2018

Band 33, Nummer 2

Data Points: Erstellen von Azure-Funktionen, die aus Cosmos DB fast ohne Code lesen können

Von Julie Lerman

Julie LermanIn der ersten dieser Kolumnen habe ich Ihnen gezeigt, wie Sie Entity Framework Core 2 (EF Core 2) verwenden können, um die Spielstände lokal auf dem Windows 10-Gerät zu speichern, auf dem das Spiel gespielt wird. In der zweiten Kolumne habe ich Ihnen schrittweise eine Azure-Funktion vorgestellt, die über HTTP gesendete Spielstände in einer Cosmos DB-Datenbank speichert. Das ultimative Ziel ist es, das Spiel so zu ändern, dass es in der Lage ist, seine Spielstände an diese Funktion zu senden, sowie Spielstandsdaten von parallelen Funktionen abzurufen.

In dieser Kolumne zeige ich Ihnen, wie Sie die Azure Functions erstellen, die auf Anforderungen zum Abrufen von zwei Sätzen von Spielstandsdaten reagieren: die fünf besten Ergebnisse für einen Benutzer auf allen Geräten dieses Benutzers und die fünf besten Ergebnisse aller Benutzer, die das CookieBinge-Spiel weltweit spielen. Nächstes Mal (in der letzten Kolumne der Serie) werde ich die neuen Azure Functions in die CookieBinge UWP-App integrieren, um es Benutzern zu ermöglichen, ihre Spielstände zu teilen und zu vergleichen.

Die Azure Function, die ich im vorherigen Artikel erstellt habe, antwortet auf einen HTTP-Aufruf und ist mithilfe der Integrationen der Funktion in der Lage, die Daten aus der HTTP-Anforderung in die Cosmos DB-Datenbank zu übertragen, ohne dass ich Datenzugriffscode schreiben muss. Abbildung 1 zeigt eines der Dokumente, die in der Binges-Sammlung der CookieBinge-Datenbank gespeichert sind. Die ersten sieben Eigenschaften sind das, was ich gespeichert habe („logged“ bis „worthit“), und der Rest sind Metadaten, die von Cosmos DB erstellt wurden, als der Datensatz eingefügt wurde.

Ein Datensatz aus der Binges-Sammlung des CookieBinge-Spiels
Abbildung 1: Ein Datensatz aus der Binges-Sammlung des CookieBinge-Spiels

Für die nächsten beiden Funktionen werde ich wieder die Vorteile der Azure Functions-Integrationen nutzen, um mit derselben Cosmos DB-Datenbank zu interagieren. Der einzige Code, der dieses Mal für die Datenbankinteraktion erforderlich ist, ist etwas SQL, das die erforderliche Abfragezeichenfolge darstellt, und dieses wird in die Integrationen eingebunden, nicht in den Code der Funktion. Beide Funktionen werden durch HTTP-Aufrufe ausgelöst. Der erste Aufruf nimmt die ID des Benutzers an, der die Anforderung ausgegeben hat, und gibt dann die fünf besten Ergebnisse zurück, die der Benutzer auf allen Geräten, auf denen er das Spiel gespielt hat, protokolliert hat. Der zweite Aufruf nimmt keine Eingaben an, sondern gibt einfach die fünf besten Ergebnisse aller Spieler weltweit zurück.

Ich führe Sie schrittweise durch das Erstellen der ersten Funktionen mithilfe des Workflows des Azure-Portals und ergänze damit die Lektionen aus der vorherigen Kolumne. Wenn Sie die erste dieser Funktionen schrittweise durcharbeiten, sollten Sie verstehen, wie alle Puzzleteile zusammenpassen. Ich zeige Ihnen dann eine Abkürzung, wenn es an der Zeit ist, die zweite Funktion zu erstellen. Denken Sie daran, dass es auch möglich ist, Azure Functions mit den in Visual Studio 2017 integrierten Tools zu erstellen, sowie mit der Functions-Befehlszeilenschnittstelle (CLI) und Visual Studio Code.

Erstellen einer Funktion zum Zurückgeben der besten Spielstände eines Benutzers

Kehren wir nun zur CookieBinge Azure Functions-App im Azure-Portal zurück und fügen der App eine neue Funktion hinzu. Erinnern Sie sich: Wenn Sie den Mauszeiger über dem Functions-Gruppenkopf unter dem Namen der Funktions-App positionieren, wird ein Pluszeichen angezeigt, auf das Sie klicken können. Wenn Sie dies tun, werden Sie aufgefordert, eine Vorlage für die neue Funktion auszuwählen. Wie bei der ersten Funktion, die Sie erstellt haben, handelt es sich um einen C# HTTP-Trigger. Nennen Sie die neue Funktion „GetUserScores“, ändern Sie die Autorisierungsebene in „Anonym“, und klicken Sie dann auf „Erstellen“.

Bevor ich den Code in der Datei „run.csx“ ändere, erstelle ich die Integrationen der Funktion. Erinnern Sie sich daran, dass es drei Arten von Integrationen gibt:

  • Trigger: Es kann nur einen einzigen Trigger geben, und in diesem Fall ist es die HTTP-Anforderung an die Funktion.
  • Eingabeintegrationen: Es kann mehrere Eingabeintegrationen geben, und diese stellen weitere Daten dar, die Sie der Funktion zur Verfügung stellen möchten.
  • Ausgabeintegrationen: Im vorherigen Artikel habe ich eine Ausgabeintegration mit der CookieBinge Cosmos DB-Datenbank verbunden, wodurch die Ergebnisse der Funktion in der Datenbank gespeichert wurden. Mit dieser neuen GetUserScores-Funktion enthält die Ausgabe die Ergebnisse der Funktion, die als HTTP-Antwort zurück an den Anforderer gesendet werden.

Ich werde die Eingabeintegration verbinden, um Daten aus der Cosmos DB-Datenbank abzurufen, in der sie durch die StoreScores-Funktion aus dem Artikel des letzten Monats gespeichert wurden. Diese Eingabe war für mich zunächst ein verwirrendes Konzept. Ich nahm an, dass ich etwas Code in der Funktion schreiben müsste, um eine Verbindung mit dieser Datenbank herzustellen und die Ergebnisse der Benutzer abzufragen. Aber die Eingabeintegration ist ziemlich cool. Ich kann sie mit dem entsprechenden SQL für die Abfrage der Cosmos DB-Datenbank definieren und sie wissen lassen, dass sie den UserId-Wert verwenden soll, der mit der HTTP-Anforderung gesendet wurde. Und Azure Functions kümmert sich um den Rest: Herstellen der Verbindung mit der Datenbank, Ausführen der Abfrage und Übergeben der Abfrageergebnisse, mit denen ich im Funktionscode arbeiten kann. Ja, wirklich sehr cool! Machen wir uns also an die Arbeit.

Erstellen einer Eingabeintegration zum Lesen aus Cosmos DB mit HTTP-Anforderungsdaten

Beginnen Sie, indem Sie im Bereich „Integrationen“ auf „Neue Eingabe“ klicken, und wählen Sie dann aus der Liste der aufgeführten Eingabetypen „Cosmos DB“ aus. Klicken Sie anschließend auf die Schaltfläche „Auswählen“ darunter. Das Ergebnis ist das Cosmos DB-Eingabeformular, dessen Felder mit Standardwerten aufgefüllt wurden. Ändern Sie die Werte wie in Abbildung 2 gezeigt.

Einstellungen für die Azure Cosmos DB- Eingabeintegration
Abbildung 2: Einstellungen für die Azure Cosmos DB- Eingabeintegration

Die Werte für den Datenbanknamen, den Sammlungsnamen und die Azure Cosmos DB-Kontoverbindung sollten mit den Werten übereinstimmen, die Sie zum Erstellen der Ausgabeintegration der StoreScores-Funktion verwendet haben. In meinem Fall handelt es sich um „CookieBinge“, „Binges“ und „datapointscosmosdb_DOCUMENTDB“. Ich habe meinen Parameter „documents“ genannt, den ich im Code meiner Funktion verwenden werde. Die Dokument-ID wird leer gelassen. Wenn Sie eine Funktion zum Abrufen eines einzelnen Cosmos DB-Dokuments anhand seiner ID erstellen, wäre dies eine praktische Option. Diese Funktion fragt jedoch auf Basis einer eingehenden UserId ab.

Die SQL-Abfrage wird im Screenshot abgeschnitten. Hier ist die vollständige Abfrage. Denken Sie daran, dass Sie sie als einzelne Zeile hinzufügen müssen, obwohl ich hier Zeilenumbrüche verwendet habe, um sie leichter lesbar zu machen:

SELECT  TOP 5 c.score,c.worthit,c.deviceName,c.dateTime
FROM c
WHERE c.userId={userId}
ORDER by c.score DESC

Azure Cosmos DB verfügt über eine SQL-Abfragesyntax, die die Arbeit mit Azure Cosmos DB angenehm vertraut macht.

Beachten Sie den Platzhalter für die userID-Variable ({userId}). Ich verwende diese Variable, um den Trigger und die Eingabeintegration zu verbinden. Zunächst muss ich die Einstellungen dieser Eingabe speichern. Dann muss ich einen Bindungsparameter erstellen, um den Trigger an die Eingabe zu binden. Dies kann auf eine Art und Weise geschehen, die mir nicht sofort klar war. Bevor ich dies ordnungsgemäß eingerichtet hatte, erhielt ich in der Tat immer wieder einen Fehler, dass der Bindungsparameter fehlt, und es kostete einige Anstrengungen, meine Erkenntnisse aus den Dokumenten, Websuchergebnissen und einigen Experimenten so anzuwenden, dass das Ergebnis richtig war. Ein Teil des Problems war mein mangelndes Verständnis. Sobald ich die Lösung gefunden hatte, machten die Verbindungen für mich jedoch Sinn. Hoffentlich geht Ihnen schneller ein Licht auf, denn ich werden Ihnen die Lösung löffelweise verabreichen!

Wählen Sie den HTTP-Trigger (req) aus, und geben Sie eine Routenvorlage für den Trigger an. Die Vorlage sollte zunächst einen Namen für die Route bereitstellen und dann die Bindungsvariable (userId) angeben.

Die Route muss nicht mit dem Namen der Funktion übereinstimmen, aber es ist wahrscheinlich verständlicher, wenn dies der Fall ist. Ich habe meine Route „GetUserScores/{userId}“ genannt, wie in Abbildung 3 gezeigt. Vergessen Sie nicht, Ihre Änderungen zu speichern.

Einstellungen der HTTP-Triggerintegration
Abbildung 3: Einstellungen der HTTP-Triggerintegration

Die Ausgabeintegration kann die Standardeinstellung verwenden, die eine HTTP-Antwort ist.

Nun ist es an der Zeit, zum Funktionscode zurückzukehren. Sie können zu diesem Zweck auf die GetUserScores-Funktion klicken. Daraufhin wird der Code aus „run.csx“ angezeigt, der sich noch im Standardzustand befindet.

Schreiben des Funktionscodes zum Antworten auf die Integrationen

Sehen Sie sich den SQL-Code für die Integrationseingabe an, der vier Spalten abruft: „score“, „worthit“, deviceName“ und „dateTime“. Sie könnten eine Klasse erstellen, die mit jedem Typ übereinstimmt (wie ich es in der StoreScores-Funktion des vorherigen Artikels getan habe), oder einfach der Funktion mitteilen, dass die Eingabefunktion einen dynamischen Typ übergibt. Mein Code geht auf diese Weise vor. Abbildung 4 zeigt die gesamte Funktion in der Datei „run.csx“.

Abbildung 4: Die Run-Methode der GetUserScores-Funktion

using System.Net;
public static HttpResponseMessage Run(string userId,
               HttpRequestMessage req, IEnumerable<dynamic> documents, 
               TraceWriter log)
{
  if (documents != null)
  {
    log.Info($"Document Count: {documents.Count()}");
    return req.CreateResponse(HttpStatusCode.OK,documents);
  }
  else
  {
    return req.CreateResponse(HttpStatusCode.NotFound);
  }
}

Die Run-Methode bestätigt in ihrer Signatur, dass der erste Parameter eine Zeichenfolge namens userId ist, die über die in den Triggereinstellungen angegebene Route eingeht. Nach der userId erwarten die Parameter die HttpRequestMessage (req) und dann das IEnumerable-Objekt, das die Dokumente enthält. Hinter den Kulissen nimmt die Funktion die userId an, die über die Route des Triggers eingegangen ist, und übergibt sie an die Eingabeintegration, die die Abfrage (unter Verwendung der userId) ausführt und die documents-Variable mit den Abfrageergebnissen auffüllt. Danach ruft die Funktion endlich die Programmlogik der Run-Methode auf, die ich geschrieben habe.

Der Code prüft zunächst, ob die Dokumente tatsächlich an die Methode übergeben wurden. Ich gebe eine Nachricht an mein Protokoll mit der Anzahl dieser Dokumente aus und gebe sie dann einfach in der HTTP-Antwort zurück. Wenn ich keine Überprüfungen und Ausgleiche vornehmen würde, würde die Methode immer noch in diesem einfacheren Format funktionieren:

public static HttpResponseMessage Run(string userId,
              HttpRequestMessage req, IEnumerable<dynamic> documents)
{
  return req.CreateResponse(documents);
}

Mit anderen Worten: Damit diese gesamte Funktion eine UserID empfängt und die fünf besten Ergebnisse des Benutzers aus der Datenbank zurückgibt, ist nur eine einzige Codezeile erforderlich, die nicht mehr besagt als „Zurücksenden der Dokumente, die aus der Eingabeintegration empfangen wurden“.

Die Integrationen von Azure Functions haben mir wirklich die ganze Arbeit abgenommen. Ich bin außerordentlich beeindruckt.

Ich kann diese Funktion im Portal oder in einem Tool wie Fiddler oder Postman testen. Das Testen in einem Browser funktioniert mit der Funktion in ihrem aktuellen Zustand nicht, da die Funktion dynamische Objekte zurückgibt und ein Browser nicht den erforderlichen Accept-Header senden kann. Alternativ können Sie auch eine Klasse definieren, die mit den Abfrageergebnissen übereinstimmt, und diese Klasse anstelle eines dynamischen Objekts verwenden. So wie es ist, wird die Funktion aber ordnungsgemäß funktionieren, wenn sie aus Fiddler, Postman oder Ihren APIs aufgerufen wird, z.B. aus der API, die Sie später aus der UWP-App verwenden.

Der Link „Funktions-URL abrufen“ informiert mich, dass ich https://cookie­binge.azurewebsites.net/api/Get­UserScores/{userId} aufrufen kann. Wenn die Funktion nicht anonym wäre, müsste ich einige Anmeldeinformationen übergeben, aber ich halte es hier alles absichtlich einfach.

Mit meiner userId (54321) im Platzhalter sieht die URL folgendermaßen aus: https://cookiebinge.azurewebsites.net/api/GetUserScores/54321.

Diese URL gibt fünf JSON-formatierte Dokumente mit dem durch die Abfrage geformten Schema zurück. Hier ist eines der Dokumente, die in der HTTP-Antwort zurückgegeben werden. Es zeigt, dass ich 18 Plätzchen verschlungen habe, als ich auf meiner Xbox spielte, aber über meinen Naschanfall an diesem Tag nicht wirklich glücklich war.

{
  "score": 18,
  "worthit": false,
  "deviceName": "XBox",
  "dateTime": "2017-11-05T15:26:00"
}

Eine Abkürzung für das Erstellen einer weiteren vollständig integrierten Azure-Funktion

Da ich nun die erste Funktion eingerichtet habe, ist es an der Zeit, die zweite Funktion zu erstellen, die die fünf besten Ergebnisse für alle Spieler rund um den Globus abruft. Anstatt alle Einstellungsformulare noch einmal zu durchlaufen, werde ich eine Abkürzung einschlagen. Die Einstellungen für alle Integrationen einer Azure-Funktion werden in der Datei „function.json“ gespeichert, die Sie im Portal aus dem Bereich „Dateien anzeigen“ öffnen können. Abbildung 5 zeigt die Datei „function.json“ für die GetUserScores-Funktion. Der Abschnitt zu den Bindungen umfasst alle Integrationseinstellungen.

Abbildung 5: Die Datei „function.json“ für GetUserScores

{
"bindings": [
  {
    "authLevel": "anonymous",
    "name": "req",
    "type": "httpTrigger",
    "direction": "in",
    "route": "GetUserScores/{userId}"
  },
  {
    "name": "$return",
    "type": "http",
    "direction": "out"
  },
  {
    "type": "documentDB",
    "name": "documents",
    "databaseName": "CookieBinge",
    "collectionName": "Binges",
    "sqlQuery": "SELECT  TOP {top} c.score,c.worthit,c.deviceName,
      c.dateTime FROM c WHERE c.userId={userId} ORDER by c.score DESC",
    "connection": "datapointscosmosdb_DOCUMENTDB",
    "direction": "in"
  }
],
"disabled": false
}

Die erste Bindung ist der httpTrigger, der in seiner type-Eigenschaft notiert wird. Beachten Sie, dass alle übrigen Einstellungen durch die anderen Eigenschaften „authLevel“, „name“, „direction“ und „route“ beschrieben werden. Als nächstes sehen Sie die http-Ausgabebindung und schließlich die Eingabebindung mit allen Einstellungen, die ich im Formular angegeben habe.

Nun, da Sie über ein besseres Verständnis aller Puzzleteile der Funktion verfügen, müssen Sie nicht wirklich alle Formulare durchlaufen, wenn Sie dies nicht möchten. Sie können einfach die Datei „function.json“ direkt erstellen. Genau so werde ich für die zweite Funktion vorgehen. Sie müssen die neue Funktion noch der Funktions-App hinzufügen. Verwenden Sie zu diesem Zweck erneut eine C# HttpTrigger-Vorlage, und nennen Sie die Funktion GetGlobalScores.

Anstatt zum Abschnitt „Integration“ zu navigieren, öffnen Sie dieses Mal die Datei „function.json“ im Bereich „Dateien anzeigen“ und ersetzen sie durch den Code in Abbildung 6. Beachten Sie, dass der sqlQuery-Wert hier in der Auflistung umbrochen wird. Diese Zeichenfolge sollte aber aus einer Zeile bestehen.

Abbildung 6: Die Datei „function.json“ für GetGlobalScores

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    },
    {
      "type": "documentDB",
      "name": "documents",
      "databaseName": "CookieBinge",
      "collectionName": "Binges",
      "connection": "datapointscosmosdb_DOCUMENTDB",
      "direction": "in",
      "sqlQuery": "SELECT  TOP 5 c.userName, c.score,c.worthit,c.deviceName,
        c.dateTime FROM c ORDER by c.score DESC"
    }
  ],
  "disabled": false
}

Da diese Anforderung keine ID übergeben muss, gibt es für die httpTrigger-Bindung keine Route wie bei der ersten Funktion. Die Ausgabebindung ist mit der vorhergehenden Ausgabebindung identisch. Der einzige Unterschied zur Eingabebindung besteht darin, dass eine andere Abfrage verwendet wird, die nicht auf eine userId filtert.

Das Verstehen und Anwenden der Bindungen in ihrem Rohformat kann Ihnen sicherlich viel Zeit ersparen. Der explizite Weg, zuerst die Formulare zur Eingabe der Einstellungen zu verwenden, war jedoch von Vorteil, da er mir half, die Einstellungen zu verstehen, als ich mich zum ersten Mal mit ihnen beschäftigt habe. Da Sie wissen, wie die Konfiguration gespeichert wird, sind Sie jetzt in der Tat sehr gut vorbereitet, Ihre Azure Functions in Visual Studio oder Visual Studio Code zu erstellen, anstatt im Portal.

Nun zum Funktionscode, der fast identisch mit dem Code der ersten Funktion ist, mit dem Unterschied, dass er die userId-Bindung nicht als ersten Parameter annimmt. Dies ist die geänderte Signatur der Run-Methode:

public static HttpResponseMessage Run(
              HttpRequestMessage req, IEnumerable<dynamic> documents, TraceWriter log)

Ich habe einige der Testdaten in meiner Cosmos DB Binges-Sammlung manuell geändert, um sicherzugehen, dass es eine Reihe von verschiedenen userId-Werten gibt, bevor ich die neue GetGlobalUserScores-Funktion ausgeführt habe, um zu bestätigen, dass alles ordnungsgemäß funktioniert.

Verknüpfen der UWP-App mit Azure Functions

Unter Verwendung der drei Azure-Funktionen und der Cosmos DB-Dokumentdatenbank zum Speichern und Abrufen von Benutzerergebnissen wird die letzte Folge dieser Serie zur UWP-App zurückkehren, um diese Funktionen zu integrieren. Erinnern Sie sich daran, dass die UWP-App zurzeit Entity Framework Core 2 verwendet, um die Benutzerergebnisse lokal auf einem beliebigen Windows 10-Gerät zu speichern, auf dem das Spiel gespielt wird. In der nächsten Iteration werden zusätzlich zum lokalen Speicher die Spielstände eines Benutzers (mit seiner Erlaubnis) an die StoreScores Azure-Funktion gesendet, um sie in der Cloud zu speichern. Und die Benutzer können eine Liste ihrer eigenen besten Ergebnisse für alle Geräte abrufen, auf denen sie gespielt haben, sowie die besten Ergebnisse aller Spieler rund um den Globus einsehen. Die App ruft die hier erstellten Funktionen auf, um diese Daten an den Benutzer zu melden.

Bitte beachten Sie, dass ich möglicherweise meine eigenen Demofunktionen unter den weiter oben in diesem Artikel genannten URLs nicht mehr bereitstellen werde. Sie werden in meinem Visual Studio-Abonnementkonto gehostet, das zu Testzwecken dient und ein Ausgabenlimit aufweist.


Julie Lerman ist Microsoft Regional Director, Microsoft MVP, Coach für das Softwareteam und Unternehmensberaterin. Sie lebt in den Bergen von Vermont. Sie hält bei User Groups und Konferenzen in der ganzen Welt Vorträge zum Thema Datenzugriff und zu anderen Themen. Julie Lerman führt unter thedatafarm.com/blog einen Blog. Sie ist die Autorin von „Programming Entity Framework“ sowie der Ausgaben „Code First“ und „DbContext“ (alle bei O’Reilly Media erschienen). Folgen Sie ihr auf Twitter: @julielerman, und sehen Sie sich ihre Pluralsight-Kurse unter juliel.me/PS-Videos an.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Jeff Hollan
Jeff Hollan ist Senior Program Manager für Microsoft Azure Functions.  Er kam vor etwas mehr als 4 Jahren zu Microsoft und hat viel Zeit damit verbracht, Back-End-IT-Systeme und Integrationen zu verwalten und andere Produkte in Azure zu betreuen, darunter Logic Apps. Jeff Hollan begeistert sich für alles, was mit Technik zu tun hat, und ist bekannt für seine Präsentationen auf Konferenzen in der ganzen Welt. Sie können Jeff Hollan auf Twitter folgen: @jeffhollan.


Diesen Artikel im MSDN Magazine-Forum diskutieren