Zum Inhalt

Power Pages React und Dataverse typisiert verbinden

Typisierte Power Pages Schnittstellen verbinden React und Dataverse über Controller, Service, Repository, Transport und geprüfte Laufzeitparser.

Typisierte Power Pages Schnittstellen verbinden React und Dataverse über Controller, Service, Repository, Transport und geprüfte Laufzeitparser.

Inhalt

Welche Transport-Schnittstellen braucht React in Power Pages?

Der technische Vertrag zwischen meiner React Suche und Power Pages beginnt mit zwei Methoden:

export interface ISearchTransport {
  submitSearchFilters(request: SearchServerLogicRequest): Promise<SearchApiData<SearchResponse>>;
  fetchSuggestions(request: SuggestServerLogicRequest): Promise<readonly SearchSuggestion[]>;
}

Dieses Interface zeigt die zwei Operationen, die das bestehende React Suchwidget selbst konsumiert.

Der gemeinsame HTTP-Endpunkt besitzt zusätzlich getPortalCatalogDetails. Dieser dritte Vertrag gehört zu Detail- und Favoritenansichten. Er wird typisiert, ohne eine ungenutzte Methode in den Search Service einzubauen.

Das Interface kennt keine jQuery Callbacks. Es kennt keinen Server Logic Endpunkt. Es kennt keine Dataverse Connector Details.

Die Anwendungsschichten sehen nur einen Request und ein Promise mit einem definierten Ergebnis.

Die öffentlichen Beispiele verwenden dafür ein kleines generalisiertes Domänenmodell:

type SupportedLcid = 1031 | 1033;
type SearchSuggestionKind = 'city' | 'postalCode' | 'region' | 'center';

interface FilterPayload {
  readonly lcid: SupportedLcid;
  readonly searchText?: string;
}

interface SearchResult {
  readonly id: string;
  readonly name: string;
}

interface SearchResponse {
  readonly lcid: SupportedLcid;
  readonly objects: readonly SearchResult[];
}

interface SearchApiData<TContent> {
  readonly IsSucceeded: boolean;
  readonly Content: TContent;
}

interface SearchSuggestion {
  readonly kind: SearchSuggestionKind;
  readonly label: string;
  readonly value: string;
  readonly isDirectMatch: boolean;
}

Diese kleine Grenze ist der Ausgangspunkt meiner Power Pages React Dataverse Server Logic Architektur. Die Übersicht der Enterprise-Suche ordnet sie in den vollständigen Datenfluss ein.

Warum TypeScript Netzwerkdaten in Power Pages nicht validiert

TypeScript kann sicherstellen, dass mein Service nur einen SearchServerLogicRequest an den Transport übergibt. Es kann verhindern, dass ein Consumer versehentlich ein veränderbares Array erwartet.

TypeScript kann nicht prüfen, was der Server tatsächlich zurücksendet.

Interfaces und Typen werden beim Build entfernt. Ein Ausdruck wie dieser erzeugt deshalb keine Laufzeitprüfung:

const response = JSON.parse(rawValue) as SearchResponse;

Der Cast ändert nur die Sicht des Compilers. Falsches JSON bleibt falsches JSON.

Die TypeScript Dokumentation zur Typverengung zeigt, wie Laufzeitprüfungen aus unknown einen belastbaren Typ ableiten.

Ich lasse Netzwerkdaten deshalb so lange unknown. Erst ein Parser prüft die erwartete Form und gibt anschließend einen Domänentyp zurück.

Auch readonly besitzt eine klare Grenze. Es verhindert unerwünschte Zuweisungen im TypeScript Code. Es friert ein Objekt zur Laufzeit nicht automatisch ein.

Vergleich von Compile-Time Verträgen und Laufzeitverträgen

Aspekt

TypeScript Typ

Laufzeitparser

Zeitpunkt

Build und EntwicklungNach Empfang echter Netzwerkdaten

Eingabe

Bekannte Werte im eigenen Code

unknown aus JSON und APIs

Prüfung

Aufrufe, Felder und RückgabetypenObjektform, Discriminator und benötigte Werte

Grenze

Nach Build nicht mehr vorhandenPrüft nur ausdrücklich implementierte Regeln

Fehlerwirkung

Compiler meldet widersprüchlichen CodeParser lehnt ungültige Daten sichtbar ab

Warum brauchen drei Power Pages Operationen getrennte Request-Verträge?

Die Suche und Auto-Suggest nutzen denselben Server Logic Endpunkt. Sie besitzen trotzdem unterschiedliche Verträge.

export type SearchServerLogicRequest = {
  readonly operation: 'search';
} & FilterPayload;

export interface SuggestServerLogicRequest {
  readonly operation: 'suggest';
  readonly lcid: SupportedLcid;
  readonly searchText: string;
}

export interface PortalCatalogDetailsServerLogicRequest {
  readonly operation: 'getPortalCatalogDetails';
  readonly entityType: 'center' | 'area';
  readonly ids: readonly string[];
  readonly languageCode: 'de' | 'de-DE' | 'en' | 'en-US' | 'en-GB';
}

export type ServerLogicRequest =
  | SearchServerLogicRequest
  | SuggestServerLogicRequest
  | PortalCatalogDetailsServerLogicRequest;

Das Feld operation ist der Discriminator.

Eine vollständige Suche kann alle unterstützten Filter tragen. Ein Suggest Request enthält nur Sprache und Suchtext. Ein Detail-Batch enthält einen Entitätstyp, eine Sprache und bekannte IDs.

Ich nutze keinen großen Request-Typ mit vielen optionalen Feldern. Ein solcher Typ würde Kombinationen zulassen, die fachlich keinen Sinn ergeben.

Der Service erzeugt die Operation ausdrücklich:

class SearchService {
  constructor(private readonly repository: SearchRepository) {}

  submitSearchFilters(payload: FilterPayload) {
    return this.repository.submitSearchFilters({
      operation: 'search',
      ...payload,
    });
  }

  fetchSuggestions(searchText: string, lcid: SupportedLcid) {
    return this.repository.fetchSuggestions({
      operation: 'suggest',
      lcid,
      searchText,
    });
  }
}

Der Search Service ergänzt weiterhin nur search und suggest, weil das vorhandene Widget keine Favoritenseite rendert. Der vollständige Endpoint-Typ enthält trotzdem den dritten Vertrag für einen separaten Browser-Consumer.

Der Deep Dive Power Pages Favoriten mit Batch-GUIDs und Server Logic zeigt diesen Vertrag.

Dadurch kann der Aufrufer die Operation nicht vergessen. Der Rückgabetyp bleibt an die gewählte Methode gekoppelt.

Vergleich von Search, Suggest und Detail-Batch Request-Verträgen

Aspekt

Vollständige Suche

Auto-Suggest

Detail-Batch

Discriminator

operation: ‘search’operation: ‘suggest’operation: ‘getPortalCatalogDetails’

Pflichtwerte

lcid

lcid und searchText

entityType, ids und languageCode

Optionale Werte

Suchtext und unterstützte FilterKeine zusätzlichen SuchfilterKeine Suchfilter

Rückgabe

SearchApiData<SearchResponse>readonly SearchSuggestion[]PortalCatalogDetailsData

Fachlicher Zweck

Ergebnisliste aus allen akzeptierten FilternKleine Vorschlagsliste für SuchtexteingabeBekannte Center oder Flächen gesammelt laden

Der Deep Dive Power Pages Auto-Suggest mit React: Debounce und Requests verfolgt diesen kleineren Vertrag vom Eingabefeld bis zur gruppierten und geprüften Vorschlagsliste.

Welche Aufgaben haben Controller, Service und Repository in React?

Ich trenne die Schichten nicht wegen ihrer Namen. Jede Schicht muss eine konkrete Entscheidung besitzen.

Der Controller prüft UI Eingaben. Er trimmt den Suchtext, liefert Validierungsfehler als discriminated union und bereitet einen serialisierten Suchvertrag vor.

type SearchPreparationResult =
  | {
      readonly isValid: false;
      readonly validationErrors: readonly string[];
    }
  | {
      readonly isValid: true;
      readonly prepared: {
        readonly payload: FilterPayload;
        readonly queryString: string;
      };
    };

Der hier auf payload und queryString reduzierte Vertrag liegt im vollständigen Widget unter serializedSearch. Wie dieser Vertrag mit Landing Navigation, pushState, replaceState und popstate verwendet wird, zeigt der nächste Deep Dive über Power Pages React URL Parameter und Browserhistorie.

Der Service ergänzt die Backend Operation und besitzt die fachnahen Methoden für Suche und Vorschläge.

Das Repository reicht den fertigen Request an ISearchTransport weiter. In diesem Projekt ist es bewusst dünn. Es bildet die Datenquellengrenze für den echten Power Pages Transport und einen lokalen Mock.

Die Verdrahtung bleibt dadurch klein:

function buildController(transport: ISearchTransport): SearchController {
  const repository = new SearchRepository(transport);
  const service = new SearchService(repository);
  return new SearchController(service);
}

Ein dünnes Repository ist nicht automatisch sinnvoll. Wenn eine Anwendung nur eine Datenquelle besitzt, keinen Mock benötigt und keine eigene Datenzugriffsgrenze braucht, kann diese Schicht unnötig sein.

Hier unterstützt sie zwei Laufzeitumgebungen mit demselben Controller. Das zentrale Host Muster für das React Widget hält im Gegenzug die Seitentemplates frei von diesen Anwendungsschichten.

Wie kapselt ein Power Pages Transport die Laufzeit?

Nur PowerPagesSearchTransport kennt window.webapi, safeAjax und den Server Logic Pfad.

export class PowerPagesSearchTransport implements ISearchTransport {
  async submitSearchFilters(request: SearchServerLogicRequest): Promise<SearchApiData<SearchResponse>> {
    const rawData = await postServerLogicRequest(request);
    return parseSearchResponse(rawData);
  }

  async fetchSuggestions(request: SuggestServerLogicRequest): Promise<readonly SearchSuggestion[]> {
    const rawData = await postServerLogicRequest(request);
    return parseSuggestionResponse(rawData);
  }
}

Repository, Service und Controller müssen dadurch keine Callback Struktur kennen. Sie arbeiten nur mit Promises.

Der Adapter übernimmt zwei Aufgaben:

  1. Er übersetzt den Promise Vertrag in den Power Pages AJAX Aufruf.
  2. Er übergibt die empfangenen Strings an den passenden Laufzeitparser.

CSRF Schutz, serverseitige Autorisierung und OData Aufbau bleiben eigene Verantwortlichkeiten. Der vorherige Beitrag zur Power Pages Server Logic Sicherheitsgrenze dokumentiert diese Prüfungen.

Welche JSON-Verträge besitzt eine Power Pages Antwort?

Die Antwort ist nicht nach einem einzigen JSON.parse abgeschlossen.

Die Microsoft Dokumentation zum Aufruf von Server Logic zeigt den äußeren Power Pages Envelope mit Request ID, Erfolgsstatus, Daten und Server Logic Namen.

Der im Projekt verwendete Wrapper stellt diese Informationen im Callback bereit. Das Feld data enthält den serialisierten Rückgabewert meiner Server Logic.

Dieser innere String besitzt einen eigenen Operationsvertrag:

{
  "operation": "search",
  "data": {
    "IsSucceeded": true,
    "Content": "{\"lcid\":1031,\"objects\":[]}"
  },
  "serverLogicName": "catalog-search"
}

Das Feld Content ist erneut JSON als String.

Das Beispiel zeigt die Search Operation. Dort enthält data den Custom API Umschlag und Content erneut serialisiertes JSON.

Suggest und Detail-Batch besitzen eine andere dritte Prüfebene. Suggest liefert direkt suggestions. Der Detail-Batch liefert direkt entityType, results, notFoundIds und invalidIds.

Der gemeinsame Operationsparser bestätigt nur Umschlag, Discriminator und Datenobjekt. Danach übernimmt ein operationsspezifischer Parser.

Ich behandle deshalb drei Grenzen getrennt:

Drei getrennte Prüfebenen der Server Logic und Custom API Antwort

Prüfebene

Bestätigt

Typische Prüfung

Power Pages Envelope

HTTP und Server Logic Aufruf waren erfolgreich

Erfolgsstatus prüfen und data an den Operationsparser übergeben

Operationsvertrag

Antwort gehört zu search, suggest oder getPortalCatalogDetails

Discriminator und vorhandenes Datenobjekt

Operationsdaten

Search Umschlag, Suggestion-Liste oder Detail-Batch

Operationsspezifische Felder und Ergebnislisten

Keine dieser Grenzen darf durch einen TypeScript Cast übersprungen werden.

Wie verhindert ein Operationsparser vertauschte Power Pages Antworten?

Der gemeinsame Parser erhält den erwarteten Discriminator als Argument:

type ServerLogicOperation = 'search' | 'suggest' | 'getPortalCatalogDetails';

function parseJson(value: string, label: string): unknown {
  try {
    return JSON.parse(value) as unknown;
  } catch {
    throw new Error(`${label} is not valid JSON.`);
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function parseOperationResult(
  value: string,
  expectedOperation: ServerLogicOperation
): {
  readonly data: Record<string, unknown>;
  readonly serverLogicName: string;
} {
  const parsed: unknown = parseJson(value, 'Server Logic data');

  if (!isRecord(parsed)) {
    throw new Error('Server Logic data must be an object.');
  }

  if (parsed['operation'] !== expectedOperation) {
    throw new Error(`Server Logic operation must be "${expectedOperation}".`);
  }

  if (!isRecord(parsed['data'])) {
    throw new Error('Server Logic data does not contain a data object.');
  }

  return {
    data: parsed['data'],
    serverLogicName: typeof parsed['serverLogicName'] === 'string' ? parsed['serverLogicName'] : 'catalog-search',
  };
}

Ein Suggest Ergebnis kann damit nicht versehentlich als vollständige Suchantwort verarbeitet werden. Dasselbe gilt für einen Detail-Batch. Der Discriminator muss zum aufrufenden Parser passen.

Ungültiges JSON, ein fehlendes Datenobjekt und eine falsche Operation erzeugen jeweils einen ausdrücklichen Fehler. Der Parser erfindet keinen Ersatzwert.

Wie tief müssen Power Pages Ergebnisse geprüft werden?

Der Suchparser prüft zuerst den Custom API Envelope.

IsSucceeded muss ein Boolean sein. Content muss ein String sein. Ein optionaler OData Kontext muss ebenfalls eine Zeichenkette sein.

Danach wird Content erneut geparst. Der Suchparser prüft die benötigten Top-Level Felder und ob die erwarteten Ergebnislisten Arrays sind.

Die Elemente dieser Listen werden dadurch nicht vollständig validiert.

Diese Unterscheidung ist wichtig. Ein TypeScript Array Cast nach Array.isArray validiert nicht automatisch die Elemente. Wenn ein Consumer ein einzelnes Ergebnisfeld als geschäftskritisch verwendet, braucht dieses Feld eine zusätzliche Laufzeitprüfung oder ein geeignetes Schema.

Die Detail-DTOs besitzen ebenfalls TypeScript Verträge. Ein Browser-Consumer behandelt empfangene Center- oder Area-Elemente zunächst als unknown. Die Laufzeitprüfung muss die Felder abdecken, auf die der Consumer zugreift. Das gilt unabhängig davon, ob ein Einzelabruf oder ein Favoriten-Batch die Daten liefert.

Der Suggestion Vertrag ist kleiner. Dort prüft der Parser jeden Eintrag:

function isSuggestionKind(value: unknown): value is SearchSuggestionKind {
  return value === 'city' || value === 'postalCode' || value === 'region' || value === 'center';
}

function parseSuggestion(value: unknown, index: number): SearchSuggestion {
  if (!isRecord(value)) {
    throw new Error(`Suggestion at index ${String(index)} must be an object.`);
  }

  if (!isSuggestionKind(value['kind'])) {
    throw new Error(`Suggestion at index ${String(index)} has an invalid kind.`);
  }

  if (typeof value['label'] !== 'string' || value['label'].trim() === '') {
    throw new Error(`Suggestion at index ${String(index)} needs a label.`);
  }

  if (typeof value['value'] !== 'string' || value['value'].trim() === '') {
    throw new Error(`Suggestion at index ${String(index)} needs a value.`);
  }

  if (typeof value['isDirectMatch'] !== 'boolean') {
    throw new Error(`Suggestion at index ${String(index)} needs isDirectMatch.`);
  }

  return {
    kind: value['kind'],
    label: value['label'],
    value: value['value'],
    isDirectMatch: value['isDirectMatch'],
  };
}

Die Prüftiefe folgt dem Vertrag. Sie wird nicht durch einen allgemeinen Anspruch auf vollständige Typisierung ersetzt.

Wie bleiben Parserfehler in React und Power Pages sichtbar?

Ein Parserfehler wird vom Transport nicht in ein leeres Ergebnis umgewandelt.

Das abgelehnte Promise läuft durch Repository und Service zurück zum Widget. Dort erhält die Oberfläche einen technischen Fehlerzustand.

Eine erfolgreiche leere Suche bleibt davon getrennt:

type SearchExecutionState =
  | { readonly status: 'success'; readonly response: SearchResponse }
  | { readonly status: 'error'; readonly message: string };

Dieser Beispieltyp fasst die sichtbare Entscheidung zusammen. Im realen Widget wird der Zustand über Promise Erfolg und Fehlerbehandlung gesteuert.

Die Grenze verhindert einen gefährlichen Eindruck. Keine Treffer bedeuten nicht automatisch, dass Server Logic, Custom API und Parser korrekt gearbeitet haben.

Wie werden validierte Suchergebnisse zum React Consumer-Vertrag?

Nach erfolgreicher Prüfung besitzt das Widget einen typisierten Ergebnisvertrag:

export type SearchResultsEventDetail = SearchApiData<SearchResponse>;

export function dispatchSearchResults(eventName: string, response: SearchResultsEventDetail): void {
  window.dispatchEvent(
    new CustomEvent(eventName, {
      detail: response,
    })
  );
}

Das Widget veröffentlicht nur Antworten, die den vorgelagerten Parser passiert haben.

Ein Vanilla JavaScript Consumer erhält zur Laufzeit dasselbe Objekt wie ein React Consumer. TypeScript unterstützt nur Consumer, die den Typvertrag importieren. Andere Consumer brauchen die dokumentierte Event Form als eigenen Vertrag.

Die Event Übergabe ist deshalb keine neue Validierungsgrenze. Sie verteilt ein bereits akzeptiertes Ergebnis.

Wie der Producer diesen Vertrag zeitlich veröffentlicht und wie unabhängige Consumer ihn abonnieren, zeigt der folgende Beitrag über Power Pages React CustomEvents.

Wann rechtfertigen typisierte React und Dataverse Schnittstellen den Aufwand?

Für einen einzelnen einfachen Web API Request wäre diese Architektur zu groß.

In meiner Suche treffen mehrere Anforderungen zusammen:

  1. Der gemeinsame POST-Endpunkt besitzt drei getrennte Request- und Result-Verträge.
  2. Vollständige Suche und Auto-Suggest besitzen unterschiedliche Verträge.
  3. Landingpage und Ergebnisseite nutzen denselben Controller in verschiedenen Abläufen.
  4. Lokale Entwicklung und Power Pages verwenden unterschiedliche Transportimplementierungen.
  5. Die Custom API liefert verschachtelte serialisierte Antworten.
  6. Mehrere Consumer sollen geprüfte Ergebnisse erhalten.

Unter diesen Bedingungen liefern die Schichten überprüfbare Grenzen. Sie verteilen nicht nur Code auf mehr Dateien.

Der allgemeine Beitrag zur React Frontend Architektur in Power Pages hilft bei der Entscheidung, wann ein isoliertes Widget sinnvoll ist und wann native Power Pages Funktionen ausreichen.

Typisierte React und Dataverse Schnittstellen im Enterprise-Projekt

Ich habe diese Struktur für eine komplexe Enterprise-Suche umgesetzt.

Der sichtbare React Teil kann dadurch mit kleinen Methoden arbeiten. Die Power Pages Laufzeit bleibt im Adapter. Server Logic und Custom API Antworten werden schrittweise geprüft. Entkoppelte Consumer erhalten erst den akzeptierten Ergebnisvertrag.

Die ECE Fallstudie zur Power Pages Enterprise-Suche zeigt den Projektkontext. Die Codeausschnitte in diesem Beitrag sind generalisiert.

Ich verkaufe damit keine allgemeine Repository Vorlage. Ich dokumentiere, wie ich Integrationsgrenzen für einen geschäftskritischen Power Pages Workflow nachvollziehbar mache.

Du planst eine React Anwendung in Power Pages, die Server Logic oder eine Dataverse Custom API nutzt? Im Power Pages Pro-Code Bereich zeige ich, wie ich Verträge, Laufzeitgrenzen und Übergaberisiken vor der Entwicklung kläre.

Ähnliche Beiträge

Was soll Ihr Portal können?

Beschreiben Sie Ihren Workflow. Wir klären gemeinsam, welcher nächste Schritt sinnvoll ist.

Workflow besprechen