Wer eine Migration als Transformation anlegt (Teil 1 unserer Serie), muss die Zielstruktur erst einmal bauen: In unserem Fall hunderte Projekt-Sites mit jeweils dutzenden Bibliotheken, einheitlichen Inhaltstypen, projektspezifischen Views und einem mehrstufigen Berechtigungsmodell. Manuell undenkbar — also PnP-Provisioning per Azure Function, Template-getrieben.
Bei kleinen Umgebungen ist das ein gelöstes Problem. Im Großformat kommen drei Gegner dazu: Throttling, fehlende Idempotenz und Parallelität.
Die Architektur
Unser Provisioning läuft als Azure Function mit zertifikatsbasierter App-Only-Authentifizierung (PnP.Core + Key Vault). Ein PnP-Provisioning-Template definiert Inhaltstypen, Felder, Bibliotheken und Views deklarativ; projekt- und baulos-spezifische Strukturen werden dynamisch ergänzt. Wichtig war uns die Trennung:
- Template = konzernweiter Standard (Felder, Inhaltstypen, Basis-Views)
- Code = projektspezifische Dynamik (Anzahl/Benennung der Strukturen, Navigation, Berechtigungsstufen)
So bleibt der Standard reviewbar und versionierbar, ohne dass jede Projektvariante ein eigenes Template braucht.
Gegner 1: Throttling (HTTP 429)
Tausende Objekte anzulegen heißt: zehntausende REST/CSOM-Calls. SharePoint Online drosselt aggressiv, und zwar pro App pro Tenant — Ihr Provisioning konkurriert also mit allem anderen, was Ihre App-Registrierung sonst noch tut.
Was sich bewährt hat:
Retry-Afterrespektieren — immer. Wer nach einem 429 sofort retried, verlängert die Sperre. PnP.Core bringt das Handling mit; eigene HTTP-Aufrufe brauchen es explizit.- Verbindungs-Gating auf Prozessebene. Wir begrenzen die parallelen Verbindungen prozessweit (bei uns: 12) statt pro Task — sonst multiplizieren sich parallele Arbeitspakete unbemerkt zu hunderten simultanen Calls.
- Throttle-Telemetrie mitschreiben. Ein simples JSON-Protokoll (Zeitpunkt, Endpoint, Retry-After-Dauer) zeigt, welche Call-Muster drosseln — Optimierung ohne Messung ist Raten.
- Batching, wo die API es hergibt. Views, Felder und Items lassen sich in
$batch-Requests bündeln; das reduziert Call-Anzahl und damit Throttling-Druck erheblich.
Gegner 2: Fehlende Idempotenz
Bei hunderten Sites wird ein Lauf mittendrin abbrechen — Function-Timeout, Throttling-Kaskade, Deployment. Die wichtigste Eigenschaft eines Enterprise-Provisionings ist deshalb: Jeder Lauf muss gefahrlos wiederholbar sein.
Konkret heißt das: Vor jedem Anlegen prüfen, ob das Objekt existiert — und dann nicht blind überspringen, sondern auf Soll-Zustand prüfen (Drift-Detection). Ein „existiert schon” kann auch „existiert falsch” bedeuten. Ein Beispiel aus unserer Praxis: Ein Case-Sensitivity-Fehler beim View-Vergleich führte dazu, dass Views als vorhanden galten, die es nicht waren — der Lauf war grün, die Struktur unvollständig. Seitdem gilt: Vergleichslogik wird getestet wie Fachlogik.
Gegner 3: Parallelität — das Lost-Update-Race
Der subtilste Fehler des Projekts: Bei paralleler Provisionierung mehrerer Strukturen derselben Site verschwanden sporadisch Navigationseinträge. Von 19 erwarteten Links kamen manchmal nur 6 an — nicht reproduzierbar, nie dieselben.
Die Ursache ist ein klassisches Read-Modify-Write-Race: Zwei parallele Worker lesen dieselbe Navigations-Konfiguration, ergänzen jeweils ihren Eintrag und schreiben zurück — der zweite Schreiber überschreibt den ersten (Lost Update).
Die Lösung: Optimistic Concurrency mit ETag/IF-MATCH und Retry-Schleife:
// Lost-Update-sicheres Update einer geteilten Property
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
var (value, etag) = await ReadWithETagAsync(url); // GET liefert ETag
var updated = AddEntryIdempotent(value, newEntry); // per fachlichem
if (updated is null) return; // Schlüssel: schon da
var request = new HttpRequestMessage(HttpMethod.Put, url)
{
Content = new StringContent(updated)
};
request.Headers.TryAddWithoutValidation("IF-MATCH", etag); // NICHT "*"
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode) return; // gewonnen
if (response.StatusCode == HttpStatusCode.PreconditionFailed)
continue; // 412: verloren → neu lesen
response.EnsureSuccessStatusCode();
}
Drei Details, die den Unterschied machen:
IF-MATCH: *schützt nicht — es sagt nur „überschreibe, was immer da ist”, also exakt das Race, das wir verhindern wollen. Es braucht den konkreten ETag des gelesenen Stands. (Achtung: manche Client-Bibliotheken erzwingen intern*— dann hilft nur der rohe REST-Call.)- Idempotenz per fachlichem Schlüssel: Der Update-Schritt prüft, ob der Eintrag logisch schon existiert — so sind Retries und wiederholte Läufe gefahrlos.
- 412 ist kein Fehler, sondern das System, das funktioniert: Precondition Failed heißt „jemand war schneller” — neu lesen, neu anwenden, neu schreiben.
Lessons Learned
- Provisioning ist Software, kein Skript. Ab einer gewissen Größe braucht es Tests, Telemetrie, Idempotenz und Concurrency-Design — dieselben Standards wie jede andere Backend-Komponente.
- Parallelität zuerst designen, nicht nachrüsten. Jede geteilte Ressource (Navigation, Property Bags, zentrale Listen) ist ein Race-Kandidat.
- Grüne Läufe verifizieren. Ein Provisioning-Lauf ohne Soll-Ist-Abgleich ist eine Hoffnung, kein Ergebnis — dieselbe Lektion wie bei der Migration selbst.
Sie provisionieren SharePoint-Strukturen im dreistelligen Site-Bereich und kämpfen mit Throttling, Abbrüchen oder Heisenbugs? Genau diese Probleme haben wir gelöst — wir unterstützen Sie gerne.
Alle Teile der Serie:
- Warum keine 1:1-Migration
- 25 TB SharePoint-Migration: Warum Standard-Tools nicht reichen
- Die SharePoint Migration API in der Praxis
- Term Store am Limit: Taxonomie-Migration im geteilten Tenant
- Versionshistorie intelligent migrieren
- Provisioning im Großformat: PnP, Throttling, Berechtigungen (dieser Beitrag)
- PnP
- Provisioning
- SharePoint Online
- Azure Functions
- Throttling
- C#