Logo Fabasoft

Fabasoft

Etablierte Firma

Dynamically created OData Datasource

Description

Gerald Leitner von Fabasoft spricht in seinem devjobs.at TechTalk über das Thema „Dynamically created OData Datasource“. Anhand eines Beispiels in einer Live-Coding Session, werden verschiedene Aspekte demonstriert.

Beim Videoaufruf stimmst Du der Datenübermittlung an YouTube und der Datenschutzerklärung zu.

Video Zusammenfassung

In Dynamically created OData Datasource zeigt Gerald Leitner (Fabasoft), wie man OData in ASP.NET 5 zunächst klassisch aktiviert (Microsoft.AspNetCore.OData 7.x, AddOData, ODataConventionModelBuilder, EnableQuery und ein Key im Modell) und dann radikal dynamisiert. Per Middleware werden aus JSON-Metadaten zur Laufzeit Controller und Entitäten generiert, mit Roslyn kompiliert, via Application Parts registriert, die Route transformiert und das OData-EDM neu aufgebaut – inklusive $metadata und Query-Optionen wie $top. Das Demo liefert sprachspezifische Endpunkte (z.B. Deutsch/Katze, Englisch/Cat) und zeigt, wie sich hochflexible, erweiterbare OData-APIs für Tools wie Power BI umsetzen lassen.

Dynamische OData-Datenquelle in ASP.NET 5: Wie Gerald Leitner (Fabasoft) OData-Modelle und -Controller zur Laufzeit generiert

Überblick: Von statischem Beispiel zur maximalen Flexibilität

In der Session „Dynamically created OData Datasource“ mit Speaker Gerald Leitner (Fabasoft) ging es um ein ambitioniertes Ziel: OData nicht nur „klassisch“ mit statischen Modellen und fix eingebauten Controllern zu betreiben, sondern zur Laufzeit dynamisch zu erzeugen – inklusive Modell, Controller, OData-Registrierung und Routen-Transformation. Oder, wie Leitner es formuliert: „Wir wollen die größtmögliche Flexibilität, die Dynamik in die OData reinbringen.“

Wir von DevJobs.at haben die Live-Demo verfolgt – vom bekannten „WeatherForecast“-Template über die Aktivierung von OData 7.x und Query-Optionen bis hin zum Herzstück: einer Middleware, die bei eingehenden Requests Sprache und Ressource erkennt, Quellcode generiert, mit dem C#-Compiler zur Laufzeit Assemblies baut, als Application Part in die laufende ASP.NET-Anwendung einhängt, die Routen umschreibt und OData das passende EDM-Modell zur Verfügung stellt. Das Ergebnis sind sprachspezifische Endpunkte wie „/deutsch/katze“ oder „/englisch/cat“, die eigene Models, Controller und OData-Metadaten liefern – vollständig zur Laufzeit erzeugt.

Kurz zu OData und dem Ausgangspunkt

Leitner ordnete OData zunächst ein: OData ist ein HTTP-basiertes Protokoll zur standardisierten Datenabfrage – ideal, wenn man große Datenmengen über einheitliche Schnittstellen für Tools wie Power BI bereitstellen möchte. Der Standard wird von OASIS weiterentwickelt; aktuell ist OData 4.0 relevant. In der Praxis bedeutet das: Clients können per URL-Query-Optionen wie „$top“, „$filter“, „$orderby“ und Co. serverseitig vorkonfigurierte Datenquellen flexibel abfragen.

Als Ausgangssituation diente das standardmäßig generierte ASP.NET-Template mit einem „WeatherForecast“-Modell und -Controller. Dieser liefert zufällig generierte Daten als JSON. Noch ohne OData – und damit natürlich ohne die bekannten Query-Features oder Metadaten.

Schritt 1: OData im statischen Projekt aktivieren (7.x)

Bevor es in die dynamische Welt geht, aktivierte Leitner OData im statischen Beispiel. Das half, die Bausteine und Anforderungen zu verstehen, die später dynamisch nachgebildet werden müssen.

  • Paket: Er ergänzte „Microsoft.AspNetCore.OData“ (Version 7.x). 8.0 existiert als Preview, unterscheidet sich aber im Handling – daher die Live-Demo mit 7.x.
  • Services: In „Startup“ wird OData via „AddOData“ aktiviert.
  • Endpoints: In „UseEndpoints“ werden OData-Features eingeschaltet – konkret „Select“, „Filter“, „Count“, „Top“. Zudem wird eine OData-Route registriert, etwa unter „/odata“.
  • EDM-Modell: OData benötigt ein Modell. Leitner erzeugte es über den „ODataConventionModelBuilder“. Wichtig: Per „EntitySet“ wird festgelegt, welche Entitäten dem OData-Modell hinzugefügt werden (z. B. „WeatherForecast“). So lässt sich ein größeres Domainmodell gezielt zuschneiden.
  • Controller: Aus dem vorhandenen Controller wird ein OData-Controller. Zentral sind das Attribut „EnableQuery“ auf der Methode sowie ein „Key“ im Modell (Primary Key). In der Demo diente das Datum als Key.

Der Effekt: Unter „/odata/weatherforecast“ liefert der Controller Daten inklusive OData-Metadaten. Via „$top=1“ oder „$orderby=Date“ lassen sich die Antworten filtern und sortieren. In der Metadaten-URL erkennt man, welche Entitäten und Eigenschaften das Modell umfasst.

Warum diese statische Aktivierung wichtig ist? Sie präzisiert die Anforderungen an das dynamische Szenario: OData will ein Modell (IEDM), braucht Controller-Endpunkte, Query-Features, eine Route – und in der Praxis müssen diese Dinge korrekt zusammenkommen.

Zielbild: Alles dynamisch – Modell, Controller, Route, OData-Registrierung

„Wir wollen uns von dem statischen Modell und die statischen Controller wegbewegen auf eine extrem dynamische Anwendung“, fasst Leitner das Ziel zusammen. Das Big Picture:

  • Die Anwendung startet ohne vordefinierte OData-Entitäten und Controller.
  • Ein eingehender Request – etwa „/deutsch“ oder „/deutsch/bär“ – triggert eine Middleware.
  • Diese Middleware analysiert Sprache und Ressource, lädt die dafür nötigen Metadaten (in der Demo aus JSON-Dateien wie „katze“ und „bär“), generiert C#-Code für Model und Controller, kompiliert diesen zur Laufzeit zu einer Assembly, hängt sie als Application Part in die laufende App, teilt dem Routing die neuen Controller mit, schreibt die Route um und gibt OData ein dynamisch aufgebautes EDM-Modell.
  • Das Ergebnis: Sprach- und ressourcenspezifische Endpunkte, die vollständig zur Laufzeit entstehen und trotzdem die vollen OData-Fähigkeiten (inklusive Metadaten) bieten.

Die Pipeline im Detail: Middleware als Orchestrator

Der Kern liegt in einer selbst geschriebenen Middleware, die Leitner im „Configure“ (Startup) via „UseMiddleware“ einbindet. Sie übernimmt mehrere Aufgaben:

1) Request-Analyse

  • Über den „HTTPContext.Request.Path.Value“ wird der Pfad ausgewertet.
  • Das erste Segment wird als Sprache interpretiert (z. B. „deutsch“, „englisch“), das zweite optional als Ressource (z. B. „bär“, „katze“).

2) Generator aufrufen

  • Ein Controller-Generator produziert aus Metadaten lauffähigen C#-Quellcode. Woher die Metadaten kommen, ist offen – Leitner betont die Freiheit: „Wir sind in unseren Möglichkeiten nicht eingeschränkt.“ In der Demo stammen sie aus JSON-Dateien in einem „Resources“-Ordner.
  • Beispiel „katze.json“: Enthält sprachabhängige Namen („Katze“/„Cat“) und die Struktur der Entität (Eigenschaften wie „Name“, „Gewicht“, „Farbe“, „Geburtstag“, „Anzahl an Impfungen“). „bär.json“ ist reduzierter (z. B. „Name“, „Gewicht“).

3) C#-Code programmatisch erzeugen

  • Der Generator baut Quellcode per String-Builder zusammen: mit passenden „using“-Direktiven, Namespace, Klassen für Model und Controller.
  • Controller-Namenskonzept: „{Language}_{Name}Controller“ – z. B. „deutsch_bär“ bzw. „deutsch_katze“. Die Klassen enthalten u. a. eine „Get“-Methode mit „EnableQuery“.
  • Model: Für jede Ressource (z. B. „Katze“) wird eine Klasse erzeugt, deren Eigenschaften aus dem JSON abgeleitet werden. Der „Key“ (Primary Key) wird per Data Annotation gesetzt.
  • Die Implementierung orientiert sich an einem Basis-Controller, auf dessen „Get“ die generierten Controller durchreichen. Entscheidend ist, dass diese Controller als OData-Controller fungieren und vom Framework erkannt werden können.

4) Zur Laufzeit kompilieren

  • Der Generator erzeugt einen Syntax-Tree und stellt Referenzen bereit. Praktischer Kniff: Er übernimmt die Referenzen aus der aktuellen AppDomain der laufenden Web-Anwendung (inkl. OData und ASP.NET Core), sodass die neu erstellte Assembly exakt dieselbe Umgebung wie das Hostprojekt nutzt.
  • Die Kompilation erfolgt mit „CSharpCompilation.Create“ (aktuelle Sprachversion, optimiert auf „Release“). Der Output wird via „Emit“ als DLL auf die Festplatte geschrieben.
  • Nach erfolgreichem Build lädt die Anwendung die Assembly.

5) Ins laufende System einhängen

  • Application Parts: Mit dem „ApplicationPartManager“ werden neue Assemblies dynamisch hinzugefügt. Das Feature kam mit ASP.NET Core, um Teile von Anwendungen modular nachzuladen.
  • Wichtig: Das Routing und die Action-Deskriptoren wissen zunächst nichts von der neuen Assembly. Leitner setzt daher in einer eigenen Implementierung eines „IActionDescriptorChangeProvider“ ein Flag „HasChanged = true“ und löst über eine Token-Quelle ein Rebuild aus. Ergebnis: ASP.NET Core lädt die Application Parts neu ein und erkennt die neuen Controller.

6) Routen dynamisch transformieren

  • User rufen intuitive Pfade auf („/deutsch/bär“, „/englisch/cat“). Die Controller heißen jedoch „deutsch_bär“ oder „englisch_cat“. Deshalb mappt Leitner eine „Dynamic Controller Route“ mit einem Transformer:
  • „Controller“ wird zur Laufzeit auf „{language}_{animal}“ umgeschrieben.
  • „Action“ wird auf „Get“ gesetzt.
  • Dadurch landen Requests auf den korrekt benannten, dynamisch erzeugten Controllerklassen und Methoden.

7) OData-Modell zur Laufzeit registrieren

  • OData „weiß“ zunächst nichts von den neuen Entitäten. Daher ruft die Middleware ebenfalls die OData-Registrierung auf – analog zum statischen Beispiel, jedoch dynamisch:
  • Aus der frisch erzeugten Assembly werden die relevanten Typen reflektiert (z. B. alle Klassen, die eine „Entity“ repräsentieren – in der Demo über eine abgeleitete Basis-Klasse kenntlich gemacht).
  • Für jeden Entity-Typ wird am „ODataConventionModelBuilder“ ein „EntitySet“ hinzugefügt.
  • OData bekommt dieses zur Laufzeit generierte „IEDMModel“ – samt aller gerade hinzugekommenen Entitäten.
  • Ergebnis: Auch „$metadata“ spiegelt die dynamischen Typen wider.

Die Demo: Deutsch/Englisch, Bär/Katze und Metadaten live

Die Live-Demo veranschaulichte das gesamte Zusammenspiel:

  • Aufruf „/deutsch/bär“: Beim ersten Request erzeugt die Middleware „Deutsch.dll“ – der Build-Prozess ist im Dateisystem sichtbar. Der Controller „deutsch_bär“ wird registriert, die Route umgeschrieben und das OData-Modell aufgebaut. Die Antwort enthält Daten und OData-Metadaten.
  • Aufruf „/englisch/cat“: Analog baut die Anwendung „Englisch.dll“. Unterschiedlich sind die Eigenschaftsnamen – im englischen Modell „Name“, „Weight“, „Color“, „Birthday“, „Vaccinations“, im deutschen Gegenstück „Name“, „Gewicht“, „Farbe“, „Geburtstag“ etc.
  • „$metadata“: Der OData-Metadaten-Endpunkt zeigt genau die dynamisch hinzugefügten EntitySets und Properties – wie sie aus den JSON-Beschreibungen abgeleitet wurden.
  • Query-Optionen: Aktiv sind Standardfeatures wie „$top“. Leitner demonstrierte u. a. „$top=1“. Optional lassen sich auch „$orderby“ oder „$filter“ verwenden – abhängig von Konfiguration und Modell.
  • Robustheit: Groß-/Kleinschreibung im Pfad handhabte die Demo tolerant („deutsch“, „Deutsch“, „katze“, „Katze“).

Leitners Fazit: „Damit ist der Beweis vollbracht. Wir können eine komplett dynamische OData-Solution machen.“

Wichtige technische Erkenntnisse aus der Session

  • OData benötigt ein explizites EDM-Modell: Auch in der dynamischen Variante ist ein „ODataConventionModelBuilder“ der zentrale Baustein, um EntitySets zu registrieren. Ohne Modell keine OData-Metadaten, und viele Clients (z. B. Power BI) verlassen sich darauf.
  • „EnableQuery“ und „Key“ sind Pflicht im Controller/Modell: Das Attribut „EnableQuery“ aktiviert die OData-Verarbeitung der Rückgabe; ein Primary Key (z. B. per Data Annotation „Key“) ist nötig, damit OData korrekt mit Entitäten umgehen kann.
  • Middleware ist der richtige Ort: Die Generierung muss vor dem eigentlichen Routing passieren. Nur so können Assemblies hinzugefügt, das Routing informiert und OData-Modelle aufgebaut werden, bevor die Anfrage final verarbeitet wird.
  • Application Parts plus IActionDescriptorChangeProvider: Neue Assemblies werden mit dem „ApplicationPartManager“ registriert. Damit das Routing neue Controller wahrnimmt, muss ein Change-Provider „HasChanged“ signalisieren und eine Token-Quelle invalidieren. Leitner begründet diesen Mechanismus auch mit Performance: Frameworks prüfen nicht bei jeder Anfrage alle Teile neu, sondern reagieren auf solche Change-Signale.
  • Routen-Transformation entkoppelt URL von Klassennamen: Userfreundliche Pfade („/deutsch/bär“) werden durch einen Transformer in echte Controller-/Action-Namen („deutsch_bär“, „Get“) übersetzt.
  • Metadatenquellen sind frei wählbar: In der Demo sind die JSON-Dateien bewusst simpel. In der Praxis könnten die Metadaten aus Webservices, Datenbanken oder anderen Repositories kommen – der Generator ist nicht gebunden.
  • OData 7.x vs. 8.0: Die Session setzt auf 7.x, weil die 8.0-Preview Änderungen mit sich bringt. Das ist für die Konzeption wichtig, da die gezeigten APIs und Patterns an 7.x ausgerichtet sind.

Praktischer Leitfaden: So nähert man sich der dynamischen Lösung

Wer die Ideen aus der Session anwenden möchte, kann sich an folgendem Fahrplan orientieren:

1) Statische OData-Basis verstehen

  • OData-Paket (7.x) integrieren.
  • „AddOData“ in den Services; OData-Features („Select“, „Filter“, „Count“, „Top“) in „UseEndpoints“ einschalten.
  • OData-Route (z. B. „/odata“) registrieren.
  • „ODataConventionModelBuilder“ einsetzen und ein „EntitySet“ für die erste Entität registrieren.
  • Controller-Methode mit „EnableQuery“ ausstatten; im Modell „Key“ definieren.

2) Middleware vorbereiten

  • Eigene Middleware einhängen, die den „Request.Path“ ausliest.
  • Sprache und (optional) Ressource aus dem Pfad extrahieren.

3) Metadatenquelle definieren

  • Für die Demo: JSON-Strukturen analog „katze“/„bär“ (Eigenschaftsnamen, Typen, Lokalisierung).
  • Für die Praxis: Webservices, Datenbank oder andere externe Quellen sind ebenfalls möglich.

4) Quellcode-Generator implementieren

  • Code als String generieren: „using“-Direktiven, Namespace, Model-Klasse(n), Controller-Klasse(n).
  • Namenskonvention „{language}_{resource}Controller“ umsetzen.
  • „EnableQuery“ auf „Get“ setzen; Primary Key per Attribut.

5) Zur Laufzeit kompilieren

  • Syntax-Tree erstellen; Referenzen aus der aktuellen AppDomain übernehmen.
  • Mit „CSharpCompilation.Create“ bauen; per „Emit“ auf Disk schreiben (z. B. „Deutsch.dll“, „Englisch.dll“).
  • Assembly laden.

6) Application Part registrieren und Routing neu aufbauen

  • „ApplicationPartManager“: Neue Assembly als Application Part hinzufügen.
  • „IActionDescriptorChangeProvider“: „HasChanged = true“ setzen; Token-Quelle invalidieren, damit ASP.NET Core die neuen Controller erkennt.

7) Routen-Transformation aktivieren

  • Dynamic Controller Route mappen; Transformer setzt „Controller = {language}_{resource}“, „Action = Get“.

8) OData-Modell dynamisch registrieren

  • Aus der frisch geladenen Assembly Entity-Typen reflektieren (z. B. abgeleitet von einer Basis-Entität).
  • Für jeden Typ „EntitySet“ am „ODataConventionModelBuilder“ hinzufügen.
  • OData-Route (mit diesem zur Laufzeit erzeugten EDM) registrieren.

9) Verifizieren

  • Endpunkt aufrufen („/deutsch/katze“, „/englisch/cat“), Daten prüfen.
  • „$metadata“ inspizieren – die Metadaten sollten die dynamischen Entitäten enthalten.
  • Query-Optionen testen (z. B. „$top=1“).

Hinweise, Grenzen und Chancen

Was wir aus der Session mitnehmen:

  • Fehlerbehandlung und Authentifizierung: Leitner ließ diese Punkte bewusst schlicht. In der Middleware ließe sich eine Authentifizierung leicht ergänzen. Für produktive Szenarien sollten Validierung, Logging und robuste Fehlerpfade aufgebaut werden.
  • Konsistenz von Schlüsseln: OData erwartet einen Key. Wer Metadaten aus externen Quellen bezieht, muss sicherstellen, dass für jede Entität ein sinnvoller Schlüssel vorhanden ist.
  • Modellgröße und Zuschnitt: Der Builder ermöglicht es, nur Teilmodelle freizugeben. Das ist für die Mehrsprach-/Mehrdomänen-Variante wertvoll, damit jede dynamische Route exakt ihr Modell bekommt.
  • Performance-Überlegungen: Das Change-Signal per „IActionDescriptorChangeProvider“ ist nötig, damit das Routing nicht ständig alles neu scannt. Gleichzeitig sollte man bedenken, wie oft man Assemblies generiert und wann Caching sinnvoll ist.
  • Tooling-Kompatibilität: Mit korrekt registrierten Metadaten können Clients wie Power BI nahtlos auf die dynamisch erzeugten Endpunkte zugreifen – genau dafür ist OData ausgelegt.

Fazit: Dynamik bewiesen – OData als flexible, zur Laufzeit komponierte API

Die Session „Dynamically created OData Datasource“ von Gerald Leitner (Fabasoft) hat überzeugend gezeigt, dass sich OData-APIs nicht auf statische Compile-Time-Modelle reduzieren müssen. Im Gegenteil: Mit einer geschickten Middleware, einem Quellcode-Generator, Roslyn-Kompilation, Application Parts, einer Routen-Transformation und einem dynamisch gebauten OData-EDM-Modell entsteht eine API, die sich erst bei der Anfrage zusammensetzt – inklusive lokalisierter Namen, unterschiedlicher Eigenschaftsschemata und OData-Metadaten.

Das funktionierte in der Demo sowohl für „/deutsch/bär“ als auch für „/englisch/cat“ – sichtbar an den erzeugten Assemblies („Deutsch.dll“, „Englisch.dll“), den lokalisierten Eigenschaften („Gewicht“ vs. „Weight“) und den jeweils korrekten „$metadata“-Dokumenten. Oder, in Leitners Worten: „Damit ist der Beweis vollbracht.“

Wer OData für Reporting, Analytics oder Integrationsszenarien nutzt, erhält damit eine interessante Blaupause: Statt alle Varianten im Voraus zu kompilieren, können Modelle und Controller on demand entstehen – präzise zugeschnitten auf Sprache, Domäne oder Mandanten. Leitner hat ein GitHub-Repository mit dem gezeigten Code angekündigt; hilfreich, um die vorgestellten Bausteine im eigenen Kontext nachzubauen und anzupassen.

Für uns als DevJobs.at-Redaktion ist diese Session ein Lehrstück darüber, wie man vertraute Frameworks (ASP.NET Core, OData 7.x) mit den richtigen Hebeln in eine äußerst flexible Architektur verwandelt – ohne Hacks, sondern mit den vorgesehenen Erweiterungspunkten des Frameworks. Das Ergebnis ist eine dynamische OData-Solution, die sowohl technisch elegant als auch praktisch nutzbar ist.