Technische Architektur
Themen:
Diese Seite richtet sich an Administratoren und technische Nutzer, die den internen Aufbau der DataBridge verstehen oder Probleme diagnostizieren möchten. Für einen konzeptuellen Einstieg siehe Wie es funktioniert. Sequenzdiagramme, Bicep-Module, DI-Verdrahtung und Nachrichten-Schemata stehen in der Entwickler-Dokumentation unter Architektur im Detail.
Azure-Komponenten
Die DataBridge besteht aus mehreren Azure-Diensten, die in einer gemeinsamen Ressourcengruppe betrieben werden:
| Komponente | Dienst | Rolle |
|---|---|---|
| Function App | Azure Functions (Isolated Worker, .NET 10) | Ausführungsumgebung für alle Funktionen |
| App Service Plan | Microsoft.Web/serverfarms | Compute-Plan: Y1 (Consumption, Standard) oder EP1–EP3 (Premium) |
| Service Bus | Azure Service Bus Namespace | Puffer zwischen Erkennung und Verarbeitung |
| Table Storage | Azure Storage Account | Laufzeit-Konfiguration (Connections, Jobs, Mappings, Watermark) |
| Application Insights | Azure Monitor | Logging, Telemetrie, Alerting; Traces und ausgehende Aufrufe werden gesampelt |
| Grafana-Dashboard | Microsoft.Dashboard/dashboards | Vorkonfiguriertes Betriebsdashboard im Application-Insights-Blade (seit v2.1, wird immer mitdeployt) |
| Metric Alerts + Action Group | Azure Monitor | Dead-Letter- und Queue-Längen-Alerts, optional über enableAlerts |
Architektur nach dem C4-Modell
Die folgenden drei Diagramme folgen dem C4-Modell (Context → Container → Component) — von außen nach innen, jede Ebene zoomt weiter in die DataBridge hinein.
Ebene 1 — System Context
Wer nutzt die DataBridge, und mit welchen Systemen tauscht sie Daten aus?
flowchart TB
admin(["👤 Administrator"])
ki(["🤖 KI-Assistent<br/><small>z. B. Claude Code</small>"])
databridge["dsyr.DataBridge<br/><small>Synchronisiert Datensätze zwischen<br/>Quell- und Zielsystemen</small>"]
bc["D365 Business Central<br/><small>ERP — Quelle/Ziel</small>"]
ce["D365 CE / Dataverse<br/><small>CRM — Quelle/Ziel</small>"]
nav["D365 NAV<br/><small>ERP — Quelle/Ziel</small>"]
sql[("SQL-Datenbanken<br/><small>nur Quelle</small>")]
rest["Beliebige REST-API<br/><small>über OpenAPI-Spec</small>"]
admin -->|"konfiguriert, überwacht"| databridge
ki -->|"MCP über HTTPS"| databridge
databridge <-->|"OData v4, OAuth2/API-Key"| bc
databridge <-->|"Dataverse SDK"| ce
databridge <-->|"OData, Basic Auth"| nav
databridge -->|"ADO.NET"| sql
databridge <-->|"REST, aus Spec aufgelöst"| rest
style databridge fill:#1565c0,color:#fff,stroke:#0d47a1,stroke-width:2
style admin fill:#08427b,color:#fff,stroke:#052e56
style ki fill:#08427b,color:#fff,stroke:#052e56
style bc fill:#8b8b8b,color:#fff,stroke:#666
style ce fill:#8b8b8b,color:#fff,stroke:#666
style nav fill:#8b8b8b,color:#fff,stroke:#666
style sql fill:#8b8b8b,color:#fff,stroke:#666
style rest fill:#8b8b8b,color:#fff,stroke:#666Dunkelblau = Personen, Blau = die DataBridge selbst, Grau = externe Systeme — dieselbe
Farbsprache wie im C4-Modell üblich, hier als Flowchart statt als
natives Mermaid-C4Context-Diagramm umgesetzt, weil dessen Layout-Engine bei mehr als
drei-vier Beziehungen zu überkreuzenden Linien neigt.
Ebene 2 — Container
Innerhalb des eigenen Azure-Abonnements des Kunden besteht die DataBridge aus fünf Containern (im C4-Sinn: separat betreibbare/skalierbare Einheiten):
flowchart TB
admin(["👤 Administrator"])
ki(["🤖 KI-Assistent"])
subgraph RG["Ressourcengruppe des Kunden"]
func["Function App<br/><small>.NET 10, Isolated Worker</small>"]
sb[["Service Bus<br/><small>7 Queues</small>"]]
ts[("Table Storage<br/><small>connections/jobs/<br/>mappings/watermark</small>")]
appins["Application Insights"]
graf["Grafana-Dashboard"]
func <-->|"Nachrichten"| sb
func <-->|"Konfiguration & Watermark"| ts
func -->|"Telemetrie"| appins
appins --> graf
end
systems["Quell-/Zielsysteme<br/><small>D365 BC/CE/NAV, SQL, REST</small>"]
admin -->|"konfiguriert (Table Storage)"| func
ki -->|"ruft MCP-Tools auf"| func
func <-->|"liest/schreibt Datensätze"| systems
style func fill:#1565c0,color:#fff,stroke:#0d47a1,stroke-width:2
style sb fill:#1565c0,color:#fff,stroke:#0d47a1
style ts fill:#1565c0,color:#fff,stroke:#0d47a1
style appins fill:#1565c0,color:#fff,stroke:#0d47a1
style graf fill:#1565c0,color:#fff,stroke:#0d47a1
style admin fill:#08427b,color:#fff,stroke:#052e56
style ki fill:#08427b,color:#fff,stroke:#052e56
style systems fill:#8b8b8b,color:#fff,stroke:#666
style RG fill:#f5f5f5,stroke:#999,color:#111111,stroke-dasharray: 4 2Das Grafana-Dashboard wird immer mitdeployt (seit v2.1). Metric Alerts + Action Group sind
zusätzlich optional über den Bicep-Parameter enableAlerts steuerbar und daher hier nicht
als eigener Container dargestellt — siehe Monitoring & Alerts.
Ebene 3 — Component
Innerhalb des Containers Function App sind die Funktionen (Trigger-Ebene) von der Service-Schicht (Logik-Ebene) getrennt. Um das Diagramm lesbar zu halten, wird die Service-Schicht hier als ein Component dargestellt und direkt im Anschluss (Abschnitt Service-Schicht) sowie in der Connector-Hierarchie weiter aufgeschlüsselt — das C4-Modell ist für genau dieses schrittweise Hineinzoomen gedacht.
flowchart TB
subgraph FA["Function App"]
getchanges["GetChanges<br/><small>Timer</small>"]
processq["Process*QueueItems<br/><small>Service-Bus-Trigger (3x)</small>"]
executejob["ExecuteJob / ExecuteHttpRequest<br/><small>HTTP</small>"]
diagapi["CheckList / Validate / JobApi /<br/>MappingApi<br/><small>HTTP, read-only</small>"]
mcp["McpApi<br/><small>MCP-Server für KI-Assistenten</small>"]
orch["Orchestrator<br/><small>Durable, alternativ zu GetChanges</small>"]
services["Service-Schicht<br/><small>InitializationService + 8 Services<br/>— siehe Detail-Diagramm unten</small>"]
connectors["Connector-Schicht<br/><small>IConnector-Implementierungen<br/>— siehe Connector-Hierarchie</small>"]
getchanges --> services
processq --> services
executejob --> services
diagapi --> services
mcp --> services
orch --> services
services --> connectors
end
style services fill:#1565c0,color:#fff,stroke:#0d47a1,stroke-width:2
style connectors fill:#1565c0,color:#fff,stroke:#0d47a1,stroke-width:2
style FA fill:#f5f5f5,stroke:#999,color:#111111,stroke-dasharray: 4 2Die Azure Functions
Alle Funktionen laufen in derselben Function App und teilen sich eine gemeinsame Service-Schicht:
| Funktion | Trigger | Aufgabe |
|---|---|---|
| GetChanges | Timer (konfigurierbar über GetChangesTimerExpression) | Fragt alle fälligen Quellsysteme nach Änderungen und schreibt Nachrichten in die Service Bus Queues |
| ProcessStandardQueueItems | Service Bus sbqexport | Verarbeitet Nachrichten mit Standard-Priorität |
| ProcessHighPriorityQueueItems | Service Bus sbqhighexport | Verarbeitet Nachrichten mit hoher Priorität |
| ProcessLowPriorityQueueItems | Service Bus sbqlowexport | Verarbeitet Nachrichten mit niedriger Priorität |
| ExecuteJob | HTTP POST | Führt einen einzelnen Job manuell und sofort aus |
| ExecuteHttpRequest | HTTP POST | Diagnose: führt eine ad-hoc HTTP-GET-Anfrage gegen eine beliebige URL aus, unabhängig von den konfigurierten Connectoren |
| GetInfo | HTTP GET | Gibt die Versionsnummer der Function App zurück |
| CheckList | HTTP GET | Diagnose: prüft Mappings gegen die Attribute der beteiligten Systeme |
| Validate | HTTP GET | Diagnose: validiert die hinterlegte Konfiguration |
JobApi (GET /job, GET /job/{rowKey}) | HTTP GET | Diagnose: gibt die eingelesene jobs-Konfiguration (alle bzw. ein einzelner Job) zurück |
MappingApi (GET /mapping, GET /mapping/{name}) | HTTP GET | Diagnose: gibt die eingelesene mappings-Konfiguration (alle bzw. ein einzelnes Mapping anhand seines Namens) zurück |
McpApi (POST /mcp) | HTTP POST | Read-only MCP-Server für KI-Assistenten — bündelt Jobs/Mappings/Queues/Connector-Metadaten als Tools |
Zusätzlich existiert eine optionale Durable-Orchestrierung als Alternative zum
Timer-Pfad (StartGetChangesOrchestration, GetChangesOrchestrator, GetChangesActivity
und die Durable Entity JobState). Sie wird ausschließlich per HTTP gestartet und ist im
Regelbetrieb nicht aktiv.
Alle Funktionen lassen sich einzeln per App-Setting deaktivieren
(AzureWebJobs.<Funktionsname>.Disabled), ohne die Function App neu zu deployen.
Queue-Struktur im Service Bus
Der Service Bus Namespace enthält sieben Queues:
flowchart TD
GC[GetChanges] -->|Priorität: Hoch| QH[sbqhighexport]
GC -->|Priorität: Standard| QS[sbqexport]
GC -->|Priorität: Niedrig| QL[sbqlowexport]
QH --> PH[ProcessHighPriorityQueueItems]
QS --> PS[ProcessStandardQueueItems]
QL --> PL[ProcessLowPriorityQueueItems]
PH -->|"Konflikt im Ziel<br/>(HTTP 409)"| TH[sbqtransitivehigh]
PS -->|"Konflikt im Ziel<br/>(HTTP 409)"| TS[sbqtransitivestandart]
PL -->|"Konflikt im Ziel<br/>(HTTP 409)"| TL[sbqtransitivelow]
PH -->|"SkipOutdatedMessages"| IGN[sbqignored]
PS --> IGN
PL --> IGN
PH -->|"max. Zustellversuche<br/>überschritten"| DLQ[("Dead-Letter-Subqueue<br/>je Queue")]
PS --> DLQ
PL --> DLQ
style QH fill:#fce4ec,stroke:#c62828,color:#111111
style QS fill:#fff3e0,stroke:#e65100,color:#111111
style QL fill:#e8f5e9,stroke:#2e7d32,color:#111111
style TH fill:#fce4ec,stroke:#c62828,color:#111111,stroke-dasharray: 4 2
style TS fill:#fff3e0,stroke:#e65100,color:#111111,stroke-dasharray: 4 2
style TL fill:#e8f5e9,stroke:#2e7d32,color:#111111,stroke-dasharray: 4 2
style IGN fill:#f3e5f5,stroke:#6a1b9a,color:#111111
style DLQ fill:#b0bec5,stroke:#37474f,color:#111111| Queue | Zweck |
|---|---|
sbqhighexport / sbqexport / sbqlowexport | Primäre Verarbeitungs-Queues nach Priorität |
sbqtransitivehigh / sbqtransitivestandart / sbqtransitivelow | Ziel-Queues bei Konflikten im Zielsystem (HTTP 409 / Duplikat-Datensatz), damit die Nachricht den Hauptpfad nicht blockiert |
sbqignored | Veraltete Nachrichten (Quelldatum < Zieldatum bei aktivem SkipOutdatedMessages) |
| Dead-Letter-Subqueue | Jede Queue besitzt eine eigene Dead-Letter-Subqueue (<queuename>/$deadletterqueue) für Nachrichten, die nach maxDeliveryCount Versuchen nicht verarbeitet werden konnten |
Auf sbqtransitive* und sbqignored liegt kein Trigger. Nachrichten werden dorthin
ausschließlich geschrieben und verbleiben dort bis zum Ablauf der Time-to-live (14 Tage).
Sie dienen der Isolation und der manuellen Analyse; ein Requeue erfolgt bei Bedarf über
das Werkzeug DeadLetterMover.
Der Name der Standard-Transitive-Queue endet auf -standart. So wird sie vom Deployment
angelegt und so sucht die DataBridge sie — die Schreibweise darf nicht „korrigiert" werden.
Service-Schicht
Alle Funktionen teilen eine gemeinsame Service-Schicht. Die Services selbst sind
DI-Singletons; lazy und thread-sicher nachgeladen wird die Konfiguration beim ersten
Aufruf. Das folgende Diagramm zoomt gegenüber dem C4-Component-Diagramm oben gezielt auf
den InitializationService-Teilbaum und dessen Initialisierungsreihenfolge. InitializationService
(bis v2.3 CoreService) ist reduziert auf Initialisierung und Ready/Error-Zustand — er ist
kein Service-Locator: ProcessJobsForRetrievalInOrderService und ProcessQueueMessageService
werden von den Functions, die sie brauchen (GetChanges/ExecuteJob bzw. Process*QueueItems),
direkt injiziert, nicht über ihn geroutet:
flowchart TB
IS["InitializationService<br/>Lazy Konfigurations-Laden<br/>thread-sicher"]
IS --> STORE["StorageService<br/>Table-Storage-CRUD"]
IS --> CONS["ConnectionService<br/>Verwaltet Connector-Instanzen<br/>je Connection-Name"]
IS --> MS["MappingService<br/>Gruppiert FieldMappings<br/>je Mapping-Name"]
IS --> JS["JobService<br/>Prüft Schedules<br/>validiert Job-Konfiguration"]
IS --> CHK["CheckListService<br/>Diagnose"]
PJRS["ProcessJobsForRetrievalInOrderService<br/>Erkennungs-Pfad"] --> WS["WatermarkService"]
PJRS --> SBS["ServiceBusService"]
PQS["ProcessQueueMessageService<br/>Verarbeitet einzelne<br/>Nachrichten End-to-End"] --> SBS
PQS --> WS
JS --> WS
WS --> STORE
style IS fill:#e8f5f1,stroke:#18b28d,color:#111111,stroke-width:2Daneben stehen Querschnitts-Services, die nicht unter dem InitializationService-Teilbaum hängen:
- SettingsService — liest App-Settings und Connection-Strings aus der Konfiguration (nicht aus Table Storage)
- MetricsService — sendet vorab aggregierte Custom Metrics an Application Insights
(z. B.
ChangesDetectedje Job) - McpToolService — bündelt die Tool-Logik hinter dem MCP-Server:
ruft
JobService,MappingService,ServiceBusServiceundConnectionServiceauf und stößt darüber (wieJobApi/MappingApi) selbst einmaligInitializationService.EnsureInitializedAsyncan — hängt also nicht imInitializationService-Baum, sondern davor
Initialisierungsreihenfolge:
StorageServiceprüft die vier Tabellen und legt fehlende anConnectionServiceliestconnections, deserialisiert die JSON-Verbindungsdaten und führt je System einen Verbindungstest ausMappingServiceliestmappingsund gruppiert die Einträge nach Mapping-NameJobServiceliestjobsund validiert sie: existieren Verbindungen und Mappings? Ist die Cron-Expression gültig?
Schlägt ein Verbindungstest fehl, wird der Fehler protokolliert und der laufende Aufruf
mit einer Exception beendet. Die Initialisierung wird zurückgesetzt und beim nächsten
Timer-Aufruf bzw. bei der nächsten Queue-Nachricht erneut versucht (Self-Repair). Bei
Queue-Nachrichten bedeutet das: die Nachricht wird abandoned und zählt gegen
maxDeliveryCount.
Connector-Hierarchie
Detailansicht der Komponente Connector-Schicht aus dem C4-Component-Diagramm oben — als UML-artiges Klassendiagramm, da C4 Vererbungshierarchien nicht sauber abbilden kann. Jedes Quell- und Zielsystem wird durch einen Konnektor abstrahiert. Alle Konnektoren implementieren dasselbe Interface und sind damit gegeneinander austauschbar:
flowchart TD
IC["«interface»<br/>IConnector"]
CB["ConnectorBase<br/>Serialisierung, Caching,<br/>Filter-Templating,<br/>Key-Vault-Integration"]
HCB["HttpConnectorBase<br/>OAuth2 / Basic Auth,<br/>Token-Lifecycle,<br/>HTTP-Client-Management"]
SCB["SqlConnectorBase<br/>ADO.NET, Schema-Caching,<br/>Paging (OFFSET / FETCH)"]
IC --> CB
CB --> HCB
CB --> SCB
CB --> CE["CeConnector<br/>Dataverse SDK"]
HCB --> BC["BcConnector<br/>OData v4 API"]
HCB --> NAV["NavConnector<br/>OData / Basic Auth"]
HCB --> OA["OpenApiConnector<br/>Generische REST-API,<br/>Pfade aus OpenAPI-Spec"]
SCB --> SQL[MsSqlConnector]
SCB --> PG[PostgreSqlConnector]
SCB --> MY[MySqlConnector]
SCB --> OR[OracleConnector]
style IC fill:#e3f2fd,stroke:#1565c0,color:#111111
style CB fill:#e8f5f1,stroke:#18b28d,color:#111111
style HCB fill:#fff3e0,stroke:#e65100,color:#111111
style SCB fill:#fce4ec,stroke:#c62828,color:#111111Der Dataverse-Konnektor erbt direkt von ConnectorBase, nicht von HttpConnectorBase —
er kommuniziert über den Dataverse ServiceClient statt über einen eigenen HTTP-Client.
Für die Typumwandlung greifen zwei Bausteine ineinander:
- TargetValueMapper — wählt anhand des Zielsystem-Metadatentyps (EDM bei BC/NAV,
AttributeMetadatabei Dataverse) den passenden Resolver aus. Existiert je Connector. - AttributeResolver — führt die eigentliche Typumwandlung durch (Boolean, Date, DateTime, Decimal, Guid, Integer, String; bei Dataverse zusätzlich Picklist, Lookup, Money, State/Status, MultiSelect). Liegt als Resolver-Familie je Systemtyp vor.
Anders als bei den übrigen HTTP-Konnektoren stammen Pfade, Wrapper-Schlüssel
({"clients":[...]} vs. {"client":{...}}) und Feldtypen beim OpenAPI-Konnektor nicht aus
connectorspezifischem Code, sondern werden zur Laufzeit aus einer OpenAPI-Spezifikation
(JSON oder YAML) aufgelöst und pro Prozess zwischengespeichert. Details:
Konnektoren → OpenAPI.
IConnector stellt zusätzlich GetTableMetadata(tableName, apiPath) bereit: liefert Typ,
Nullable, maximale Länge und Primary-Key-Information je Spalte direkt aus dem Zielsystem —
implementiert für alle Connector-Typen, seit v2.3 auch für die SQL-Familie. Dient vor allem dem
MCP-Server zur Validierung von Mappings gegen das echte Schema.
Änderungserkennung pro Connector-Typ
Die Methode, mit der Änderungen erkannt werden, unterscheidet sich je nach System:
| Connector | Mechanismus | Watermark-Typ |
|---|---|---|
| D365BC | OData-$filter auf das im Job konfigurierte Feld WatermarkField (z. B. lastModifiedDateTime) | datetime |
| D365CE / Dataverse | Dataverse Change Tracking API (opaker Token) | string |
| D365NAV | OData-$filter auf das konfigurierte WatermarkField | datetime oder date |
| SQL-Datenbanken | SQL-WHERE-Klausel auf das konfigurierte WatermarkField | date oder datetime |
| OpenAPI | WatermarkField wird als JSON-Pfad auf jeden gelesenen Datensatz angewendet; Filter wird unverändert als Query-String an die API angehängt (API-abhängig) | datetime |
Der Watermark-Wert wird pro Job in der Tabelle watermark gespeichert und am Ende der
Erkennungsphase fortgeschrieben. Ein manuelles Leeren des Watermarks erzwingt einen
vollständigen Neulauf des jeweiligen Jobs.
Über CRUD hinaus: Aktions-Jobs, job-eigene Pfade, eingebettete Kinder
Drei neuere Erweiterungen des Job-/Mapping-Modells betreffen nicht nur den OpenAPI-Konnektor, sind dort aber am relevantesten:
- InvokeAction — ein Job mit einem
Action-Wert, der keinem bekanntenJobActionTypeentspricht, ruft statt eines CRUD-Vorgangs einen benannten, nicht-CRUD-Endpunkt auf (z. B. einen OpenAPI-operationIdoder ein CE-OrganizationRequest). Details: Jobs → InvokeAction. - Job-eigene Source-/Target-Pfade — ein Job kann über
Source/TargetdieSourceTable/TargetTableseiner Mapping-Zeilen überschreiben (bzw. bei fehlendem Mapping ein synthetisches Mapping erzeugen), sodass eine Mapping-Gruppe für mehrere Ressourcen oder Aktionspfade wiederverwendet werden kann. Details: Jobs → Source und Target. - EmbedChildren — bettet die Datensätze eines referenzierten Kind-Jobs bereits beim Lesen in die Nachricht des Elterndatensatzes ein, sodass Eltern- und Kind-Datensätze gemeinsam als eine Nachricht verarbeitet werden (derzeit nur mit D365CE als Quellsystem). Details: Mappings → EmbedChildren.
Retry- und Fehlerverhalten
| Szenario | Verhalten |
|---|---|
| Konflikt im Zielsystem (HTTP 409 / Duplikat) | Weiterleitung in die passende Transitive-Queue |
| Transientes Netzwerkproblem, Zielsystem temporär nicht erreichbar | Backoff (DeliveryCount² · 1000 ms), Abandon, erneute Zustellung nach Lock-Ablauf |
| Veraltete Nachricht (Quelle älter als Ziel) | Bei aktivem SkipOutdatedMessages: Weiterleitung in sbqignored |
| Maximale Zustellversuche überschritten | Weiterleitung in die Dead-Letter-Subqueue der jeweiligen Queue, Alert ausgelöst (wenn konfiguriert) |
| Initialisierungsfehler (Verbindung) | Fehler wird geloggt, laufender Aufruf bricht ab, nächster Aufruf versucht erneut |