Sie bauen eine Komponente in Power Pages. Sie soll drei Dinge gleichzeitig können.
Schnell rendern, ohne Spinner. Sicher bleiben, sodass ein Benutzer immer nur die eigenen Daten sieht und ändert. Und sich aktualisieren, ohne die Seite neu zu laden, denn ein Schalter, der die Seite neu lädt, ist kein Schalter.
Wenn Sie sich für ein Werkzeug entscheiden, geben Sie eines dieser drei Ziele auf. In diesem Beitrag geht es darum, keines aufzugeben.
Warum ein Werkzeug allein nie reicht
Reines Liquid rendert auf dem Server. Microsoft sagt es direkt: Liquid wird serverseitig verarbeitet und die Ausgabe als reines HTML an den Endanwender geliefert. Ihre Daten stecken schon im ersten Byte HTML. Schnell, und auf der Leseseite sicher, weil Tabellenberechtigungen greifen. Was Liquid nicht kann, ist irgendetwas über einen Schreibvorgang durchzusetzen, denn wenn der Benutzer klickt, ist Liquid längst fertig.
Reines clientseitiges Web API gibt Ihnen Schreibvorgänge und Aktualisierungen ohne Reload. Der Preis ist eine leere Komponente, bis die erste Antwort da ist, und Sie müssen die Web API für die Tabelle über Webapi/<table name>/enabled aktivieren und die Spalten in Webapi/<table name>/fields auflisten. Damit öffnen Sie eine Tür, die für jedes Skript auf Ihrer Site offen bleibt.
Basic Forms sind sicher und konfigurationsgetrieben. Sie sind aber kein Schalter. Wer eigenes JavaScript an ein Formularfeld hängt, bekommt laut Microsoft beim Absenden die Meldung "Invalid postback or callback argument". Das sagt schon fast alles über das Interaktionsmodell.
Also: drei Werkzeuge, drei Aufgaben.
| Vorgang | Mechanismus | Warum |
|---|---|---|
| Erstes Rendern | Liquid fetchxml, serverseitig | Daten stecken im HTML, kein Spinner |
| Änderung | Server Logic über AJAX | Prüfung, die der Client nicht umgehen kann |
| Abgleich | Gezielter Refresh auf ein Server-Logic-GET, DOM neu aufbauen | Bringt optimistische Anzeige und Realität zusammen |
Ich nenne das Render-Mutate-Refresh. Entstanden ist es an einem Cockpit für Einwilligungsverwaltung, aber nichts daran ist auf Einwilligungen beschränkt. Es passt auf jede Komponente, die datengebunden, benutzerspezifisch und direkt änderbar ist.
Ebene 1: Rendern mit Liquid
{% fetchxml preferences %}
<fetch>
<entity name="my_userpreference">
<attribute name="my_topic"></attribute>
<attribute name="my_value"></attribute>
<attribute name="my_userpreferenceid"></attribute>
<filter>
<condition attribute="my_contactid" operator="eq" value="{{ user.id }}" />
</filter>
</entity>
</fetch>
{% endfetchxml %}
{% for item in preferences.results.entities %}
<div class="pref-item"
data-id="{{ item.my_userpreferenceid }}"
data-topic="{{ item.my_topic | escape }}"
data-checked="{{ item.my_value.Value }}">
<span class="pref-label">{{ item.my_topic | escape }}</span>
</div>
{% endfor %}
Zwei Syntaxhinweise, die jeweils einen Nachmittag kosten können.
Die Ergebnisse liegen unter results.entities, nicht direkt auf der Variablen. Und verwenden Sie keine selbstschließenden Tags für Attribute. Microsoft sagt es ausdrücklich: statt <attribute name="title"/> schreiben Sie <attribute name="title"></attribute> mit explizitem schließendem Tag. Beim <condition>-Element ist die Kurzform in Ordnung, bei Attributen nicht.
user.id ist die dokumentierte Eigenschaft. Das user-Objekt ist ein Entity-Objekt, und für dieses ist ID als GUID der Tabelle in Form einer Zeichenfolge dokumentiert. In viel Community-Code sehen Sie user.contactid, und das funktioniert über die allgemeine Regel zum Zugriff per logischem Namen. Namentlich dokumentiert ist aber user.id.
Warum das sicher ist: Der Filter steht serverseitig, in einem Template, das der Browser nie zu Gesicht bekommt. Der Browser erhält gerenderte Zeilen und sonst nichts. Es gibt keinen clientseitigen Filter, an dem sich manipulieren ließe, weil es keine clientseitige Abfrage gibt.
Ebene 2: Ändern über Server Logic
Lesen läuft über Liquid. Schreiben läuft über einen Serverendpunkt, mit dem der Client nicht verhandeln kann.
Auf Clientseite:
function onToggle(recordId, topic, newValue) {
shell.getTokenDeferred().done(function (token) {
$.ajax({
type: "PUT",
url: "/_api/serverlogics/preference-update",
headers: { "__RequestVerificationToken": token },
contentType: "application/json",
data: JSON.stringify({ id: recordId, topic: topic, value: newValue })
})
.done(function () { /* optimistischen Zustand behalten */ })
.fail(function () { revertToggle(recordId); });
});
}
Das URL-Muster ist dokumentiert: https://<site-url>/_api/serverlogics/<server-logic-name>. Ebenso die Anforderung, dass jeder Aufruf einer Server-Logic-API ein Token gegen Cross-Site Request Forgery enthalten muss.
Hinweis: Die Server-Logic-Dokumentation verweist auf eine Methode shell.safeAjax, veröffentlicht wird von Microsoft aber der Wrapper webapi.safeAjax, der auf shell.getTokenDeferred() aufsetzt. Wenn shell.safeAjax in Ihrer Site nicht definiert ist, liegt es daran, live bestätigt im Demo-Portal: shell stellt ajaxSafePost, getTokenDeferred und refreshToken bereit, kein safeAjax. Nutzen Sie den dokumentierten Wrapper oder rufen Sie getTokenDeferred direkt auf, wie oben.
Auf Serverseite setzt der Endpunkt durch, was der Browser nicht kann:
function put() {
const user = Server.User;
if (!user) {
return JSON.stringify({ ok: false, error: "Anmeldung erforderlich" });
}
const payload = JSON.parse(Server.Context.Body);
// Whitelist. Einem Themennamen von der Leitung traut man nicht.
const allowed = ["newsletter", "product-updates", "events"];
let permitted = false;
for (let i = 0; i < allowed.length; i++) {
if (allowed[i] === payload.topic) { permitted = true; }
}
if (!permitted) {
Server.Logger.Log("WARN: Thema abgelehnt: " + payload.topic);
return JSON.stringify({ ok: false, error: "Unbekanntes Thema" });
}
const body = JSON.stringify({ my_value: payload.value ? 1 : 0 });
const raw = Server.Connector.Dataverse.UpdateRecord(
"my_userpreferences", payload.id, body);
return JSON.stringify({ ok: true, marker: "put-v3", raw: String(raw) });
}
Server.Context.Body ist der dokumentierte Weg, den rohen HTTP-Request-Body zu lesen. Für Server.User ist dokumentiert, dass es bei anonymen Aufrufern null liefert. Damit ist die Prüfung ganz oben eine echte Schranke und keine Dekoration.
Lesen und Schreiben auf zwei Mechanismen aufzuteilen bringt Ihnen etwas Konkretes: Sie müssen die Web API auf dieser Tabelle nie aktivieren. Der Schreibpfad ist ein benannter Endpunkt mit Ihrer Prüfung darin, abgesichert über Web Roles, und es gibt keine generische CRUD-Oberfläche, die jemand entdecken könnte.
Ebene 3: Abgleich auf Wunsch
Optimistische Anzeige ist hervorragend, bis zu dem Moment, in dem sie falsch ist. Ein zweiter Tab, ein Flow im Hintergrund, eine Kollegin mit delegiertem Zugriff, und der Bildschirm lügt.
Geben Sie dem Benutzer also einen Weg zum Abgleich, über dieselbe Familie von Server-Logic-Endpunkten:
function refresh() {
shell.getTokenDeferred().done(function (token) {
$.ajax({
url: "/_api/serverlogics/preferences-list",
headers: { "__RequestVerificationToken": token }
}).done(function (response) {
const payload = response.data || response.Data;
const items = JSON.parse(payload).items;
rebuildDom(items);
});
});
}
Beachten Sie das defensive response.data || response.Data. Microsoft dokumentiert die Antworthülle mit großem Data und zeigt im Clientbeispiel kleines data. Fangen Sie beides ab und arbeiten Sie weiter.
Die Berechtigungsdisziplin, die dieses Muster verlangt
Jetzt kommt der Teil, der darüber entscheidet, ob dieses Muster sicher oder eine Belastung ist. Er hat nichts mit Code zu tun.
Ebene 1 braucht Leserecht auf der Tabelle für die Web Role des angemeldeten Benutzers. Tabellenberechtigungen gelten nicht nur für ein Template. Microsoft hält ausdrücklich fest, dass der Zugriff auf Dataverse-Datensätze eingeschränkt wird, wenn Formulare, Listen, Liquid, die Portals Web API und weitere Komponenten auf Dataverse-Tabellen zugreifen. Dasselbe Leserecht, das Ihr Liquid-Template abfragen lässt, lässt auch jede Liste, jedes Basic Form und jeden Web-API-Aufruf auf Ihrer Site abfragen.
Wenn Ihr Liquid-Filter das Einzige ist, was Benutzer voneinander trennt, dann ist Leserecht mit globalem Geltungsbereich genau eine falsch konfigurierte Liste von einem Datenleck entfernt. Microsofts Site Checker existiert unter anderem dafür und meldet Tabellenberechtigungen, die der anonymen Web Role zugewiesen wurden und ungewollt Daten offenlegen.
Zwei Wege in die Sicherheit.
Nehmen Sie den engeren Geltungsbereich. Der Bereich Kontakt wendet die Berechtigung nur auf Datensätze an, die dem angemeldeten Benutzer zugeordnet sind. Wenn Ihr Datenmodell das hergibt, nutzen Sie ihn, und lassen Sie die Plattform die Trennung durchsetzen statt Ihres Templates. Ihr Liquid-Filter ist dann eine Optimierung und keine Sicherheitsmaßnahme.
Wenn es der globale Bereich sein muss, behandeln Sie ihn als dauerhafte Verpflichtung. Jede Komponente, die diese Tabelle berührt, wird Teil Ihrer Angriffsfläche. Schreiben Sie es in die Lösungsdokumentation, und prüfen Sie es jedes Mal nach, wenn jemand eine Liste ergänzt.
Und wenn Sie ohnehin dabei sind: Steuern Sie die Oberfläche über die Berechtigungen, die die Plattform bereits kennt. Das Liquid-Objekt für Berechtigungen auf einem Datensatz ist dokumentiert und liefert can_read, can_write, can_create, can_delete, can_append, can_append_to sowie rules_exist. Wer den Schalter nur rendert, wenn can_write wahr ist, sorgt dafür, dass Oberfläche und Durchsetzung dasselbe sagen. Das erspart Ihren Benutzern viele fehlgeschlagene Schreibvorgänge.
Was Sie das kostet
Seien Sie ehrlich zu sich, bevor Sie sich festlegen.
Zwei Renderer für eine Komponente. Liquid baut das DOM beim ersten Aufbau. JavaScript baut es nach einem Refresh erneut. Das ist dasselbe Markup in zwei Ausprägungen, und es wird auseinanderlaufen. Halten Sie beides in einer Datei und prüfen Sie es gemeinsam.
Zwei Abfragesprachen. Liquid arbeitet mit FetchXML, Server Logic mit OData. Zwei Dialekte, zwei Escaping-Regime, eine Komponente.
Refresh ist eine UX-Verpflichtung. Eine Schaltfläche mit der Aufschrift "Aktualisieren" lässt Benutzer rätseln, wann sie sie drücken sollen. Lösen Sie es entweder automatisch über einen sinnvollen Auslöser wie den Fensterfokus, oder beschriften Sie so, dass die Frage beantwortet ist.
Liquid ist nicht umsonst. Microsofts eigene Hinweise zur Leistung warnen davor, große Mengen verknüpfter Datensätze zu laden oder in einem Template auf viele Beziehungen zuzugreifen, weil das die Renderzeit spürbar verschlechtert. Serverseitiges Rendern verschiebt die Wartezeit, es löscht sie nicht.
Eine Neuerung, die Sie kennen sollten
Seit dieses Muster zum ersten Mal aufgeschrieben wurde, hat Microsoft einen Liquid-Tag {% serverlogic %} ergänzt, der Server Logic während des Seitenrenderns aufruft. Die Dokumentation hält fest, dass dieser serverseitige Aufruf weder eine clientseitige HTTP-Anfrage noch ein CSRF-Token benötigt.
Das ist eine echte dritte Option für Ebene 1. Wenn Ihre Leselogik so komplex ist, dass FetchXML im Template unangenehm wird, können Sie jetzt über Server Logic rendern und behalten den sofortigen Seitenaufbau. Dieselbe Dokumentation ergänzt die naheliegende Warnung: Lange laufende Dataverse-Operationen oder externe Aufrufe in diesem Tag verlängern die Antwortzeit der Seite unmittelbar, weil die Seite darauf wartet.
Am Muster ändert das nichts. Die Ebene Render hat lediglich eine Umsetzungsvariante dazugewonnen.
Wann Sie es einsetzen und wann nicht
Setzen Sie Render-Mutate-Refresh ein, wenn die Komponente benutzerspezifischen Zustand zeigt, der Benutzer ihn ohne Seitenwechsel ändern kann, die Daten wichtig genug sind, dass clientseitiges Filtern nicht in Frage kommt, und der erste Seitenaufbau jemandem auffällt, dem er wichtig ist.
Lassen Sie es, wenn die Komponente nur liest, denn reines Liquid ist einfacher, und einfacher gewinnt. Oder wenn es um ein einzelnes Absenden geht, denn ein Basic Form ist weniger Code und weniger Risiko. Oder wenn sich die Daten ständig von außen ändern, denn dann bauen Sie eine Echtzeitanwendung, und dafür hat Power Pages die falsche Form.
Diese Grenze falsch zu ziehen ist genau die Art Entscheidung, die es wert ist, vor dem Bau auf Tragfähigkeit geprüft zu werden, nicht danach. Dafür gibt es den Architecture Workshop.
Checkliste für die Umsetzung
- Die Tabelle mit dem benutzerspezifischen Zustand bestimmen
- Den engsten Geltungsbereich der Tabellenberechtigung wählen, den das Datenmodell hergibt
- Vor der Vergabe des Leserechts jede vorhandene Liste, jedes Formular und jede Web-API-Einstellung auf dieser Tabelle prüfen
- Das Liquid-Template mit serverseitig gesetztem Benutzerfilter bauen
- Einen Server-Logic-Datensatz mit get() und put() anlegen und ihm Web Roles zuweisen
- Die Web Roles der Server Logic mit denen der Tabellenberechtigung abgleichen
- Die änderbare Oberfläche über das Liquid-Berechtigungsobjekt steuern
- Optimistisches Aktualisieren umsetzen und bei Fehlern zurückrollen
- Den Auslöser für den Refresh ergänzen und entscheiden, ob manuell oder automatisch
- Die Berechtigungsentscheidung dort dokumentieren, wo der nächste Entwickler sie findet
Bevor Sie eine Tabellenberechtigung ausweiten: Wissen Sie, was sonst noch darauf steht.
Das Security Audit ist meine Festpreisprüfung genau dieser Fläche: Tabellenberechtigungen, Web Roles, Listen, Formulare, Web-API-Einstellungen und das, was ein anonymer Besucher tatsächlich erreicht. Sie bekommen einen Befundbericht mit Schweregraden, kein Bauchgefühl.
Security Audit, ab Festpreis, mit schriftlichem BefundberichtWeitere Artikel
Power Pages Server Logic: 4 versteckte Grenzen
Die dokumentierte Sperrliste, vier Grenzen gefunden durch Bisektion, und ein Debugging-Leitfaden für eine undurchsichtige Laufzeitumgebung.
Artikel lesen → DevelopmentLiquid FetchXML vs. Web API in Power Pages
Ein Entscheidungsbaum, Leistungsvergleich und Sicherheitsmuster zwischen den beiden Datenzugriffspfaden in Power Pages.
Artikel lesen →Quellen
- What is Liquid, serverseitige Verarbeitung
- Liquid template tags, fetchxml-Syntax, results.entities, Hinweis zu selbstschließenden Tags
- Available Liquid objects, user, Entity-Objekt, Berechtigungsobjekt, Leistungshinweis
- Power Pages security overview, Liquid durch Tabellenberechtigungen geschützt
- Set table permissions, Geltungsbereiche
- Web API overview, Site Settings Webapi/<table>/enabled und fields
- Portal Web API how-to, shell.getTokenDeferred() und __RequestVerificationToken-Wrapper
- Server logic overview, URL-Muster, CSRF-Anforderung, Web Roles
- Create and manage server logic, Rollenzuweisung, Antworthülle, serverlogic-Tag
- Server objects, Server.Context.Body, Server.User, Server.Connector.Dataverse
- Add custom JavaScript to a form, Hinweis zu Invalid postback
- Site Checker configuration issues