Der Use Case ist banal: Zu einer Installation die verknüpften Produkte anzeigen, verbunden über eine Many-to-Many-Beziehung. Man greift zur Portal Web API, ruft den Navigationspfad ab, und bekommt einen Fehler statt Daten.
Danach beginnt die übliche Suche an den falschen Stellen: Site Settings, Berechtigungen, Schreibweise der Navigation Property. Die Wahrheit ist differenzierter als "die Web API kann kein N:M". Dieser Beitrag zeigt zwei Wege: was die Portal Web API wirklich kann (und wo sie stolpert) und wie Liquid FetchXML das N:M-Lesen serverseitig löst. Beides habe ich im Demo-Portal nachgestellt (Enhanced Data Model, zwei eigene Tabellen Installation und Produkt mit N:M-Beziehung). Einen dritten Weg, Server Logic (on-demand, serverseitiges JavaScript), behandle ich in einem eigenen Folge-Post.
Weg 1: Portal Web API, was sie kann und wo sie stolpert
Die Portal Web API (/_api/...) bietet laut Microsoft ein Subset der Dataverse-Operationen: Lesen, Anlegen, Aktualisieren/Löschen und Associate/Disassociate. $expand ist dokumentiert für single-valued (N:1) und collection-valued (1:N) Navigation Properties, letzteres aber nur eine Ebene tief. Ein N:M-$expand-Beispiel fehlt in der Portal-Doku komplett.
Der belastbare, dokumentierte N:M-Haken ist ein Known Issue. Microsoft schreibt wörtlich:
"Users get a CDS error if they invoke a GET Web API request for tables that have multiple levels of one-to-many or many-to-many table permissions when Parental, Contact, or Account scopes add more conditions to the query. To resolve this issue, use FetchXML in the OData query."
Genau dieses "use FetchXML" ist der offizielle Ausweg, dazu gleich. Was über die Portal Web API dagegen sauber funktioniert, ist das Verknüpfen und Lösen von N:M-Beziehungen über $ref (im Demo bestätigt):
webapi.safeAjax({
type: "POST",
url: "/_api/pwrprtl_installations(" + installationId + ")/pwrprtl_installation_product/$ref",
contentType: "application/json",
data: JSON.stringify({
"@odata.id": baseUrl + "/_api/pwrprtl_products(" + productId + ")"
})
});
Merksatz: N:M schreiben (associate) über die Web API ist unproblematisch. Das komfortable N:M-Lesen über $expand ist es nicht: dafür gibt es den FetchXML-Weg.
Weg 2: Liquid FetchXML, Microsofts eigener Workaround
FetchXML löst N:M serverseitig über verschachtelte link-entity-Elemente, wobei die automatische Intersect-Tabelle mit intersect="true" als Join dient. Im Web Template:
{% fetchxml products %}
<fetch>
<entity name="pwrprtl_product">
<attribute name="pwrprtl_product_name" />
<link-entity name="pwrprtl_installation_product" from="pwrprtl_productid" to="pwrprtl_productid" intersect="true">
<link-entity name="pwrprtl_installation" from="pwrprtl_installationid" to="pwrprtl_installationid">
<filter>
<condition attribute="pwrprtl_installationid" operator="eq" value="{{ installationId }}" />
</filter>
</link-entity>
</link-entity>
</entity>
</fetch>
{% endfetchxml %}
<ul>
{% for p in products.results.entities %}
<li>{{ p.pwrprtl_product_name }}</li>
{% endfor %}
</ul>
Im Demo-Portal liefert genau das die verknüpften Produkte einer Installation:
Der Stolperstein, der mich beim Nachstellen Zeit gekostet hat (und der Sie sonst genauso trifft): Liquid FetchXML läuft im Kontext des angemeldeten Nutzers und respektiert dessen Web-Rolle und Table Permissions. Als ich die Seite anonym aufrief, kam eine leere Liste zurück, keine Fehlermeldung, nichts. Erst als Portal-Contact angemeldet (mit einer Web-Rolle, der Lese-Table-Permissions auf die beteiligten Tabellen zugewiesen sind) erschienen die Daten. Setzen Sie also Read-Table-Permissions auf die beteiligten Tabellen für die passende Web-Rolle; ich habe zusätzlich eine Permission auf die Intersect-Tabelle gesetzt und nicht isoliert, ob sie zwingend nötig ist, im Zweifel mitnehmen.
Charakteristik: Die Daten stehen beim Seiten-Rendering bereit (vorgeladen). Ideal, wenn die Liste ohnehin gleich sichtbar ist.
Entscheidungshilfe
| Aufgabe | Empfohlener Weg |
|---|---|
| N:M lesen, Liste beim Page-Load sichtbar | Liquid FetchXML (link-entity/intersect) |
| N:M schreiben (verknüpfen/lösen) | Portal Web API $ref (POST/DELETE) |
| 1:N / N:1 lesen | Portal Web API $expand (eine Ebene) oder Liquid |
Die eigentliche Lehre steckt weniger im "welcher Weg geht" als im Kontext: Beide Wege respektieren Web-Rolle und Table Permissions des Nutzers. Ein leeres Ergebnis ist deshalb oft kein Bug im Code, sondern eine fehlende Berechtigung oder ein nicht angemeldeter Nutzer. Bevor Sie die FetchXML zerlegen, prüfen Sie erst, als wer die Seite läuft.
Ausblick: Es gibt einen dritten Weg, N:M-Daten zu holen, Server Logic, serverseitiges JavaScript, das der Client on-demand aufruft (seit 2026 allgemein verfügbar). Wann sich das gegenüber dem vorgeladenen FetchXML lohnt und worauf man dabei achten muss, kommt in einem eigenen Folge-Post.
Weiterführende Artikel
Liquid FetchXML vs. Web API
Der grundsätzliche Vergleich der beiden Datenzugriffswege, Entscheidungsbaum, Performance, Security.
Artikel lesen → SicherheitPower Pages Sicherheitsarchitektur
Table Permissions, Web Roles und Scopes, das Modell hinter der Berechtigungsfalle in diesem Beitrag.
Artikel lesen → SicherheitWildcard im Web API
Ein weiteres Web-API-Detail mit Sicherheitsfolgen: die Ablösung des Wildcard-Werts in den Feldlisten.
Artikel lesen →Quellen
- Microsoft Learn: Overview of the portals Web API (Operations-Subset, "Known issues": N:M-GET-Fehler + FetchXML-Workaround, "follows the table permissions")
- Microsoft Learn: Read operations (
$expandfür N:1/1:N, "only one level of depth") - Microsoft Learn: Write, update, delete (Associate/Disassociate
$ref) - Microsoft Learn: fetchxml Liquid tag und link-entity / intersect
- Hands-on im Power-Portals-Demo-Portal: zwei Tabellen mit N:M-Beziehung, Associate über
$refund N:M-Lesen über Liquid FetchXML live nachgestellt