LookupImportPlus: Schluss mit Raten bei Dataverse-Lookup-Importen
LookupImportPlus: Schluss mit Raten bei Dataverse-Lookup-Importen
Du kennst das Problem, wenn du schon mal Kontakte oder Accounts per Excel in Dataverse importiert hast: Du exportierst eine Liste, jemand trägt in einer Textspalte einen Firmennamen ein, du importierst zurück — und Dataverse verknüpft einen Teil deiner Kontakte mit dem falschen Account, weil zwei Accounts zufällig denselben Namen tragen. Kein Fehler, keine Warnung. Du merkst es erst Wochen später, wenn jemand seinen eigenen Kontakt nicht findet.
LookupImportPlus ist ein Power-Apps-Tool, das genau das verhindert. Hier zeigen wir dir, wie es funktioniert und wie du es Schritt für Schritt einsetzt — inklusive eines echten Importlaufs, bei dem du jedes Matchingverfahren einmal in Aktion siehst.
Das Problem: Raten ist kein Auflösen
Ein Lookup in Dataverse ist eine Spalte, die auf einen Datensatz einer anderen Tabelle verweist — zum Beispiel das Feld “Mutterkonto” eines Kontakts, das auf einen Account zeigt. Technisch ist ein Lookup nicht der Name, den du siehst, sondern ein Verweis über die GUID (eine eindeutige ID) plus die Zieltabelle.
Der Standard-Excel-Import von Dataverse weiß das nicht. Importierst du eine Spalte “Mutterkonto” voller Firmennamen, sucht der Import nach einem Treffer — findet er mehrere, nimmt er stillschweigend den ersten. Zwei Accounts namens “Contoso GmbH”? Dann hängt ein Teil deiner importierten Kontakte jetzt am falschen, und das Protokoll meldet trotzdem “erfolgreich”.
Die Regel von LookupImportPlus: entweder eindeutig auflösen, oder anhalten und dich fragen. Nie raten.
Warum das technisch nicht trivial ist
Ein paar Punkte, die du kennen solltest, bevor du loslegst:
- Namen sind nicht eindeutig. Die GUID wäre der zuverlässige Identifikator — steht aber so gut wie nie in deiner Excel-Spalte, weil Fachanwender nicht mit GUIDs arbeiten.
- Polymorphe Lookups. Manche Lookups —
customerid,ownerid,regardingobjectid— können auf mehrere Tabellen zeigen. Eincustomeridkann Account oder Kontakt sein. Jede Zieltabelle hat eigene Feldnamen (Account sucht übername, Kontakt überfullname), du brauchst also pro möglichem Ziel eine eigene Abfrage. - Den Link schreiben. Kennst du den Zieldatensatz, setzt du den Lookup über die Dataverse Web API mit dem annotierten Feld
@odata.bind:
POST /contacts
{ "firstname": "Max", "lastname": "Mustermann",
"parentcustomerid_account@odata.bind": "/accounts(8f2c…-guid)" }
Das ist eine Schreibanweisung — deshalb ist es so wichtig, vorher die richtige GUID zu finden.
- OData-Filter. Kommen deine Quelldaten aus einer Dataverse-View, definiert in FetchXML, musst du das mit übersetzen:
<condition attribute="statecode" operator="eq" value="0"/> → statecode eq 0
<condition attribute="name" operator="like" value="%Contoso%"/> → contains(name,'Contoso')
Nichts davon ist exotisch — aber zusammen ist es genau die Art Problem, die bei Skalierung leise Daten kaputt macht, wenn du es nicht sauber löst.
So arbeitest du damit: Konfiguration, Export, Import
Du startest nicht bei einer Excel-Datei, sondern bei einer Job-Konfiguration: einer gespeicherten, versionierten Beschreibung, wie eine Dataverse-Tabelle exportiert und wieder importiert wird — inklusive der genauen Auflösungslogik für jede Lookup-Spalte. Jeder Importlauf speichert einen unveränderlichen Schnappschuss der verwendeten Konfiguration, damit spätere Änderungen einen alten Lauf nie nachträglich umdeuten.
Dein Ablauf sieht so aus:
Konfigurieren → Exportieren (Template oder Daten) → in Excel bearbeiten → Importieren (Hochladen → Dry Run → Konflikte lösen → Commit).
Praxis: Screen für Screen
1. Job-Konfigurationen — dein Startpunkt
Auf der Startseite siehst du jede gespeicherte Konfiguration als Karte: Zieltabelle, Operation (Anlegen / Aktualisieren / Anlegen-oder-Aktualisieren), Anzahl Spalten und Lookups, Version, Entwurfsstatus. Pro Karte hast du Export ▾ (leeres Template oder echter Datenexport), Bearbeiten, Import starten und Löschen.
Die Konfigurationsliste (Dark Theme) — eine Konfiguration, “Contacts – Parent Account”, Version 4, Entität contact, createOrUpdate.
Die helle Variante desselben Screens. Der Theme-Umschalter (Mond-/Sonne-Icon) sitzt oben rechts neben der Sprache.
Klicke Neue Konfiguration, um den geführten Assistenten zu öffnen. Spätere Tabs schalten sich frei, sobald du eine Zielentität gewählt hast:
- Entität & Quelle — wähle die Ziel-Dataverse-Tabelle. Metadaten (Attribute, Primärschlüssel, Anzeigename) laden automatisch. Du kannst statt der Rohtabelle auch aus einer gespeicherten View exportieren.
- Allgemein — Name, Beschreibung, Operationstyp, Standard-Schreibmodus: strict (nichts schreiben, bis alle Zeilen aufgelöst sind) oder partial (saubere Zeilen sofort committen, Konflikte warten).
2. Spalten — leg fest, was mitgeht
Im Tab Spalten siehst du jedes Attribut der gewählten Tabelle, mit Filtern für Suche, nur ausgewählte, nur Lookups, nur Pflichtfelder, nur beschreibbare — bei Tabellen mit 200+ Systemspalten spart dir das viel Scrollen.
Der Spalten-Tab — Spalten ankreuzen, Filter wie “Nur Lookups” verfügbar, darunter Daten-Vorschau / Leeres Template / Daten exportieren.
Kreuze die Spalten an, die du brauchst, und nutze direkt von hier aus Daten-Vorschau, Leeres Template oder Daten exportieren.
3. Daten-Vorschau — prüf, bevor du exportierst
Aus dem Spalten-Tab öffnest du dieses Modal: bis zu 10, 25 oder 50 echte Datensätze, umschaltbar zwischen CRM-Spalten (rohe Dataverse-Werte) und Schema-Spalten (das Excel-Layout, das der Export erzeugt), mit farbcodierten Gruppen und Legende, welche Spalten zu welchem Lookup gehören.
Die Schema-Ansicht zeigt, was der Export wirklich erzeugt: neben “Parent Account” auch die technischen Spalten “Parent Account Id” (GUID), “Parent Account Type” und “Parent Account Number” — die Kanäle für GUID- und Business-Key-Matching.
Merk dir diese drei technischen Spalten — sie sind gleich im Szenario-Teil die Abkürzung an der Konfliktliste vorbei.
4. Lookups — hier passiert die eigentliche Arbeit
Jede ausgewählte Lookup-Spalte bekommt eine eigene Konfigurationskarte. Hier setzt du das “nie raten”-Prinzip praktisch um. Die Karte bündelt alles, was ein Matching braucht:
Eine Lookup-Konfiguration im Detail — Zielfeld parentcustomerid, Business-Key-Spalte “Parent Account Number”, Konfliktstrategie escalate, Zieltabellen account/contact (polymorpher Scope) und eine Bedingung “Modified On ≥ relatives Datum −7 Tage”.
- Sichtbare Excel-Spalte — die menschenlesbare Spalte, die deine Leute ausfüllen (“Parent Account”).
- Business-Key-Spalte (optional) — eine Spalte mit einem eindeutigen Alternativwert (z. B. eine Kontonummer), die vor der Namenssuche greift.
- Konfliktstrategie — was bei mehreren Treffern passiert:
escalate(in die Konfliktliste), überspringen oder als Fehler markieren. - Zieltabelle(n) — bei polymorphen Lookups kreuzt du nur die Tabellen an, die deine Excel tatsächlich referenziert. Pro Ziel setzt du Suchfeld, Business-Key-Attribut und Bedingungen — weil derselbe Lookup für ein Ziel
account.nameund für ein anderescontact.fullnamebraucht. - Bedingungen — grenzen ein, welche Ziel-Datensätze überhaupt passen: links ein Feld des Ziels, rechts ein fester Wert, eine andere Excel-Spalte derselben Zeile oder ein relatives Datum.
5. Importlauf — hochladen, Dry Run, entscheiden
Lädst du eine ausgefüllte Tabelle hoch (oder die eingebaute Demo-Datei), läuft zuerst eine Konfigurationsprüfung (Schema-Drift-Schutz), dann ein Dry Run: jede Zeile bekommt einen Status und du siehst einen echten Fortschrittsbalken, weil das Tool die Gesamtzeilenzahl von Anfang an kennt.
Dry-Run-Ergebnisse der Demo-Datei — vier Zeilen, vier verschiedene Ausgänge. Genau die sehen wir uns unten einzeln an.
Wähle Strict (nichts schreiben, bis alles aufgelöst ist) oder Partial (saubere Zeilen jetzt schreiben) und committe.
6. Konfliktliste — Entscheidungen bündeln
Jeder ungelöste Lookup landet hier, gruppiert nach Quellwert — “1 Zeile sagt Contoso GmbH” ist eine Entscheidung, nicht viele.
Die Konfliktliste — “Nichts wird automatisch geraten.” Jede Entscheidung wird protokolliert: Regel, gezeigte Kandidaten, gewählte GUID, Benutzer, Zeitpunkt.
7. Importhistorie
Jeder Lauf steht hier mit eingefrorenem Konfigurations-Snapshot und Zahlen (Zeilen, geschrieben, Konflikte, Status) — nachvollziehbar bis zur einzelnen Zeile und Lookup-Entscheidung.
Importhistorie (Light Theme) — ein Partial-Lauf: 4 Zeilen gelesen, 1 sauber geschrieben, 2 Konflikte offen.
Das Auflösungsmodell
Die Reihenfolge ist fest, ohne Ausnahme. Genau diese drei Stufen erklärt die App dir auch direkt im Lookups-Tab:
Die drei Matching-Stufen, wie die App sie selbst dokumentiert — erster Treffer gewinnt.
1. GUID-Spalte → per ID abrufen, Zieltyp prüfen → binden
2. Business Key → per eindeutigem Alternate-Attribut abfragen
3. Suchfeld + Bedingungen → Kandidaten abfragen
genau ein Treffer → aufgelöst | null Treffer → NotFound | mehrere → Mehrdeutig (→ Konfliktliste)
Eine Lookup-Konfiguration, vereinfacht:
{
"lookupAttribute": "parentcustomerid",
"visibleColumn": "Parent Account",
"guidColumn": "Parent Account Id",
"businessKeyColumn": "Parent Account Number",
"targetEntities": ["account", "contact"],
"searchAttribute": "name",
"targetOverrides": {
"account": { "searchAttribute": "name" },
"contact": { "searchAttribute": "fullname" }
},
"conflictStrategy": "escalate"
}
Import-Szenarien: die Matchingverfahren in Aktion
Genug Modell — schauen wir es an einem echten Lauf an. Die eingebaute Demo-Datei hat vier Zeilen, und jede fällt bewusst in einen anderen Matching-Ausgang. Genau das siehst du oben im Dry-Run-Screen; hier ist die Tabelle Zeile für Zeile:
| Last Name | Parent Account | Auflösung | Status | Was greift |
|---|---|---|---|---|
| Schmidt | Fabrikam Inc. | → 22222222… (account) | ✅ Aufgelöst | Suchfeld, genau 1 Treffer |
| Mustermann | Contoso GmbH | 2 Kandidaten | ⚠️ Mehrdeutig | Suchfeld, >1 Treffer → eskaliert |
| — | Adventure Works | kein Treffer | ❌ Pflichtfeld fehlt | Validierung vor dem Matching |
| Berg | Unknown Corp | kein Treffer | ❌ Nicht gefunden | Suchfeld, 0 Treffer |
Szenario 1 — Eindeutiger Name: der Normalfall
Zeile Schmidt → Fabrikam Inc.: Es gibt genau einen Account namens “Fabrikam Inc.”. Die GUID- und Business-Key-Spalten sind leer, also greift Stufe 3 (Suchfeld). Die Abfrage name eq 'Fabrikam Inc.' liefert genau einen Treffer → die Zeile ist aufgelöst, die App zeigt die aufgelöste GUID (22222222…) an und würde beim Commit parentcustomerid_account@odata.bind auf diesen Account setzen. Kein Nutzereingriff. So sollen so viele Zeilen wie möglich durchlaufen.
Szenario 2 — Mehrdeutiger Name: zwei “Contoso GmbH”
Zeile Mustermann → Contoso GmbH: Auch hier greift Stufe 3 — aber die Namenssuche findet zwei Accounts “Contoso GmbH”. Die Konfliktstrategie steht auf escalate, also rät die App nicht, sondern schiebt die Zeile in die Konfliktliste. Öffne sie und dann die Gruppe:
Die Resolve-Seite legt offen, worauf gematcht wurde: name eq 'Contoso GmbH' and modifiedon ge 2026-06-28… — und zeigt die zwei Kandidaten, die sich nur an der accountnumber (100230 vs. 200981) unterscheiden.
Du wählst den richtigen Datensatz, optional mit “Entscheidung auf alle Zeilen mit diesem Wert anwenden”, und übernimmst. Jede Entscheidung wird protokolliert. Das ist der manuelle Weg — aber genau diesen Konflikt kannst du an drei Stellen vorab abfangen, ohne die Liste je zu sehen:
a) GUID-Round-Trip (Stufe 1). Exportiere echte Daten statt eines leeren Templates, blende die mitgelieferte Spalte “Parent Account Id” ein, trag bei bekannt mehrdeutigen Zeilen direkt die richtige GUID ein, importiere erneut. GUID gewinnt die Reihenfolge uneingeschränkt — kein Konfliktscreen.
b) Business Key: die Kontonummer (Stufe 2). Genau dafür ist die Spalte “Parent Account Number” da. Die zwei Contoso unterscheiden sich an der accountnumber (100230 vs. 200981). Trag die richtige Nummer in die Business-Key-Spalte, und die App löst über das eindeutige Alternate-Attribut auf — noch bevor die Namenssuche überhaupt startet.
c) Bedingung: ein zweites Merkmal (Stufe 3). Hast du kein GUID/keine Nummer, aber ein anderes unterscheidendes Feld in der Excel-Zeile, häng es als Bedingung an:
name = Excel["Parent Account"] UND address1_country = Excel["Country"]
Jetzt trifft nur noch die deutsche Contoso GmbH zu — die Zeile löst sauber auf. (Die Demo nutzt bereits eine Bedingung auf modifiedon als Zeitanker; nach demselben Muster hängst du deine eigenen an.)
Szenario 3 — Pflichtfeld fehlt: vor dem Matching abgefangen
Zeile Adventure Works ohne Nachnamen: Der Lookup wäre auflösbar, aber der Zeile fehlt ein Pflichtfeld (lastname). Das prüft die App vor dem Schreiben und markiert die Zeile als “Pflichtfeld fehlt”, statt beim Commit in eine rohe API-Exception zu laufen. Ergänze den Wert in Excel und importiere erneut — es ist kein Lookup-Problem, sondern reine Datenvalidierung, die dir früh gemeldet wird.
Szenario 4 — Nicht gefunden: null Treffer
Zeile Berg → Unknown Corp: Stufe 3 sucht, findet aber keinen Account “Unknown Corp” → Status Nicht gefunden. Das ist ehrlicher als ein falscher Treffer. Ursachen sind meist Tippfehler in der Quelle oder ein Datensatz, den es (noch) nicht gibt. Du entscheidest: den Account anlegen, den Namen korrigieren, oder die Zeile bewusst überspringen — in der Konfliktliste bekommt auch “Nicht gefunden” eine Zeile mit “Bearbeiten →”.
Der rote Faden
Vier Zeilen, vier Ausgänge — und in keinem einzigen wird geraten. Jeder Status ist direkt auf eine Matching-Stufe zurückführbar: aufgelöst (Stufe 3, 1 Treffer), mehrdeutig (Stufe 3, >1 Treffer, escalate), nicht gefunden (Stufe 3, 0 Treffer), Pflichtfeld (Validierung davor). Und für die unangenehmen Fälle hast du mit GUID (Stufe 1) und Business Key (Stufe 2) zwei Hebel, um sie schon beim nächsten Round-Trip verschwinden zu lassen.
Unter der Haube
Falls dich das technisch interessiert: LookupImportPlus ist eine Power Apps Code App — du schreibst echten React- + Fluent-UI-Code statt Screens visuell zu bauen, mit Vite als Build-Tool. Der Datenzugriff läuft über die Dataverse-Datenquelle des @microsoft/power-apps-SDKs — die deutlich mehr kann, als die Basis-Connector-Doku vermuten lässt: vollständige Entity-/Attribut-/Beziehungsmetadaten, OData-Paging und die für polymorphe Lookups nötigen Navigation Properties. Kein eigener Connector, kein Plug-in nötig für den Kernfunktionsumfang.
Konfigurationen und Laufhistorie liegen aktuell im localStorage deines Browsers; die App selbst ist das Einzige, was du in deiner Dataverse-Umgebung installierst. Vor jedem Lauf prüft eine Schema-Drift-Preflight deine gespeicherte Konfiguration gegen die aktuellen Metadaten der Tabelle — umbenannte Spalten, entfernte Lookups, geänderte Ziele — und blockiert bei echten Fehlern, während Warnungen nur angezeigt werden. So läufst du nicht mitten im Import in eine rohe API-Exception.
Was noch fehlt
Damit du weißt, worauf du dich einstellen musst:
- Zeilenweise Schreibvorgänge. Commits laufen einzeln über Dataverse (mit begrenzter Nebenläufigkeit und Retry pro Zeile, also durchaus flott), nicht über Bulk-Operationen wie
CreateMultiple/UpdateMultiple. Kommt als nächster Durchsatz-Meilenstein. - Noch keine geteilte, persistierte Historie. Konfigurationen und Laufhistorie liegen im Browser-Speicher jedes einzelnen Nutzers. Team-weit geteilte, in Dataverse gespeicherte Audit-Tabellen (
lip_jobconfiguration,lip_importjobusw.) sind für Phase 2 geplant, aber noch nicht gebaut. - Keine asynchrone Warteschlange für sehr große Läufe. Für Volumina jenseits dessen, was dein Browser live verarbeiten sollte, ist eine Dataverse-native Custom API geplant, die Zeilen als Hintergrundjob verarbeitet — bewusst keine externe Azure Function. Auch das: noch nicht gebaut.
Nichts davon blockiert das Kernversprechen — deterministische, auditierbare Lookup-Auflösung. Das ist Arbeit für später.
Leg selbst los
Der komplette Quellcode ist offen auf GitHub: github.com/brunsforge/LookupImportPlus. Klonen, lokal starten, ausprobieren:
git clone https://github.com/brunsforge/LookupImportPlus
cd LookupImportPlus
npm install && npm run dev # lokal, Demo-Daten, kein Dataverse nötig
npm run deploy -- -EnvironmentUrl https://<org>.crm.dynamics.com # auf eine Trial
Der lokale Modus läuft komplett gegen einen In-Memory-Mock — inklusive der zwei absichtlich kollidierenden “Contoso GmbH”-Accounts aus dem Szenario-Teil oben. So erlebst du einen Konflikt und löst ihn, bevor du überhaupt eine echte Umgebung anfasst.