Der Use Case ist banal: Zu einer Installation die verknuepften Produkte anzeigen, verbunden ueber eine Many-to-Many-Beziehung. Man greift zur Portal Web API, ruft den Navigationspfad ab, und bekommt einen Fehler statt Daten.
Danach beginnt die uebliche 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 loest. 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/Loeschen und Associate/Disassociate. $expand ist dokumentiert fuer 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 woertlich:
"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 ueber die Portal Web API dagegen sauber funktioniert, ist das Verknuepfen und Loesen von N:M-Beziehungen ueber $ref (im Demo bestaetigt):
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) ueber die Web API ist unproblematisch. Das komfortable N:M-Lesen ueber $expand ist es nicht – dafuer gibt es den FetchXML-Weg.
Weg 2: Liquid FetchXML – Microsofts eigener Workaround
FetchXML loest N:M serverseitig ueber 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 verknuepften Produkte einer Installation:
Der Stolperstein, der mich beim Nachstellen Zeit gekostet hat (und der Sie sonst genauso trifft): Liquid FetchXML laeuft im Kontext des angemeldeten Nutzers und respektiert dessen Web-Rolle und Table Permissions. Als ich die Seite anonym aufrief, kam eine leere Liste zurueck – 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 fuer die passende Web-Rolle; ich habe zusaetzlich eine Permission auf die Intersect-Tabelle gesetzt und nicht isoliert, ob sie zwingend noetig 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 (verknuepfen/loesen) | 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, pruefen Sie erst, als wer die Seite laeuft.
Ausblick: Es gibt einen dritten Weg, N:M-Daten zu holen – Server Logic, serverseitiges JavaScript, das der Client on-demand aufruft (seit 2026 allgemein verfuegbar). Wann sich das gegenueber dem vorgeladenen FetchXML lohnt und worauf man dabei achten muss, kommt in einem eigenen Folge-Post.
Weiterfuehrende Artikel
Liquid FetchXML vs. Web API
Der grundsaetzliche 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 Abloesung 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 (
$expandfuer 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 ueber
$refund N:M-Lesen ueber Liquid FetchXML live nachgestellt