Technische Architektur

C4-Modell (Context, Container, Component), Azure-Komponenten, Service-Schichten, Queue-Struktur und Connector-Hierarchie der dsyr.DataBridge.

Azure-Komponenten

Die DataBridge besteht aus mehreren Azure-Diensten, die in einer gemeinsamen Ressourcengruppe betrieben werden:

KomponenteDienstRolle
Function AppAzure Functions (Isolated Worker, .NET 10)Ausführungsumgebung für alle Funktionen
App Service PlanMicrosoft.Web/serverfarmsCompute-Plan: Y1 (Consumption, Standard) oder EP1–EP3 (Premium)
Service BusAzure Service Bus NamespacePuffer zwischen Erkennung und Verarbeitung
Table StorageAzure Storage AccountLaufzeit-Konfiguration (Connections, Jobs, Mappings, Watermark)
Application InsightsAzure MonitorLogging, Telemetrie, Alerting; Traces und ausgehende Aufrufe werden gesampelt
Grafana-DashboardMicrosoft.Dashboard/dashboardsVorkonfiguriertes Betriebsdashboard im Application-Insights-Blade (seit v2.1, wird immer mitdeployt)
Metric Alerts + Action GroupAzure MonitorDead-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:#666

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 2

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 2

Die Azure Functions

Alle Funktionen laufen in derselben Function App und teilen sich eine gemeinsame Service-Schicht:

FunktionTriggerAufgabe
GetChangesTimer (konfigurierbar über GetChangesTimerExpression)Fragt alle fälligen Quellsysteme nach Änderungen und schreibt Nachrichten in die Service Bus Queues
ProcessStandardQueueItemsService Bus sbqexportVerarbeitet Nachrichten mit Standard-Priorität
ProcessHighPriorityQueueItemsService Bus sbqhighexportVerarbeitet Nachrichten mit hoher Priorität
ProcessLowPriorityQueueItemsService Bus sbqlowexportVerarbeitet Nachrichten mit niedriger Priorität
ExecuteJobHTTP POSTFührt einen einzelnen Job manuell und sofort aus
ExecuteHttpRequestHTTP POSTFührt eine ad-hoc HTTP-Anfrage über einen konfigurierten Connector aus
GetInfoHTTP GETGibt die Versionsnummer der Function App zurück
CheckListHTTP GETDiagnose: prüft Mappings gegen die Attribute der beteiligten Systeme
ValidateHTTP GETDiagnose: validiert die hinterlegte Konfiguration
JobApi (GET /job, GET /job/{rowKey})HTTP GETDiagnose: gibt die eingelesene jobs-Konfiguration (alle bzw. ein einzelner Job) zurück
MappingApi (GET /mapping, GET /mapping/{rowKey})HTTP GETDiagnose: gibt die eingelesene mappings-Konfiguration (alle bzw. eine einzelne Zeile) zurück
McpApi (POST /mcp)HTTP POSTRead-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
QueueZweck
sbqhighexport / sbqexport / sbqlowexportPrimäre Verarbeitungs-Queues nach Priorität
sbqtransitivehigh / sbqtransitivestandart / sbqtransitivelowZiel-Queues bei Konflikten im Zielsystem (HTTP 409 / Duplikat-Datensatz), damit die Nachricht den Hauptpfad nicht blockiert
sbqignoredVeraltete Nachrichten (Quelldatum < Zieldatum bei aktivem SkipOutdatedMessages)
Dead-Letter-SubqueueJede Queue besitzt eine eigene Dead-Letter-Subqueue (<queuename>/$deadletterqueue) für Nachrichten, die nach maxDeliveryCount Versuchen nicht verarbeitet werden konnten

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:2

Daneben 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. ChangesDetected je Job)
  • McpToolService — bündelt die Tool-Logik hinter dem MCP-Server: ruft JobService, MappingService, ServiceBusService und ConnectionService auf und stößt darüber (wie JobApi/MappingApi) selbst einmalig CoreService.EnsureInitializedAsync an — hängt also nicht im CoreService-Baum, sondern davor

Initialisierungsreihenfolge:

  1. StorageService prüft die vier Tabellen und legt fehlende an
  2. ConnectionService liest connections, deserialisiert die JSON-Verbindungsdaten und führt je System einen Verbindungstest aus
  3. MappingService liest mappings und gruppiert die Einträge nach Mapping-Name
  4. JobService liest jobs und 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:#c62828

Für die Typumwandlung greifen zwei Bausteine ineinander:

  • TargetValueMapper — wählt anhand des Zielsystem-Metadatentyps (EDM bei BC/NAV, AttributeMetadata bei 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.

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:

ConnectorMechanismusWatermark-Typ
D365BCOData-$filter auf das im Job konfigurierte Feld WatermarkField (z. B. lastModifiedDateTime)datetime
D365CE / DataverseDataverse Change Tracking API (opaker Token)string
D365NAVOData-$filter auf das konfigurierte WatermarkFielddatetime oder date
SQL-DatenbankenSQL-WHERE-Klausel auf das konfigurierte WatermarkFielddate oder datetime
OpenAPIWatermarkField 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 bekannten JobActionType entspricht, ruft statt eines CRUD-Vorgangs einen benannten, nicht-CRUD-Endpunkt auf (z. B. einen OpenAPI-operationId oder ein CE-OrganizationRequest). Details: Jobs → InvokeAction.
  • Job-eigene Source-/Target-Pfade — ein Job kann über Source/Target die SourceTable/TargetTable seiner 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

SzenarioVerhalten
Konflikt im Zielsystem (HTTP 409 / Duplikat)Weiterleitung in die passende Transitive-Queue
Transientes Netzwerkproblem, Zielsystem temporär nicht erreichbarBackoff (DeliveryCount² · 1000 ms), Abandon, erneute Zustellung nach Lock-Ablauf
Veraltete Nachricht (Quelle älter als Ziel)Bei aktivem SkipOutdatedMessages: Weiterleitung in sbqignored
Maximale Zustellversuche überschrittenWeiterleitung 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