Das ist der dritte und letzte Teil eines kleinen Bogens ums Lesen einer Many-to-many-Beziehung in Power Pages, und er schließt den Kreis auf befriedigende Weise.
Teil eins las N:M mit Liquid FetchXML: serverseitig gerendert, aber beim Seitenaufbau vorgeladen. Teil zwei verlagerte denselben Read in Server Logic, on demand vom Client aufgerufen, keine Abfrage im Browser, dafür ein Client-Roundtrip mit CSRF-Token. Jede Seite hatte ihren Preis.
Der serverlogic-Liquid-Tag nimmt beide Preise auf einmal weg. Dieselbe serverseitige Operation aus Teil zwei, aufgerufen aus Liquid, während die Seite rendert. Kein Client-Call, kein CSRF-Token, und die Abfrage berührt den Browser trotzdem nie. Microsoft hat das 2026 ausgeliefert, und es ist genau das Stück, das aus dem Ganzen einen Kreis macht: wiederverwendbare, testbare Server-Logik, direkt in die Seite gerendert wie damals das FetchXML.
Der Tag
{% serverlogic name: 'installation-products',
operation: 'getInstallationProducts',
output: result %}
{% if result.success %}
<ul>
{% for inst in result.data.installations %}
<li>{{ inst.name | escape }}</li>
{% endfor %}
</ul>
{% endif %}
Vier Parameter: name (der Server-Logic-Record), operation (die auszuführende JavaScript-Funktion, jede von Ihnen definierte, nicht nur get), input (optionaler String, JSON wenn Sie Struktur brauchen) und output (die Liquid-Variable, die das Ergebnis erhält). Das Ergebnisobjekt trägt success, status_code (500 bei Ausführungsfehler), data (das geparste JSON Ihrer Operation) und raw_result (die ungeparste Antwort, falls der Rückgabewert kein gültiges JSON ist).
Die entscheidende Zeile aus Microsofts Doku: dieser serverseitige Aufruf "doesn't require a client-side HTTP request or Cross-Site Request Forgery (CSRF) token." Weisen Sie den Server-Logic-Record einer Web Role zu, auf die der aktuelle Nutzer Zugriff hat, den Rest erledigt der Tag zur Render-Zeit.
Der Full Circle, live
Ich habe genau das im Demo-Portal gebaut: ein Web Template mit dem obigen Tag, das eine Operation aufruft, die die N:M-Beziehung aus Teil eins liest und eine kompakte Liste zurückgibt. Als angemeldeter Portal-Nutzer rendert die Seite die verknüpften Produkte serverseitig:
Dieselben Daten wie in Teil eins (Pump A, Valve B unter einer Installation), derselbe serverseitige Dataverse-Connector wie in Teil zwei, jetzt direkt von Liquid in die Seite gerendert. Das ist der geschlossene Kreis.
Eine Funktion, zwei Kontexte
Hier ist das Detail, das Ihnen einen Nachmittag spart. Dieselbe Server-Logic-Funktion lässt sich auf zwei Wegen aufrufen, vom Client über HTTP und aus Liquid, aber sie liest ihre Eingabe aus zwei verschiedenen Quellen:
- Über HTTP aufgerufen:
Server.Context.QueryParameters["id"], dazuBody,Headers,HttpMethod. - Aus Liquid aufgerufen:
Server.Context.Input, der String, den Sie iminput-Parameter des Tags übergeben haben.
Wenn Sie eine Operation für beide Einstiegspunkte schreiben, verzweigen Sie nach der Eingabequelle. QueryParameters ist bei einem Liquid-Aufruf leer, und Input ist über HTTP leer.
Zwei Dinge, die die Doku nicht ausbuchstabiert
Ich bin beim Bauen des Live-Beispiels über beide gestolpert, damit Sie es nicht müssen.
Der Connector liefert einen Envelope. Wenn Sie das Ergebnis innerhalb der Operation verarbeiten (statt es durchzureichen), gab mir Server.Connector.Dataverse.RetrieveMultipleRecords einen String der Form { "StatusCode": 200, "Body": "..." }, wobei Body die OData-JSON als String ist. Sie parsen zweimal: den Envelope, dann Body, dann erreichen Sie value. Die Doku-Beispiele geben das Connector-Ergebnis direkt zurück und zeigen das nie, deshalb bekommen Sie beim ersten Versuch, über result.value zu iterieren, nichts.
function getInstallationProducts() {
var options = "$select=pwrprtl_installation_name" +
"&$expand=pwrprtl_installation_product($select=pwrprtl_product_name)";
var res = Server.Connector.Dataverse.RetrieveMultipleRecords("pwrprtl_installations", options, true);
var envelope = (typeof res === "string") ? JSON.parse(res) : res;
var odata = (typeof envelope.Body === "string") ? JSON.parse(envelope.Body) : envelope.Body;
var rows = (odata && odata.value) ? odata.value : [];
var installations = [];
for (var i = 0; i < rows.length; i++) {
var prods = rows[i].pwrprtl_installation_product || [];
var names = [];
for (var j = 0; j < prods.length; j++) { names.push(prods[j].pwrprtl_product_name); }
installations.push({ name: rows[i].pwrprtl_installation_name, products: names });
}
return JSON.stringify({ installations: installations });
}
Kompilierte Server Logic wird gecacht. Nachdem Sie den Code geändert haben, liefern der Endpunkt und der Liquid-Tag weiter die vorige Version, bis der Site-Cache geleert wird (/_services/about, Clear cache, mit Administrator-Web-Rolle). Wenn eine Änderung nicht zu greifen scheint, ist es der Cache, nicht Ihr Code.
Sicherheit: kein CSRF heißt nicht keine Verantwortung
Auf das CSRF-Token zu verzichten ist hier sicher, weil der Aufruf den Server nie verlässt; es gibt keine Cross-Site-Anfrage, die man fälschen könnte. Aber die Operation läuft weiter im Kontext des aktuellen Nutzers und respektiert Web Roles und Tabellenberechtigungen, und die Autorisierungsentscheidungen liegen weiter bei Ihnen. Microsoft ist deutlich: Behandeln Sie Eingabe und Rückgaben als untrusted. Validieren Sie Server.Context.Input, bevor er eine Abfrage berührt, und wenden Sie den escape-Filter an, bevor Sie einen zurückgegebenen String in HTML rendern, genau wie im Tag oben.
Und der eine echte Preis: der Tag läuft während des Seitenaufbaus, eine langsame Dataverse-Abfrage oder ein externer Call erhöht also die Page-Response-Zeit. Das ist der Handel fürs serverseitige Rendern. Wenn Reaktionsschnelligkeit wichtiger ist als der erste Seitenaufbau, bleiben Sie beim Client-Call aus Teil zwei.
Drei serverseitige Wege, eine Entscheidung
| Sie wollen | Nutzen Sie |
|---|---|
| N:M (oder jeden Read) beim Seitenaufbau gerendert, einfache Abfrage | Liquid FetchXML (link-entity / intersect) |
| Wiederverwendbare/testbare Logik beim Seitenaufbau gerendert | {% serverlogic %}-Tag (Server.Context.Input) |
| Dieselbe Logik on demand, nach dem Laden | Server Logic über HTTP (safeAjax, CSRF, QueryParameters) |
| N:M schreiben (verknüpfen / trennen) | Portal Web API $ref |
FetchXML ist der schnellste Weg für eine einfache Abfrage. Sobald die Logik wiederverwendbar oder testbar sein soll, gibt Ihnen {% serverlogic %} das zur Render-Zeit, und dieselbe Operation beantwortet nach dem Laden einen Client-Call. Drei Werkzeuge, ein Denkmodell: entscheiden Sie danach, wann der Nutzer die Daten braucht und wie viel Logik dahintersteckt. Das ist der Bogen, geschlossen.
Verwandte Artikel
N:M on demand mit Server Logic
Teil zwei: derselbe Read, on demand vom Client aufgerufen, keine Abfrage im Browser.
Artikel lesen → DevelopmentN:M lesen: Web API vs. FetchXML
Teil eins: wo die Web API bei N:M stolpert, und der Liquid-FetchXML-Workaround.
Artikel lesen → SecurityPower Pages Security-Architektur
Web Roles und Tabellenberechtigungen: das Modell, das Server Logic weiter regiert, CSRF hin oder her.
Artikel lesen →Quellen
- Microsoft Power Platform Blog: Extend Liquid with Server Logic in Power Pages (die Ankündigung)
- Microsoft Learn: serverlogic Liquid object (Syntax, Parameter, Ergebnis-Attribute)
- Microsoft Learn: Author server logic, call from Liquid ("doesn't require a client-side HTTP request or CSRF token"; Render-Zeit-Kosten)
- Microsoft Learn: Server objects, Context (
Server.Context.Inputfür Liquid vs.QueryParametersfür HTTP) - Hands-on im Power-Portals-Demo-Portal: ein Web Template, das eine Server-Logic-Operation per
{% serverlogic %}aufruft und die N:M-Liste serverseitig rendert, live verifiziert (result.success true, status_code 200)