Technische Architektur
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.
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,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>CoreService + 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,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 | Führt eine ad-hoc HTTP-Anfrage über einen konfigurierten Connector aus |
| 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/{rowKey}) | HTTP GET | Diagnose: gibt die eingelesene mappings-Konfiguration (alle bzw. eine einzelne Zeile) 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
style QS fill:#fff3e0,stroke:#e65100
style QL fill:#e8f5e9,stroke:#2e7d32
style TH fill:#fce4ec,stroke:#c62828,stroke-dasharray: 4 2
style TS fill:#fff3e0,stroke:#e65100,stroke-dasharray: 4 2
style TL fill:#e8f5e9,stroke:#2e7d32,stroke-dasharray: 4 2
style IGN fill:#f3e5f5,stroke:#6a1b9a
style DLQ fill:#b0bec5,stroke:#37474f| 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 CoreService-Teilbaum und dessen Initialisierungsreihenfolge:
flowchart TB
CS["CoreService<br/>Lazy Konfigurations-Laden<br/>thread-sicher"]
CS --> STORE["StorageService<br/>Table-Storage-CRUD"]
CS --> CONS["ConnectionService<br/>Verwaltet Connector-Instanzen<br/>je Connection-Name"]
CS --> MS["MappingService<br/>Gruppiert FieldMappings<br/>je Mapping-Name"]
CS --> JS["JobService<br/>Prüft Schedules<br/>validiert Job-Konfiguration"]
CS --> CHK["CheckListService<br/>Diagnose"]
CS --> PJRS["ProcessJobsForRetrievalInOrderService<br/>Erkennungs-Pfad"]
CS --> PQS["ProcessQueueMessageService<br/>Verarbeitet einzelne<br/>Nachrichten End-to-End"]
PJRS --> WS["WatermarkService"]
PJRS --> SBS["ServiceBusService"]
PQS --> SBS
PQS --> WS
JS --> WS
WS --> STORE
style CS fill:#e8f5f1,stroke:#18b28d,stroke-width:2Daneben stehen Querschnitts-Services, die nicht unter dem CoreService-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 einmaligCoreService.EnsureInitializedAsyncan — hängt also nicht imCoreService-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
style CB fill:#e8f5f1,stroke:#18b28d
style HCB fill:#fff3e0,stroke:#e65100
style SCB fill:#fce4ec,stroke:#c62828Der 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 BC, NAV, CE und OpenAPI (SQL-Familie: noch offen). 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 |