Ein lokaler Entity Manager für JavaScript

Eine frische Breeze
Kommentare

Viele Single Page Applications verwenden ein datenbankbasiertes Web-API. Hier kommt Breeze ins Spiel, um die Zugriffe von der App zum Backend zu abstrahieren und auf ein Minimum zu reduzieren, sowie die Applikation offlinefähig zu machen.

Um lokale Abfragen durchführen zu können, wird ein Entity Manager auf dem Client benötigt, der die Struktur der Daten kennt. Auch ein Mechanismus zur Änderungsverfolgung ist erforderlich, um nur die Daten an den Server zu übermitteln, die sich verändert haben. Diese zwei Aufgaben lassen sich sehr gut mit der JavaScript-Bibliothek Breeze lösen. Der folgende Artikel soll einen Überblick über die Möglichkeiten der Verwendung von Breeze im Zusammenspiel mit dem ASP.NET Web API 2 und dem Entity Framework 6 geben. Das vollständige Beispielprojekt finden Sie auf Github.com.

Funktionsweise von Breeze

Abbildung 1 zeigt eine bereits geladene Single Page Application, mit einem separatem Application Layer und einem separatem Data Access Layer. Der Application Layer ist für die Anwendungslogik sowie die Darstellung der Daten zuständig. Der Data Layer lädt, aktualisiert und verwaltet die Daten. Um die Kommunikation zwischen dem Server und dem Client so gering wie möglich zu halten, sollen bereits übertragene Daten auf den Client zwischengespeichert und mithilfe lokaler Abfragen dargestellt werden.

Abb. 1: Übersicht einer Single Page Application mit Breeze

Abb. 1: Übersicht einer Single Page Application mit Breeze

Breeze nimmt die Abfragen des Application Layers entgegen und formt daraus eine Anfrage im OData-Format. Diese Abfrage kann entweder an einen OData-Controller oder an einen Web-API-Server gesendet werden. Da das ASP.NET Web API 2 eine OData-Abfrage nicht direkt verarbeiten kann, gibt es passend zur Clientbibliothek von Breeze eine serverseitige Erweiterung des Web API, die die zusätzlichen Parameter verarbeiten kann (Abb. 2).

Abb. 2: Zusammenspiel der serverseitigen und der clientseitigen Bibliotheken von Breeze

Abb. 2: Zusammenspiel der serverseitigen und der clientseitigen Bibliotheken von Breeze

Auf diese Weise ermöglicht Breeze Anfragen in JavaScript, die der Funktionsweise einer LINQ-Query sehr ähnlich sind. Abfragen können vor der Ausführung dynamisch erweitert werden. Die Ergebnisse einer serverbasierten Abfrage werden als asynchrones Promise zurückgegeben. Darüber hinaus verfügen Breeze Entitys über eine Änderungsverfolgung sowie Navigationseigenschaften.

Breeze-Operationen geben die Schnittstelle IQueryable< > zurück. Bevor die Query auf dem Server zur Ausführung kommt, wird sie unter Berücksichtigung der zusätzlichen Query-Parameter mithilfe eines Postprozessors erweitert.

Erstellen eines Breeze-Web-API-Controllers

Ausgehend von einem Web-API-2-Projekt mit einem vorhandenen EF6-Datacontext werden zwei NuGet-Pakete benötigt, um Breeze verwenden zu können. Das erste Paket Breeze.Server.WebAPI 2 stellt alle generischen Erweiterungen bereit, die benötigt werden, um die Abfrage um die OData-Query-Parameter zu erweitern. Das zweite Paket Breeze.Server.ContextProvider.EF6 stellt einen Adapter bereit, der die von der Clientbibliothek benötigten Metainformationen über das Datenmodel automatisch aus einem EF6-Context ableiten kann.

Nach der Installation dieser Bibliotheken erstellt NuGet automatisch eine zusätzliche Routen-Konfiguration für alle Breeze-Controller in der Datei App_Start/BreezeWebApiConfig.cs. Diese statische Klasse verwendet das automatische Konfigurationsfeature von ASP.NET, um die Konfigurationsfunktion beim Start der Webapplikation zu laden:

 [assembly: WebActivator.PreApplicationStartMethod(
  typeof(BookShop.App_Start.BreezeWebApiConfig), "RegisterBreezePreStart")]  

Die Route wird in dieser generierten Konfiguration um einen Action-Parameter erweitert, der für die Verwendung im Zusammenspiel mit der Breeze-Controller-Erweiterung erforderlich ist:

public static void RegisterBreezePreStart() {
  GlobalConfiguration.Configuration.Routes.MapHttpRoute(
    name: "BreezeApi", routeTemplate: "breeze/{controller}/{action}"
); }

Um einen eigenen Breeze-Controller zu erstellen, wird als Erstes ein Web-API-2-Controller um das Attribut [BreezeController]erweitert. Dieses Attribut aktiviert das Postprocessing eingehender OData-Abfragen:

 [BreezeController]
public class BookShopController : ApiController { . . . }

Anschließend wird der Breeze-Data-Context erstellt, indem die Eigenschaft contextProvider mit einer neuen Instanz eines EFContextProviders angelegt wird:

 readonly EFContextProvider_contextProvider 
  = new EFContextProvider();

Um dem Client später die Möglichkeit zu geben, die Metadaten automatisch abfragen zu können, ist es erforderlich, eine entsprechende Get-Operation mit dem Namen MetaData hinzuzufügen, die die Metadaten mithilfe des zuvor angelegten Breeze-Data-Contextes ermittelt:

 // ~/breeze/delivery/Metadata
[HttpGet]
public string Metadata() {
  return _contextProvider.Metadata();
}

Für jeden Entity-Typen, der später über diesen Controller zur Verfügung stehen soll, wird jetzt eine Get-Operation mit dem Namen des bereitzustellenden Entity-Typen hinzugefügt. So heißt zum Beispiel die Operation zum Abfragen von Büchern Books:

 // ~/breeze/BookShop/Books
// ~/breeze/BookShop/Books?$filter=Title eq 'ASP.NET 4.0'
[HttpGet]
public IQueryable Books() {
  return _contextProvider.Context.Boooks;
}

Soll ein Zurückspeichern der Daten ermöglicht werden, wird eine zusätzliche Post-Operation für alle Entity-Typen benötigt. Diese Operation muss Post heißen:

[HttpPost]
public SaveResult SaveChanges(JObject saveBundle) {
  return contextProvider.SaveChanges(saveBundle);
}

Da Breeze alle Änderungen auf der Clientseite mitverfolgt, werden nur die geänderten Eigenschaften und ihre neuen Werte übermittelt.

Breeze-Client erstellen

Die Breeze-Clientbibliothek kann entweder direkt geladen oder mithilfe des NuGet-Pakets Breeze.Client installiert werden. Nachdem die Bibliothek geladen wurde, wird zunächst ein Entity Manager benötigt, der die Abfragen später an den Server übermittelt und die Ergebnisse bereitstellt:

 // lokalen Datacontext auf dem Client erstellen
var serviceName = "/breeze/bookshop";
var manager = new breeze.EntityManager(serviceName);

Dieser Manager fragt die zur Konfiguration erforderlichen Metadaten automatisch mit dem ersten Zugriff an. So lange keine Abfrage an den Manager gestellt wird, fragt dieser auch keine Metadaten ab.

Eine serverseitige Abfrage wird ähnlich wie eine LINQ-Query mithilfe der fromFunktion des Objekts EnityQuery erstellt. Dabei muss mindestens der Entity-Typ angegeben sein. In dem Beispiel wird die Query zusätzlich um die Category erweitert. Das bedeutet, dass die Navigationseigenschaft Category der jeweiligen Bücher mit übertragen wird:

 var query = breeze.EntityQuery.from("Books").expand("Category");
manager.executeQuery(query)
       .then(function (data) {
         . . .
}).fail(function (error) {
  . . 
});

Breeze kann auch lokal Daten abfragen, ohne den Server zu kontaktieren. Dabei werden immer nur die Daten berücksichtigt, die bereits übertragen wurden. Es erfolgt keine automatische Abfrage am Server, falls sich nicht ausreichend Daten auf dem Client befinden. Wenn eine lokale Abfrage verwendet werden soll, empfiehlt es sich daher, beim Initialisieren der App zunächst einmal alle Bücher vom Server abzufragen. Eine lokale Abfrage gibt keine Promises zurück, sondern direkt die abgefragten Daten:

 var query = breeze.EntityQuery
          .from('Books')
          .where('Id', '==', 4711)
          .expand("Category");
var books = manager.executeQueryLocally(query);

Nachdem Änderungen an den Daten vorgenommen wurden, können diese Änderungen wieder an den Web-API-Server zurück übertragen werden, sofern dieser die Save-Operation implementiert. Hierzu wird die Funktion saveChanges() des Managers aufgerufen. Alternativ können Änderungen durch Aufrufen der Funktion rejectChanges() rückgängig gemacht werden.

Hinweis: Breeze ist nicht darauf ausgelegt, automatisch zu erkennen, dass der Browser offline ist. Wenn eine Pufferung der Daten erfolgen soll, bis der Server das nächste Mal online ist, muss diese Prüfung und die Sicherung der Daten manuell implementiert werden. Der Breeze-Manager stellt hierfür die Methoden exportEntities und importEntities zur Verfügung, die im Zusammenspiel mit dem Local Storage verwendet werden können, um alle Änderungen zu sichern, bis der Server das nächste Mal erreichbar ist.

Breeze im Zusammenspiel mit Angular

Viele Single Page Applications verwenden Angular. Breeze hat eine eigene Implementierung für die zurückgegebenen Promises. Um nicht bei jedem Zugriff auf Breeze eine Konvertierung zwischen einem Breeze Promise und einem Angular Promise durchführen zu müssen, gibt es eine Erweiterung, die einen Breeze-Provider für Angular in einem entsprechenden Modul zur Verfügung stellt. Wird dieser Provider verwendet, um auf Breeze zuzugreifen, wird automatisch eine Instanz von Breeze bereitgestellt, die Angular Promises verwendet.

Hierzu ist zunächst die NuGet-Bibliothek Breeze.Angular zu installieren und zu laden. Anschließend muss das Modul breeze.angular geladen werden. Der Zugriff auf die Breeze-Bibliothek erfolgt, indem der Breeze-Provider verwendet wird, um eine Instanz von Breeze mithilfe der Dependency Injection einzufügen. In der Beispielanwendung wird das entsprechende Modul bei der Erstellung des Servicemoduls geladen und verwendet:

 angular.module('bookShop.services', ['breeze.angular']) 
.factory('datacontext', ['breeze' , 'common',  function(breeze, common) {
  ...
}

Anschließend wird der Breeze-Manager konfiguriert, um mit dem Web-API zusammenzuspielen:

 breeze.NamingConvention.camelCase.setAsDefault();
var serviceName = "/breeze/BookShop/";
var manager = new breeze.EntityManager(serviceName);

Da die Namenskonvention in .NET besagt, dass öffentliche Eigenschaften mit einem Großbuchstaben beginnen, wird Breeze so konfiguriert, dass es automatisch eine Konvertierung der Anfangsbuchstaben der Eigenschaften der Entitys bei der Übertragung nach camelCase übernimmt.

Um Bücher oder Leser abfragen zu können, werden zwei entsprechende Funktionen bereitgestellt, die die jeweilige Abfrage abhängig von den verwendeten Parametern dynamisch erweitern können (Listing 1).

Listing 1
getBooks: function(skip, take, orderBy, reverse) {
  var query = breeze.EntityQuery.from("Books");
  if (orderBy) {
    query = (reverse || false) 
    ? query.orderByDesc(orderBy) 
    : query.orderBy(orderBy);
  }
  if (skip || take) {
    query = query.skip(skip || 0).take(take || 10).inlineCount();
  }
  return manager.executeQuery(query);
},

getReaders: function(skip, take, orderBy, reverse) {
  var query = breeze.EntityQuery.from("Readers");
  if (orderBy) {
    query = (reverse || false) 
    ? query.orderByDesc(orderBy) 
    : query.orderBy(orderBy);
  }
  if (skip || take) {
    query = query.skip(skip || 0).take(take || 10).inlineCount();
  }
  return manager.executeQuery(query);

Für die Detailansicht wird jeweils eine weitere Funktion bereitgestellt, die ein einzelnes Element abfragen kann, sowie je eine Funktion zum Speichern und eine Funktion zum Verwerfen der Änderungen (Listing 2).

 Listing 2
getBookById: function(id) {
  return manager.getEntityByKey('Book', id);
},

getReaderById: function(id) {
  return manager.getEntityByKey('Reader', id);
},

save: function() {
  return manager.saveChanges();
},

cancel: function() {
  manager.rejectChanges();
}

Zusammenfassung

Mit der Breeze-Bibliothek lassen sich im Zusammenspiel mit anderen JavaScript-Bibliotheken wie z. B. Angular sehr leistungsfähige datenbankbasierte Anwendungen erstellen, die sowohl lokale Abfragen als auch eine Offlineunterstützung enthalten können.

Aufmacherbild: A grassy field in front of a lake while the wind blows via Shutterstock.com / Urheberrecht: TAGSTOCK1

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -