Das ist eine für den Ausdruck optimierte Ansicht des gesamten Kapitels inkl. Unterseiten. Druckvorgang starten.

Zur Standardansicht zurückkehren.

Entwicklung

Für Entwickler der dsyr.DataBridge — lokale Testumgebung mit echten Datenbanken, MCP-Anbindung und die technische Architektur im Detail.

Dieser Bereich richtet sich an Entwickler der dsyr.DataBridge, nicht an Betreiber einer Instanz. Er beschreibt Werkzeuge aus dem Repository, die beim Entwickeln und Testen helfen und nicht Teil des ausgelieferten Produkts sind.


Architektur im Detail

Architektur im Detail

Systemkontext, Bicep-Module, DI und host.json, HTTP- und Nachrichten-Referenz, Sequenzdiagramme der Datenflüsse, Abhängigkeiten und Fähigkeiten-Katalog — eine Ebene tiefer als die Architektur-Übersicht für Administratoren.

→ Zum Abschnitt


Lokale Testumgebung

MCP für die Testumgebung

Den lokalen Container als eigenen MCP-Server databridge-dev in Claude Code nutzen — parallel zur Cloud-Instanz.

→ Zum Artikel

Podman-Testumgebung

DataBridge-Container, Azurite und wahlweise MSSQL, PostgreSQL, MySQL und Oracle — mit Beispieldaten, Connections, Jobs und Mappings, gestartet mit einem Befehl.

→ Zum Artikel

Debuggen

Die DataBridge lokal mit Breakpoints debuggen — gegen den Podman-Stack oder gegen eine echte Azure-Instanz.

→ Zum Artikel

1 - Architektur im Detail

Technische Detaildokumentation für Entwickler: Systemkontext, Bicep-Infrastruktur, Anwendungsaufbau, HTTP- und Nachrichten-Schnittstellen, Datenflüsse, Abhängigkeiten und Fähigkeiten-Katalog.

Dieser Abschnitt beschreibt die dsyr.DataBridge so detailliert, dass sie ohne Lesen des Quellcodes verstanden werden kann. Er richtet sich an Entwickler, die Connectoren, Resolver oder Services erweitern, und an technische Reviewer.

Die Übersicht für Administratoren steht unter Dokumentation → Überblick → Architektur. Dort sind C4-Diagramme, Queue-Struktur und Connector-Hierarchie beschrieben. Die Seiten hier setzen darauf auf und gehen eine Ebene tiefer: Sequenzdiagramme, Bicep-Module, DI-Verdrahtung, Request- und Nachrichten-Schemata.

SeiteInhalt
Systemkontext und PrinzipienZweck, Hauptfunktionen, Architekturprinzipien, Phasen, Betriebs- und Skalierungsmodell
Infrastruktur (Bicep)Module, Ressourcen, App-Settings, Rollen, Alerts, Marketplace-Artefakte
AnwendungsarchitekturProjekte, DI, host.json, Functions, Services, Connector-Basisklassen, Transformations-Pipeline, Domänenobjekte
API- und Nachrichten-ReferenzHTTP-Endpunkte mit Request/Response, Trigger-Bindungen, Service-Bus-Nachrichtenschema, Fehlerverhalten
DatenflüsseSequenzdiagramme für Erkennung, Verarbeitung, Response-Jobs, Fehlerpfad; Persistenz; Watermark-Logik
AbhängigkeitenProjekt- und Azure-Abhängigkeiten, externe Systeme, architekturkritische Kopplungen
Fähigkeiten-KatalogAlle Fähigkeiten mit ihrer technischen Umsetzung

1.1 - Systemkontext und Prinzipien

Zweck, Hauptfunktionen, Architekturprinzipien, die zwei Verarbeitungsphasen sowie Betriebs- und Skalierungsmodell der dsyr.DataBridge.

Zweck

Die dsyr.DataBridge ist eine serverlose Datenintegrations-Bridge auf Azure Functions (.NET 10, Isolated Worker). Sie synchronisiert Datensätze nahezu in Echtzeit zwischen heterogenen Geschäftssystemen, ohne diese direkt zu koppeln. Quelle und Ziel sind über Azure Service Bus entkoppelt, Änderungen werden über Watermarks change-getrieben übertragen.

Die Bridge hält keine Fachdaten dauerhaft. Sie speichert nur Konfiguration und Synchronisationsstände in Azure Table Storage; Nachrichten liegen transient im Service Bus.

Hauptfunktionen

FunktionBeschreibung
Change DetectionPeriodisches Pollen der Quellsysteme (Timer GetChanges, optional Durable-Orchestrierung)
Asynchrone EntkopplungErkannte Änderungen landen als Nachrichten in priorisierten Queues (Low / Standard / High)
Feld-Mapping und TransformationKonfigurierbare Zuordnung inkl. Typ-Coercion, Default-Werten, Option-Mapping, Lookups, Source-/Target-Operationen
Multi-System-KonnektivitätD365 BC, CE/Dataverse, NAV, generische REST-APIs über OpenAPI-Spezifikation, SQL-Datenbanken (MSSQL, PostgreSQL, MySQL, Oracle)
Idempotente ZielverarbeitungUpsert/Delete über Primär-, Custom- oder Filter-Keys, mehrere Schlüssel mit Rangfolge
Aktions-JobsInvokeAction ruft benannte Nicht-CRUD-Endpunkte auf (OpenAPI operationId, CE OrganizationRequest)
WatermarkingPersistierter Stand pro Job, nur neue Änderungen werden übertragen
Response-JobsRückschreiben von im Ziel erzeugten Schlüsseln in die Quelle
Diagnose und KI-AnbindungHTTP-Endpunkte (CheckList, Validate, JobApi, MappingApi) und ein read-only MCP-Server
Selektive TelemetrieGepuffertes Logging pro Invocation, Dependency-Sampling, pre-aggregierte Custom Metrics
FehlerresilienzBackoff-Retry, Transitive-Queues, Dead-Letter-Queues, Alerts

Zielgruppen

ZielgruppeNutzen
Integrations-/ERP-BeraterKonfigurieren Connections, Jobs und Mappings in Table Storage, ohne Code zu deployen
Cloud-/DevOps-EngineersDeployen via Bicep oder Marketplace, betreiben und überwachen die Function App
EntwicklerErweitern Connectoren, Resolver und Source-Operationen in DataBridge.Core
KI-AssistentenLesen Konfiguration und Queue-Zustand über den MCP-Server

Architekturprinzipien

PrinzipUmsetzung
Separation of ConcernsHost (Trigger) ↔ Core (Logik) ↔ Connectoren (System-Anbindung) ↔ Resolver/Mapper (Transformation)
Configuration over CodeConnections, Jobs, Mappings, Watermarks liegen in Table Storage
Lazy InitializationServices sind DI-Singletons; die Konfiguration wird einmalig und thread-sicher beim ersten Aufruf geladen
Asynchrone EntkopplungService Bus zwischen Lese- und Schreibseite; jede Nachricht wird isoliert verarbeitet
IdempotenzUpsert/Delete über definierte Target-Keys
ResilienzBackoff, Lock-Renewal, Transitive-Queues, Dead-Letter-Queues, DeadLetterMover
ErweiterbarkeitSource-Operation-Handler werden per Reflection registriert; Connectoren über ConnectionTypes gemappt; Resolver-Registries je Connector als statische FrozenDictionary
Observability firstGepuffertes Logging, Requests und Exceptions nie gesampelt, Custom Metrics ohne Sampling

Die zwei Phasen

flowchart LR
    SRC["Quellsystem"] -->|"1. Erkennung (Pull)"| FUNC1["GetChanges<br/>oder Orchestrator + Activity"]
    FUNC1 -->|"QueueMessage"| SB[("Service Bus<br/>Low / Standard / High")]
    SB -->|"2. Verarbeitung (Push)"| FUNC2["Process*QueueItems"]
    FUNC2 -->|"Upsert / Delete / Action"| TGT["Zielsystem"]
    FUNC1 -.->|"Watermark"| WM[("watermark")]
    FUNC2 -.->|"Response-Job / Transitive / Ignored"| SB
  • Erkennung: GetChanges (Timer) liest Änderungen aus den Quellsystemen und enqueued sie. Die Durable-Orchestrierung ist eine Alternative, die nur per HTTP gestartet wird und im Regelbetrieb nicht läuft.
  • Verarbeitung: Drei Service-Bus-Trigger (je Priorität) dequeuen und schreiben ins Zielsystem.

Diese Entkopplung erlaubt unabhängige Skalierung beider Seiten und überbrückt temporäre Ausfälle des Zielsystems. Details in Datenflüsse.

Serverless-Modell

  • Azure Functions v4, .NET 10, Isolated Worker (FUNCTIONS_WORKER_RUNTIME=dotnet-isolated).
  • App Service Plan Y1 (Consumption, Windows) als Standard, optional EP1–EP3 über den Bicep-Parameter appServiceSku.
  • functionAppScaleLimit begrenzt die Instanzen (Default 12).
  • Durable Functions mit Azure-Storage-Provider (DurableFunctionsHub).
  • Auslieferung als Run-from-Package-Zip; alternativ als Container-Image (siehe Container-Betrieb).

Betriebsmodell

  • Trigger-gesteuert: Timer, Service Bus, HTTP, Durable.
  • Single-Message-Processing: autoCompleteMessages = false, manuelles Complete/Abandon; maxConcurrentCalls = 12 je Instanz.
  • Function-Timeout: 10 Minuten (host.json).
  • Einzelne Functions abschaltbar über AzureWebJobs.<Name>.Disabled; Bicep setzt das über den Parameter disabledFunctions.
  • Konfigurations-Cache pro Prozess: Änderungen an den Tabellen greifen erst nach einem Neustart der Function App.

Skalierungsmodell

StellschraubeWirkung
appServiceSkuConsumption vs. Premium (kein Cold Start, mehr RAM/CPU)
functionScaleLimitMaximale parallele Instanzen
maxConcurrentCalls (host.json)Parallel verarbeitete Nachrichten je Instanz
Priorisierte QueuesDrei getrennte Trigger für unabhängige Lastpfade
maxPageSize in der Connection, MaxBatchSize in der ActivitySeitengröße beim Abruf
Order und Priority am JobReihenfolge und Priorität

Abgrenzung

  • Keine eigene Benutzeroberfläche. Konfiguration erfolgt in Table Storage, Diagnose über HTTP-Endpunkte und MCP.
  • Kein ETL- oder Data-Warehouse-Werkzeug für Massenläufe, sondern inkrementelle Synchronisation.
  • Transaktionen und fachliche Konsistenz bleiben in den angebundenen Systemen.

1.2 - Infrastruktur (Bicep)

Aufbau des Bicep-Deployments: Module und Parameterfluss, Ressourcen, App-Settings der Function App, Managed Identity und Rollen, Alerts, Marketplace-Artefakte.

Einstiegspunkt ist bicep/main.bicep. Das Deployment läuft im Scope einer bestehenden Resource Group (Bicep-Default, kein explizites targetScope), im Modus Incremental. Die Parameter aus Anwendersicht sind unter Installation beschrieben; hier geht es um den inneren Aufbau.

Module und Parameterfluss

flowchart TD
    MAIN["main.bicep"]

    SA["storageaccount.bicep"]
    AI["applicationinsights.bicep"]
    ASP["appserviceplan.bicep"]
    SB["servicebus.bicep"]
    ALERT["metricalerts.bicep<br/>if enableAlerts"]
    DASH["dashboard.bicep<br/>if enableGrafanaDashboard"]
    FUNC["azurefunction.bicep"]
    ROLES["roleassignments.bicep<br/>if enableManagedIdentity"]

    MAIN --> SA & AI & ASP & SB & ALERT & DASH & FUNC & ROLES

    SA -->|"id, name"| FUNC
    AI -->|"connectionString, id"| FUNC
    AI -->|"id"| DASH
    ASP -->|"id"| FUNC
    SB -->|"authorizationRuleId, name"| FUNC
    SB -->|"id"| ALERT
    FUNC -->|"principalId"| ROLES
    SA -->|"id"| ROLES
    SB -->|"id"| ROLES

Die Abhängigkeiten ergeben sich über Modul-Outputs, es gibt keine Zyklen. Zusätzlich legt main.bicep ein leeres Tracking-Deployment für die Microsoft-Partner-Attribution an.

Ressourcen

RessourceTypSKU / EigenschaftenModul
Storage Account st<name>Microsoft.Storage/storageAccountsStorageV2, Standard_LRS, TLS 1.2storageaccount
Table Service mit vier Tabellen…/tableServices/tablesconnections, jobs, mappings, watermarkstorageaccount
Application Insights appi<name>Microsoft.Insights/componentsRetention 90 Tage, klassisch (ohne eigenen Log-Analytics-Workspace)applicationinsights
App Service Plan asp<name>Microsoft.Web/serverfarmsY1 (capacity 0) oder EP1–EP3, Windowsappserviceplan
Service Bus Namespace sb<name>Microsoft.ServiceBus/namespacesBasicservicebus
Sieben Queues…/namespaces/queuessiehe untenservicebus
Authorization Rule sbexportFunction…/authorizationRulesSend + Listenservicebus
Function App func<name>Microsoft.Web/siteskind functionapp, FTPS only, netFrameworkVersion 10.0, System-Identity immer aktivazurefunction
Grafana-DashboardMicrosoft.Dashboard/dashboardseingebettete JSON-Definitiondashboard
Action Group + fünf Metric AlertsMicrosoft.Insights/actionGroups, metricAlertsnur mit enableAlertsmetricalerts, dlqmetricalert
RollenzuweisungenMicrosoft.Authorization/roleAssignmentsnur mit enableManagedIdentityroleassignments

Queues

Alle Queues haben deadLetteringOnMessageExpiration = true, TTL defaultMessageTimeToLive (Default P14D) und lockDuration (Default PT30S).

QueueZweckMaxDeliveryCount
sbqhighexport, sbqexport, sbqlowexportVerarbeitung nach PrioritätmaxDeliveryCount (Default 3)
sbqtransitivehigh, sbqtransitivestandart, sbqtransitivelowAblage bei Konflikten im Ziel (HTTP 409); kein TriggermaxDeliveryCount × 2
sbqignoredVeraltete Nachrichten bei SkipOutdatedMessages; kein TriggermaxDeliveryCount

Die Schreibweise standart ist in Code und Bicep konsistent und darf nicht geändert werden, weil bestehende Deployments die Queue unter diesem Namen besitzen.

App-Settings der Function App

SettingQuelle
APPLICATIONINSIGHTS_CONNECTION_STRINGOutput des AI-Moduls
AzureWebJobsStorage bzw. AzureWebJobsStorage__accountNameKey-basiert per listKeys() oder identitätsbasiert, je nach enableManagedIdentity
ExportQueueServiceBus bzw. ExportQueueServiceBus__fullyQualifiedNamespaceanalog für den Service Bus
TARGET_BASED_SCALING_ENABLED0, nur im schlüsselbasierten Modus (ohne enableManagedIdentity)
WEBSITE_CONTENTAZUREFILECONNECTIONSTRINGimmer key-basiert (Content Share)
FUNCTIONS_EXTENSION_VERSION, FUNCTIONS_WORKER_RUNTIME~4, dotnet-isolated
WEBSITE_RUN_FROM_PACKAGE1
GetChangesTimerExpressionParameter, Default */15 * * * * *
SkipOutdatedMessagesParameter, Default false
TELEMETRY_CONTROL, TELEMETRY_INFO_SAMPLE_RATE, TELEMETRY_DEPENDENCY_SAMPLE_RATE, TELEMETRY_DEPENDENCY_SLOW_MSParameter, siehe Logging
AzureWebJobs.<Name>.Disabledaus dem Array disabledFunctions

alwaysOn wird nicht gesetzt. Für Premium-Pläne kann das manuell nachgezogen werden.

Managed Identity und Rollen

Die System-Identity der Function App wird immer angelegt. Mit enableManagedIdentity = true entfallen die Schlüssel in den App-Settings, stattdessen weist roleassignments.bicep folgende Rollen zu:

RolleRessourceGrund
Storage Table Data ContributorStorage AccountKonfigurationstabellen, Durable-Tabellen
Storage Blob Data OwnerStorage AccountDurable-Hub-Blobs
Storage Queue Data ContributorStorage AccountDurable-Hub-Queues
Azure Service Bus Data SenderNamespaceEnqueue
Azure Service Bus Data ReceiverNamespaceDequeue
Azure Service Bus Data OwnerNamespaceQueue-Laufzeitdaten für die MCP-Tools list_queue_statuses und get_queue_status

SettingsService liefert Connection-Strings tolerant (null statt Exception); StorageService und ServiceBusService bauen ihre Clients aus dem Connection-String, wenn vorhanden, sonst über DefaultAzureCredential.

Alerts

Mit enableAlerts entstehen eine Action Group (E-Mail an notificationEmail), vier DLQ-Alerts auf DeadletteredMessages (Info ≥ 1, Warning > 50, Error > 200, Critical > 500, Schwellen parametrisierbar) und ein Queue-Längen-Alert auf ActiveMessages > 300000. Alle Alerts sind namespace-weit (EntityName = *). Anwendersicht: Monitoring & Alerts.

Netzwerk

Es gibt keine VNet-Integration, keine Private Endpoints und keine IP-Restriktionen. Storage, Service Bus und Function App sind öffentlich erreichbar; Transport ist TLS 1.2 bzw. HTTPS. Netzwerk-Isolation wäre eine Erweiterung des Bicep-Deployments.

Parameterdateien

bicep/params/databridge.sample.bicepparam ist die Vorlage mit allen Parametern. Kundenspezifische Parameterdateien liegen daneben im Repository und werden von cicd/manage/deploy_bicep.yml verwendet. storageKind erlaubt nur StorageV2; ältere Parameterdateien mit Storage schlagen an der Validierung fehl.

Marketplace-Artefakte

  • bicep/marketplace/mainTemplate.json wird mit GenerateMarketplace.sh aus main.bicep erzeugt und muss nach jeder Bicep-Änderung neu generiert und committet werden.
  • createUiDefinition.json ist Handarbeit und bildet die Parameter auf den Marketplace-Assistenten ab.

CI/CD

  • cicd/manage/build_and_deploy_functionapp.yml baut das Zip, setzt die Version (VersionMajor.VersionMinor.yyyy.MMdd) und veröffentlicht ein globales Artefakt.
  • cicd/manage/deploy_bicep.yml rollt Infrastruktur und Zip je Kunde aus.
  • cicd/manage/build_docfx_and_publish.yml baut und veröffentlicht diese Dokumentation (der Dateiname stammt noch aus der DocFX-Zeit).

1.3 - Anwendungsarchitektur

Projekte, Dependency Injection, host.json, Functions, Service-Schicht, Connector-Basisklassen, Transformations-Pipeline und Domänenobjekte.

Projekte (src/DataBridge.slnx)

ProjektTypZweck
DataBridgeAzure Functions, net10.0, isolatedHost: Trigger, DI, Telemetrie-Bootstrap, Durable Functions, MCP-Endpunkt
DataBridge.CoreClass LibraryServices, Connectoren, Resolver, Mapper, Source-Operationen, Modelle, Telemetrie
DataBridge.TestxUnit v3 + Moq (Microsoft Testing Platform)Unit-Tests; Ordnerstruktur spiegelt DataBridge.Core
DeadLetterMoverKonsolenanwendungRequeue von Dead-Letter-Nachrichten

Dependency Injection (Program.cs)

  • FunctionsApplication.CreateBuilder(args) (moderner Einstiegspunkt statt new HostBuilder()), mit builder.UseMiddleware<InvocationContextMiddleware>(), die jede Invocation in einen InvocationContext einbettet und am Ende das gepufferte Log flusht.
  • Application Insights über AddApplicationInsightsTelemetryWorkerService, Adaptive Sampling deaktiviert; DependencySamplingProcessor per AddApplicationInsightsTelemetryProcessor. Der Standard-AI-Logger-Provider ist auf LogLevel.None gesetzt, einziger Pfad nach AI ist der BufferingLoggerProvider.
  • Alle Services sind Singletons: MetricsService, SettingsService, StorageService, ConnectionService, MappingService, JobService, WatermarkService, ServiceBusService, ProcessQueueMessageService, ProcessJobsForRetrievalInOrderService, CheckListService, InitializationService, McpToolService.
  • Log-Level: Kategorien DataBridge und Function auf Trace, der Puffer-Provider filtert auf Debug.

host.json

BereichEinstellungWert
AllgemeinfunctionTimeout10 min
AI-SamplingexcludedTypesRequest;Exception
DurablehubNameDurableFunctionsHub, Azure-Storage-Provider, Distributed Tracing V2
Storage QueuesbatchSize / maxDequeueCount1 / 5 (nur für Durable relevant)
Service BusautoCompleteMessagesfalse
Service BusmaxConcurrentCalls12
Service BusmaxAutoLockRenewalDuration5 min

Functions

FunctionTriggerAufgabe
GetChangesTimer %GetChangesTimerExpression%Initialisiert thread-sicher über InitializationService und ruft ProcessJobsForRetrievalInOrderService.Execute auf
ProcessStandardQueueItems, ProcessHighPriorityQueueItems, ProcessLowPriorityQueueItemsService Bus sbqexport, sbqhighexport, sbqlowexportVerarbeiten je eine Nachricht end-to-end über ProcessQueueMessageService.Execute
ExecuteJobHTTP POSTStößt einen Job per Name über ProcessJobsForRetrievalInOrderService.ExecuteByJobName an
ExecuteHttpRequestHTTP POSTAd-hoc-HTTP-GET mit eigenem HttpClient (NTLM oder anonym), unabhängig von den Connectoren
GetInfoHTTP GETAssembly-Version
CheckListHTTP GETInitializationService.CheckAsync(): Mappings gegen Live-Metadaten der Systeme
ValidateHTTP GETErzwingt die Initialisierung, liefert das Fehlerlog
GetJobs, GetJobByRowKeyHTTP GET job, job/{rowKey}Geladene Job-Konfiguration
GetMappings, GetMappingByRowKeyHTTP GET mapping, mapping/{name}Geladene Mappings, Suche per Name mit Fallback auf RowKey
Mcp, McpGetHTTP POST/GET mcpMCP-Server; GET antwortet mit 405, weil SSE-Streams nicht unterstützt werden
StartGetChangesOrchestration, GetChangesOrchestrator, GetChangesActivity, JobStateHTTP, Orchestration, Activity, EntityOptionale Durable-Variante der Erkennung

Details zu Request und Response: API-Referenz.

Besonderheiten der Trigger

  • Einmal-Initialisierung: GetChanges, ProcessQueueItems, JobApi, MappingApi und McpToolService halten je eine eigene statische InitializationGuard-Instanz (DataBridge.Core/Services/Initialization/InitializationGuard.cs) — sie kapselt das Lock+Flag-Muster, das vorher in jeder Klasse dupliziert war. Bewusst nicht geteilt: ein Fehler in einer Function zwingt keine der anderen zur Neu-Prüfung. Bei Fehlern wird InitializationService.ResetInitialization() gerufen und beim nächsten Aufruf neu initialisiert (Self-Repair). GetChanges fängt dafür zusätzlich EndpointNotFoundException und CommunicationException.
  • Lock-Renewal: ProcessQueueItems erneuert den Service-Bus-Lock in einem Hintergrund-Task alle 30 Sekunden, bis die Nachricht abgeschlossen ist.
  • Fehlerstempel: Bei Verarbeitungsfehlern werden ErrorMessage, ErrorTimeStamp, ErrorStackTrace und OperationId als Application Properties gesetzt und die Nachricht abandoned. Nach maxDeliveryCount Versuchen landet sie in der Dead-Letter-Subqueue.

Durable-Variante

flowchart TD
    HTTP["StartGetChangesOrchestration"] --> ORCH["GetChangesOrchestrator"]
    ORCH -->|"je Job aus GetJobsToRun"| CHK{"JobState.InProgress?"}
    CHK -->|ja| SKIP["überspringen"]
    CHK -->|nein| ADD["JobState.Add"]
    ADD --> LOOP["CallActivity(GetChangesActivity)"]
    LOOP -->|HasMoreRecord| LOOP
    LOOP -->|fertig| REM["JobState.Remove"]
    ORCH -. Fehler .-> REM

Die Activity liest seitenweise (MaxBatchSize = 3) und sendet jede Nachricht einzeln. Jobs laufen sequentiell. Der Durable-Pfad schreibt die Watermark nicht zurück und ist im Timer-Betrieb nicht aktiv.

Service-Schicht

InitializationService (bis v2.3 CoreService) ist der Einstieg. Er lädt beim ersten Aufruf Storage → Connections → Mappings → Jobs und stellt nur noch den Ready/Error-Zustand bereit. Er ist kein Service-Locator mehr: Functions, die z. B. ProcessJobsForRetrievalInOrderService oder ProcessQueueMessageService brauchen, injizieren diese direkt statt über InitializationService zu routen.

ServiceVerantwortungWichtige Methoden
InitializationServiceInitialisierung, Ready/Error-ZustandEnsureInitializedAsync, CheckAsync, ResetInitialization, GetErrorLogs, HasError
SettingsServiceApp-Settings, Connection-StringsGetConnectionString mit Fallback-Kette ConnectionStrings: → SERVICEBUSCONNSTR_ → SQLCONNSTR_ → CUSTOMCONNSTR_ → App-Setting; GetStorageAccountName, GetServiceBusFullyQualifiedNamespace
StorageServiceTable StorageValidateAndInitialize, GetTableEntries<T>, UpdateTableEntry<T> (Upsert Merge, ohne ETag)
ConnectionServiceConnector je ConnectionInitialize mit Verbindungstest, IsValidConnection, GetConnector
MappingServiceMappings gruppiert nach NameInitialize, GetMapping, ValidateMappings
JobServiceJobs, Validierung, CronInitialize, GetJobsToRun (NCrontab, 6 Felder), GetJobByName
WatermarkServicePersistenz des StandsGetWatermarkByJobName, GetAllWatermarks (Bulk), UpdateWatermarkByJobName; Wertlogik in WatermarkValue
ServiceBusServiceEnqueueSendMessageAsync, AddMessageOrSendAsync (Batch bis 200 oder Größenlimit), FlushBatch; Priorität → Queue
ProcessJobsForRetrievalInOrderServiceErkennungExecute (aktive Jobs nach Order), ExecuteByJobName, ExecuteJob
ProcessQueueMessageServiceVerarbeitungExecute: Deserialisierung, Backoff, Upsert/Delete/Action, Response, Transitive, Ignored
CheckListServiceDiagnoseCheck() gegen Live-Metadaten der Connectoren
MetricsServiceCustom Metricspre-aggregiert über GetMetric, kein Sampling
McpToolServiceMCP-Tool-Logikread-only, siehe MCP-Server
flowchart TB
    INIT["InitializationService"] --> STORE["StorageService"] & CONN["ConnectionService"] & MAP["MappingService"] & JOB["JobService"] & CHK["CheckListService"]
    GC["GetChanges/ExecuteJob"] --> INIT & PJRS["ProcessJobsForRetrievalInOrderService"]
    PQI["ProcessQueueItems"] --> INIT & PQMS["ProcessQueueMessageService"]
    PJRS --> JOB & MAP & CONN & WM["WatermarkService"] & SBS["ServiceBusService"]
    PQMS --> JOB & MAP & CONN & SBS
    JOB --> WM
    WM --> STORE
    CONN --> CONNECTORS["Connectoren"]
    MCP["McpToolService"] --> INIT & JOB & MAP & CONN & SBS

Connector-Basisklassen

Die Hierarchie ist unter Architektur → Connector-Hierarchie abgebildet. Aufgaben der Basisklassen:

  • ConnectorBase<TRecord, TConnectionSetting>: Logger, ConnectionSetting, Lookup-Cache (ConcurrentDictionary<ThreeStringKey,string>), Culture, generische Serialize-Pipeline, Filter-Templating ({{…}}), optionale Key-Vault-Auflösung der Settings über keyVaultUrl + DefaultAzureCredential, GetTableMetadata. Vertragsmethoden: Connect, RetrieveChangesAsync, Upsert, Delete, ProcessMappingSerialization, Key-Aufbau.
  • HttpConnectorBase: HTTP-Client mit Kompression, Basic, OAuth2 Client Credentials (konfigurierbare tokenUrl), API-Key oder Managed Identity (nur bei authmethod: ManagedIdentity, Scope aus der Connection), Token-Refresh.
  • SqlConnectorBase<TConnection>: typisierte DbCommand/DataAdapter, SELECT 1 als Verbindungstest, seitenweises SELECT mit ORDER BY watermarkField, Metadaten über INFORMATION_SCHEMA (Oracle: USER_*/ALL_*), Schema-Cache. Upsert und Delete werfen NotImplementedException; SQL ist nur Quelle.
  • CeConnector: Dataverse ServiceClient, RetrieveEntityChangesRequest mit DataVersion-Token, Batch-Pre-Caching von Lookups, EmbedChildren.
  • OpenApiConnector: Pfade, Wrapper-Schlüssel und Typen aus der OpenAPI-Spezifikation, pro Prozess gecacht.
ConnectionTypesConnector
D365CECeConnector
D365BCBcConnector
D365NAVNavConnector
OPENAPIOpenApiConnector
MSSQL, PGSQL, MYSQL, ORACLEMsSqlConnector, PostgreSqlConnector, MySqlConnector, OracleConnector

Transformations-Pipeline

flowchart LR
    REC["Quell-Record<br/>JObject / DataRow / Entity"] --> KEY["Key-Aufbau → QueueMessage.Id"]
    KEY --> FM{"SourceField<br/>beginnt mit '_'?"}
    FM -->|ja| SOP["SourceOperation-Handler<br/>LookupValue, ReadFromMessage,<br/>ActivityParty, LinkToRecord, EmbedChildren"]
    FM -->|nein| RES["TargetValueMapper → AttributeResolver<br/>Typ-Coercion nach Metadaten"]
    SOP --> ATTR["QueueMessage.Attributes"]
    RES --> ATTR
  • AttributeResolver (Resolvers/Common, D365Bc, D365Ce, D365Nav): Typ-Coercion auf String-Basis (Value + TypeName in der Nachricht). CE hat zusätzlich Picklist, Lookup, Money, State/Status, MultiSelect.
  • TargetValueMapper (Connectors/<X>/<X>TargetValueMapper.cs, SqlTargetValueMapper<T>): wählt anhand des Ziel-Metadatentyps den Resolver. Registry einmalig als FrozenDictionary; Coverage-Tests in DataBridge.Test machen fehlende Registrierungen zum Build-Fehler.
  • Source-Operationen (ISourceOperationHandler): per Reflection registriert, Konstruktor (TConnector, ILogger). Felder mit _-Präfix sind virtuell.

Domänenobjekte

ModellZweck
ConnectionTableEntryPartitionKey connection, RowKey = Name, Type, Json = ConnectionSetting
JobTableEntry / JobPartitionKey job; Source/Target-Connection, Order, Filter, Mapping, WatermarkType/WatermarkField, Schedule, Action, Priority, ResponseJob, Source/Target (job-eigene Pfade)
MappingTableEntry / TableMapping + FieldMappingFeldzuordnung, TargetKey/TargetKeys (mit KeyRank), SourceOperation, TargetOperation
WatermarkTableEntryPartitionKey watermark, RowKey = Job-Name, Watermark, LastCronRun
QueueMessage + QueueMessagePropertyTransportobjekt, siehe Nachrichtenschema
TableMetadata / ColumnMetadataSchema-Informationen für MCP und CheckList
EnumsConnectionTypes, JobActionType, JobPriorityType, TargetKeyType, LookupType, WatermarkType

Querschnitt

  • Telemetrie (DataBridge.Core/Telemetry): BufferingLoggerProvider, InvocationContext (AsyncLocal, operation_id), DependencySamplingProcessor, MetricsService. Verhalten und Steuerung: Logging.
  • Konstanten (DataBridge.Core/Constants): QueueNames, StorageTables, MessageProperties, EnviromentVariables, Errors, MappingDefaultValues.
  • Exceptions: TransitiveException steuert die Ablage in Transitive-Queues.

1.4 - API- und Nachrichten-Referenz

HTTP-Endpunkte mit Request und Response, Trigger-Bindungen, Schema der Service-Bus-Nachrichten und Fehlerverhalten.

Authentifizierung

Alle HTTP-Functions verwenden AuthorizationLevel.Function. Der Funktionsschlüssel wird als Query-Parameter code oder Header x-functions-key übergeben. Es gibt keine Easy-Auth- oder Entra-Konfiguration im Deployment. Timer-, Service-Bus- und Durable-Trigger haben keinen HTTP-Endpunkt.

Routen ohne Route-Attribut folgen der Konvention /api/<FunctionName>.

HTTP-Endpunkte

GET /api/GetInfo

Liefert die Assembly-Version, keine Gesundheitsprüfung.

{ "Version": "2.3.2026.0926" }

POST /api/ExecuteJob

Stößt einen Job aus der Tabelle jobs manuell an (Erkennungsphase).

{ "JobName": "<RowKey des Jobs>" }

Ablauf: JobName prüfen → EnsureInitializedAsync → ProcessJobForRetrieval. Antwort 200 OK; Fehler propagieren als 500. Der Aufruf läuft ohne Job-Locking parallel zum Timer, siehe Datenflüsse → Watermark.

POST /api/ExecuteHttpRequest

Diagnose-Endpunkt: setzt eine rohe HTTP-GET-Anfrage mit eigenem HttpClient ab, unabhängig von den konfigurierten Connectoren.

FeldDefaultBeschreibung
BaseAddresserforderlichBasis-URL
Urlerforderlichrelativer Pfad inkl. Query
MethodGETnur GET implementiert
AuthenticationNTLMNTLM mit UserName/Password, sonst ohne Credentials
ODataMaxVersion, ODataVersion4.0OData-Header
Preferodata.include-annotations="*"Prefer-Header
MediaTypeWithQualityapplication/jsonAccept-Header

Antwort: 200 OK mit dem rohen Antwort-Body des Zielservers.

GET /api/CheckList

EnsureInitializedAsync → InitializationService.CheckAsync(). Prüft Connections, Job-Connections, eindeutige Order, Mapping-Existenz sowie Tabellen und Felder gegen die Live-Metadaten der Connectoren. Antwort 200 OK mit serialisiertem CheckResponse, auch wenn Befunde vorliegen.

GET /api/Validate

Erzwingt die Initialisierung. 200 OK bei Erfolg, sonst 500; Body text/plain mit InitializationService.GetErrorLogs().

GET /api/job, GET /api/job/{rowKey}

Geladene Jobs (alle bzw. einer). 404, wenn der RowKey unbekannt ist.

GET /api/mapping, GET /api/mapping/{name}

Geladene Mappings. Die Suche läuft über den Mapping-Namen (PartitionKey), Fallback RowKey.

POST /api/mcp

Streamable-HTTP-Endpunkt des MCP-Servers. GET /api/mcp antwortet mit 405, weil kein SSE-Stream angeboten wird. Tools und Nutzung: MCP-Server.

GET /api/StartGetChangesOrchestration

Startet die Durable-Orchestrierung. Antwort 202 Accepted mit der Instance-ID und den Standard-Status-URLs des Durable-Clients.

Trigger-Bindungen

FunctionBindungKonfiguration
GetChangesTimer%GetChangesTimerExpression%, Bicep-Default */15 * * * * *; RunOnStartup nur im Debug-Build
ProcessStandardQueueItemsService BusQueue sbqexport, Connection ExportQueueServiceBus, AutoCompleteMessages = false
ProcessHighPriorityQueueItemsService BusQueue sbqhighexport
ProcessLowPriorityQueueItemsService BusQueue sbqlowexport
GetChangesOrchestratorOrchestrationDurable
GetChangesActivityActivityMaxBatchSize = 3
JobStateEntityAdd, Remove, InProgress, Reset

Die Datei sample.local.settings.json setzt lokal * */2 * * * *. Effektiv gilt immer der App-Setting-Wert der jeweiligen Umgebung.

Nachrichtenschema (Service Bus)

Body: JSON-serialisiertes QueueMessage (Newtonsoft.Json).

FeldTypBeschreibung
IdstringSchlüssel des Quell-Datensatzes
JobNamestringerzeugender Job
ActionCodeJobActionTypeNewUpdate, Delete, TimeStamp, DeleteOnUpdate oder ein InvokeAction-Name
AttributesDictionary<string, QueueMessageProperty>gemappte Werte als Value (string) + TypeName
ModifiedOndatetime?Änderungszeitpunkt der Quelle, Basis für SkipOutdatedMessages
ChangeTrackerstring[JsonIgnore], nur für die Watermark während der Erkennung
NeedToIgnorebool[JsonIgnore]

Application Properties:

PropertyWann gesetzt
JobName, SourceKey, Timestampbeim Senden
ErrorMessage, ErrorTimeStamp, ErrorStackTrace, OperationIdbeim Abandon nach einem Fehler

SourceKey nutzt der DeadLetterMover zur Deduplizierung. OperationId ist die W3C-Root-ID und erlaubt die Korrelation mit Application Insights.

Fehlerverhalten

SituationVerhalten
ExecuteJob ohne JobNameException → 500
InitialisierungsfehlerValidate liefert 500 mit Log; Timer- und Queue-Trigger setzen die Initialisierung zurück und versuchen es beim nächsten Aufruf erneut
Verarbeitungsfehler in ProcessQueueItemsAbandon mit Fehler-Properties; nach maxDeliveryCount in die Dead-Letter-Subqueue
TransitiveException (HTTP 409 im Ziel)Nachricht wird in die Transitive-Queue der Priorität kopiert und im Original abgeschlossen; kein Trigger auf der Transitive-Queue
Veraltete Nachricht bei SkipOutdatedMessagesKopie nach sbqignored, Original abgeschlossen
Wiederholte ZustellungProcessQueueMessageService wartet DeliveryCount² · 1000 ms vor dem Versuch
EndpointNotFoundException, CommunicationException in GetChangesReset der Initialisierung, nächster Timer-Lauf versucht erneut

1.5 - Datenflüsse

Sequenzdiagramme für Erkennung (Timer und Durable), Verarbeitung, Response-Jobs und Fehlerpfad; Persistenz der Daten; Watermark-Logik; DeadLetterMover.

Die konzeptuelle Beschreibung steht unter Wie es funktioniert. Hier folgen die Abläufe auf Klassenebene.

Erkennung (Timer)

sequenceDiagram
    autonumber
    participant Timer as GetChanges (Timer)
    participant Init as InitializationService
    participant PJ as ProcessJobsForRetrievalInOrderService
    participant Job as JobService
    participant WM as WatermarkService
    participant Conn as Source-Connector
    participant SBS as ServiceBusService
    participant SB as Service Bus

    Timer->>Init: EnsureInitializedAsync()
    Note over Init: einmalig: Storage → Connections → Mappings → Jobs
    Timer->>PJ: Execute()
    PJ->>Job: GetJobsToRun()
    Note over Job,WM: GetAllWatermarks() (Bulk) für LastCronRun
    loop je Job nach Order
        PJ->>WM: GetWatermarkByJobName(job)
        PJ->>Conn: RetrieveChangesAsync(mapping, job, watermark, filter)
        loop seitenweise
            Conn-->>PJ: QueueMessage (serialisiert)
            PJ->>SBS: AddMessageOrSendAsync(msg, priority)
            alt Batch voll (200) oder Größenlimit
                SBS->>SB: SendMessagesAsync(batch)
            end
            PJ->>PJ: lastChangeTracker = msg.ChangeTracker
        end
        PJ->>SBS: FlushBatch()
        PJ->>WM: UpdateWatermarkByJobName(newWatermark, lastCronRun)
    end
  • Die Serialisierung (Key-Aufbau → Feld-Mappings → Source-Operationen / Resolver) passiert im Connector (ConnectorBase.Serialize).
  • Die Watermark wird erst am Ende eines Jobs geschrieben. Bricht der Lauf vorher ab, wird beim nächsten Lauf erneut ab dem alten Stand gelesen; SkipOutdatedMessages fängt Doppel-Nachrichten im Ziel ab.
  • Bei reinem Zeitstempel-Vorrücken ohne fachliche Änderung wird JobActionType.TimeStamp verwendet.
  • MetricsService zählt ChangesDetected je Job.

Erkennung (Durable)

sequenceDiagram
    autonumber
    participant HTTP as StartGetChangesOrchestration
    participant Orch as GetChangesOrchestrator
    participant State as JobState (Entity)
    participant Act as GetChangesActivity
    participant Conn as Source-Connector
    participant SBS as ServiceBusService

    HTTP-->>HTTP: 202 + InstanceId
    loop je Job aus GetJobsToRun
        Orch->>State: InProgress(jobName)?
        alt in Bearbeitung
            Orch->>Orch: überspringen
        else
            Orch->>State: Add(jobName)
            loop do … while HasMoreRecord
                Orch->>Act: CallActivity(JobActivityParam)
                Act->>Conn: RetrieveChangesAsync(…, MaxBatchSize=3)
                Act->>SBS: SendMessage(record, priority)
                Act-->>Orch: Page, Cookie, Watermark, HasMoreRecord
            end
            Orch->>State: Remove(jobName)
        end
    end

Der Durable-Pfad persistiert die Watermark nicht und ist im Timer-Betrieb nicht aktiv.

Verarbeitung (Service Bus → Zielsystem)

sequenceDiagram
    autonumber
    participant SB as Service Bus Queue
    participant PQI as Process*QueueItems
    participant Init as InitializationService
    participant PQM as ProcessQueueMessageService
    participant Conn as Target-Connector
    participant SBS as ServiceBusService

    SB->>PQI: Nachricht
    PQI->>PQI: Lock-Renewal-Task starten (30 s)
    PQI->>Init: EnsureInitializedAsync()
    PQI->>PQM: Execute(busMessage)
    PQM->>PQM: QueueMessage deserialisieren, Job + Mapping laden
    alt DeliveryCount > 1
        PQM->>PQM: Backoff DeliveryCount² · 1000 ms
    end
    alt SkipOutdatedMessages und Quelle älter als Ziel
        PQM->>SBS: SendMessage → sbqignored
    else NewUpdate
        PQM->>Conn: Upsert(message, mapping[, responseMapping, job])
        opt Response vorhanden
            PQM->>SBS: SendMessage(response, priority)
        end
    else Delete
        PQM->>Conn: Delete(message, mapping)
    else InvokeAction
        PQM->>Conn: InvokeAction(message, job)
    end
    alt Erfolg
        PQI->>SB: CompleteMessageAsync()
    else TransitiveException
        PQM->>SBS: SendMessage → sbqtransitive*
        PQI->>SB: CompleteMessageAsync()
    else andere Exception
        PQI->>SB: AbandonMessageAsync(+ Error-Properties)
        Note over SB: nach maxDeliveryCount ⇒ Dead-Letter
    end
    PQI->>PQI: Lock-Renewal stoppen

MetricsService zählt MessagesProcessed mit dem Ausgang (Success, Transitive, Ignored, Failed).

Response-Job

sequenceDiagram
    autonumber
    participant ProcA as Process*QueueItems (Job A: CRM → BC)
    participant BC as Target-Connector BC
    participant SB as Service Bus
    participant ProcB as Process*QueueItems (ResponseJob: BC → CRM)
    participant CRM as Target-Connector CRM

    ProcA->>BC: Upsert(message, mapping, responseMapping, job)
    BC-->>ProcA: QueueMessage mit erzeugtem Ziel-Schlüssel
    ProcA->>SB: SendMessage(responseMessage, priority)
    SB->>ProcB: Response-Nachricht
    ProcB->>CRM: Upsert (z. B. BC-Belegnummer zurückschreiben)

Fehlerpfad

flowchart TD
    MSG["Queue-Nachricht"] --> PROC["ProcessQueueMessageService.Execute"]
    PROC -->|Erfolg| DONE["Complete"]
    PROC -->|TransitiveException| TRANS["sbqtransitive*<br/>kein Trigger, liegt bis TTL"]
    PROC -->|veraltet| IGN["sbqignored<br/>kein Trigger"]
    PROC -->|andere Exception| AB["Abandon + Error-Properties"]
    AB --> RETRY{"DeliveryCount<br/>< maxDeliveryCount?"}
    RETRY -->|ja| MSG
    RETRY -->|nein| DLQ["Dead-Letter-Subqueue"]
    DLQ -.->|manuell| DLM["DeadLetterMover"]
    TRANS -.->|manuell| DLM
    DLM -.->|Resubmit ohne Error-Properties| MSG

Transitive- und Ignored-Queues werden nicht automatisch weiterverarbeitet. Sie dienen der Isolation und der Analyse. Ein Requeue erfolgt über den DeadLetterMover.

DeadLetterMover

Konsolenanwendung unter src/DeadLetterMover. Sie liest eine Queue oder deren $DeadLetterQueue, entscheidet anhand der ErrorMessage per Muster (Resubmit, None, Skip) und sendet Nachrichten ohne Error-Properties zurück in die Original-Queue, dedupliziert über SourceKey. Connection-String, Queue-Name und der Schalter UseDeadletter stehen als Konstanten im Code und werden vor dem Lauf angepasst. Der Connection-String darf nicht eingecheckt werden.

Persistenz

DatenartOrtLebensdauer
KonfigurationTable Storage connections, jobs, mappingspersistent, manuell gepflegt
Watermarks (Watermark, LastCronRun)Table Storage watermarkpersistent je Job
NachrichtenService-Bus-Queuestransient, TTL P14D
Durable-StateAzure Storage DurableFunctionsHublaufzeitgebunden
TelemetrieApplication Insights90 Tage
Fachdatennur Quell- und Zielsysteme–

Watermark-Logik

Die Wertlogik liegt in WatermarkValue (Models/Common), der Service persistiert nur.

  • GetWatermarkByJobName liefert bei fehlendem Eintrag eine nicht-persistierte Entity; LastCronRun == null bedeutet „Job ist fällig“.
  • GetAllWatermarks ist eine Bulk-Abfrage auf PartitionKey watermark und wird vom Scheduler genutzt, damit nicht je Job eine Einzelabfrage anfällt.
  • WatermarkValue.Initialize(type, stored) setzt Typ-Defaults (date → 0001-01-01, datetime → 0001-01-01T00:00:00Z). Der Typ wird getrimmt und case-insensitiv geparst; ein unbekannter Typ belässt den gespeicherten Wert und erzeugt eine Warnung.
  • Advance(changeTracker) vergleicht typabhängig mit InvariantCulture und RoundtripKind.
  • UpdateTableEntry schreibt ohne ETag (Last-Writer-Wins). Der HTTP-Endpunkt ExecuteJob kann parallel zum Timer laufen; ein veralteter Stand ist immer älter und führt zu Wiederholungen, nicht zu Lücken.
  • Reset: Feld Watermark in der Tabelle leeren → vollständiger Neulauf. Anwendersicht: Watermarks.

1.6 - Abhängigkeiten

Projekt-Abhängigkeiten, Azure-Dienste, externe Systeme und architekturkritische Kopplungen.

Plattform

  • net10.0 in allen Projekten, ImplicitUsings und Nullable aktiv.
  • Azure Functions v4, Isolated Worker, OutputType=Exe.
  • Tests auf xUnit v3 mit Microsoft Testing Platform. Aufruf mit dotnet test --project …; das Positionsargument funktioniert nicht mehr.

Projekte

flowchart LR
    HOST["DataBridge (Host)"] --> CORE["DataBridge.Core"]
    DLM["DeadLetterMover"] --> CORE
    TEST["DataBridge.Test"] --> CORE
    TEST --> HOST

NuGet-Pakete

Die Versionen werden zentral in src/Directory.Packages.props gepflegt (Central Package Management); die .csproj-Dateien referenzieren Pakete nur noch ohne Version. Ein Update passiert also an genau einer Stelle.

Sicherheitsprüfung beim Build: Directory.Packages.props aktiviert NuGet Audit für alle Projekte, inklusive transitiver Pakete, und meldet Treffer jeder Schwere. Ab „moderate“ (NU1902–NU1904) gelten sie als Fehler, „low“ (NU1901) bleibt eine Warnung — solche Lücken betreffen oft Funktionen, die gar nicht genutzt werden, und sollen kein Release blockieren. Ein Paket mit einer Sicherheitslücke ab „moderate“ lässt damit Restore und Build lokal, im Dockerfile und in der Pipeline scheitern. Abhilfe ist ein Update der betroffenen Version; liegt die Lücke in einem transitiven Paket, dieses mit eigener Version in Directory.Packages.props aufnehmen. Weil neue Advisories jederzeit erscheinen, kann ein bisher grüner Stand ohne Code-Änderung rot werden. Ist der Advisory-Feed nicht erreichbar, bleibt es bei der Warnung NU1900, offline bauen funktioniert also weiter.

Hier nur die Rolle der wichtigsten Pakete:

PaketProjektRolle
Microsoft.Azure.Functions.Worker.*HostRuntime, HTTP/ASP.NET-Core-, Service-Bus-, Timer-, Storage-, Durable-Extensions
Microsoft.ApplicationInsights.WorkerService, Microsoft.ApplicationInsightsHost, CoreTelemetrie. Pin auf 2.23.0, weil 3.x ITelemetryProcessor entfernt hat
ModelContextProtocol.AspNetCoreHostMCP-Server
ModelContextProtocol.CoreCorenur McpException, damit Tool-Fehlerursachen den MCP-Client erreichen
Azure.Data.TablesCoreTable Storage
Azure.Messaging.ServiceBusCoreQueues, Queue-Laufzeitdaten
Azure.Identity, Azure.Security.KeyVault.SecretsCoreDefaultAzureCredential, optionaler Key-Vault-Bezug
Microsoft.PowerPlatform.Dataverse.ClientCoreCE-Connector
Microsoft.PowerPlatform.Dataverse.Client.AzAuthCoreCE-Connector, Managed Identity
Microsoft.OData.Edm, Microsoft.OData.CoreCore, HostEDM-Metadaten für BC/NAV
Microsoft.OpenApi, Microsoft.OpenApi.YamlReaderCoreOpenAPI-Connector
Microsoft.Data.SqlClient, Npgsql, MySql.Data, Oracle.ManagedDataAccess.CoreCoreSQL-Connectoren
NCrontab.SignedCoreCron-Auswertung mit Sekundenfeld
Newtonsoft.JsonalleSerialisierung von QueueMessage und Konfigurations-JSON
System.Linq.AsyncCoreIAsyncEnumerable beim seitenweisen Abruf

Alle DB-Treiber werden immer mitgeladen, auch wenn nur ein Teil genutzt wird.

Azure-Dienste

DienstVerwendungKritikalität
Azure Functions / App Service PlanAusführungkritisch
Azure Service Bus (Basic)Messaging zwischen Erkennung und Verarbeitungkritisch
Azure Table StorageKonfiguration und Watermarkskritisch
Azure Blob/File/Queue StorageFunctions-Runtime, Content Share, Durable-Statekritisch
Application InsightsTelemetriehoch
Grafana-Dashboard (Azure Managed)Betriebsdashboardoptional
Azure Key Vaultoptionaler Secret-Bezug der Connectoren, nicht deploytoptional

Externe Systeme

SystemProtokoll / SDKLesenSchreiben
D365 Business CentralOData v4, OAuth2 / Basic / Managed Identityjaja
D365 CE / DataverseServiceClient (Connection-String oder Managed Identity über AzAuth), Change Trackingjaja
D365 NAVOData, OAuth2 / Basicjaja
REST-API mit OpenAPI-SpezifikationHTTP, API-Key / OAuth2 / Basicjaja
MSSQL, PostgreSQL, MySQL, OracleADO.NETjanein

Architekturkritische Kopplungen

AbhängigkeitRisiko
Service Bus BasicKeine Topics, Sessions, Partitionierung, Duplicate Detection
Table Storage als Konfig-StoreFehlkonfiguration wirkt global; Prüfung nur über CheckListService und JobService.Initialize
Dataverse-SDK-VersionEnge Kopplung des CE-Connectors
NCrontab.SignedBestimmt die Scheduling-Semantik
Eigener BufferingLoggerProviderDiagnose hängt am Custom-Sampling; AI-Standardprovider ist abgeschaltet
Newtonsoft.JsonVerbindliches Wire-Format der Nachrichten
String-Typisierung der NachrichtenJeder Resolver parst selbst inkl. Locale; Quelle von Kulturproblemen zwischen Systemen

1.7 - Fähigkeiten-Katalog

Katalog aller Fähigkeiten der dsyr.DataBridge mit ihrer technischen Umsetzung.

Integration und Datenfluss

FähigkeitUmsetzung
Change Detection (Pull)GetChanges → ProcessJobsForRetrievalInOrderService; je Connector RetrieveChangesAsync (BC/NAV $filter, CE RetrieveEntityChangesRequest, SQL seitenweises SELECT, OpenAPI Query-String)
Orchestrierter AbrufDurable: GetChangesOrchestrator, GetChangesActivity, Entity JobState
Asynchrone EntkopplungService Bus mit sieben Queues, ServiceBusService mit Batch bis 200
Priorisierte VerarbeitungJobPriorityType → Queue; drei Trigger
ReihenfolgensteuerungJob.Order
Idempotentes SchreibenConnector.Upsert/Delete; TargetKey/TargetKeys mit KeyRank, erster Treffer gewinnt
Aktions-JobsInvokeAction: Job-Action ohne CRUD-Bedeutung ruft einen benannten Endpunkt
Job-eigene PfadeSource/Target am Job überschreiben SourceTable/TargetTable des Mappings
Eingebettete KinderEmbedChildren (nur CE als Quelle)
Response-JobsUpsert(..., responseMapping, job) → Response-Nachricht
WatermarkingWatermarkService + WatermarkValue; Typen date, datetime, string

Konnektivität

FähigkeitUmsetzung
D365 Business CentralBcConnector : HttpConnectorBase, OAuth2 / Basic / Managed Identity
D365 CE / DataverseCeConnector : ConnectorBase, ServiceClient (Connection-String oder url + Managed Identity)
D365 NAVNavConnector : HttpConnectorBase
REST über OpenAPIOpenApiConnector : HttpConnectorBase, Spec aus openApiSpecUrl
MSSQL, PostgreSQL, MySQL, OracleSqlConnectorBase<T>, nur lesend
Auth-Varianten HTTPBasic, OAuth2 Client Credentials mit tokenUrl, API-Key, Managed Identity (explizit über authmethod)
Key-Vault-BezugkeyVaultUrl in der Connection + DefaultAzureCredential
Schema-MetadatenIConnector.GetTableNames, GetTableMetadata für alle Connector-Typen
Ad-hoc-HTTPExecuteHttpRequest (nur GET)

Transformation und Mapping

FähigkeitUmsetzung
Feld-MappingTableMapping + FieldMapping aus mappings
Typ-CoercionAttributeResolver-Familien je Systemtyp
Metadaten-basierte Resolver-WahlTargetValueMapper je Connector, statische FrozenDictionary-Registry mit Coverage-Tests
Default-WerteTarget-Operationen DefaultValue, DefaultValueIfNullOrEmpty, DefaultValueMap
Option-/Picklist-MappingOptionMapping, CE-Resolver für Picklist, MultiSelect, State/Status
Lookup-AuflösungSource-Operation LookupValue mit Cache; CE mit Batch-Pre-Caching; DoNotCreate
CE-BeziehungenActivityParty, LinkToRecord
ReadFromMessageWert aus der Nachricht übernehmen
Virtuelle Felder_-Präfix im SourceField, nie gegen Metadaten validiert
ErweiterbarkeitSource-Operation-Handler per Reflection

Konfiguration und Steuerung

FähigkeitUmsetzung
Konfigurationsgetriebener BetriebTable Storage, StorageService, SettingsService
Cron je JobJob.Schedule, NCrontab.Signed, sechs Felder
Manueller Job-StartExecuteJob
Function-TogglesAzureWebJobs.<Name>.Disabled, Bicep disabledFunctions
Konfig-DiagnoseCheckList, Validate, JobApi, MappingApi
KI-AnbindungMCP-Server mit read-only Tools
Self-Repair der InitialisierungResetInitialization bei Fehlern
Quell-FilterJob.Filter mit {{…}}-Templating
Veraltete Nachrichten überspringenSkipOutdatedMessages → sbqignored

Resilienz

FähigkeitUmsetzung
Backoff-RetryDeliveryCount² · 1000 ms
Lock-RenewalHintergrund-Task alle 30 s
Konflikt-IsolationTransitiveException → sbqtransitive*
Dead-LetteringmaxDeliveryCount, Fehler-Properties an der Nachricht
RequeueDeadLetterMover
KommunikationsfehlerGetChanges fängt EndpointNotFoundException, CommunicationException

Observability

FähigkeitUmsetzung
Gepuffertes LoggingBufferingLoggerProvider, Flush pro Invocation
Dependency-SamplingDependencySamplingProcessor
Custom MetricsMetricsService (ChangesDetected, MessagesProcessed)
KorrelationInvocationContext, operation_id
SteuerungTELEMETRY_CONTROL, drei Raten-Settings
AlertsDLQ-Stufen, Queue-Länge, Action Group
DashboardGrafana-Dashboard aus Bicep
VersionGetInfo

Infrastruktur und Auslieferung

FähigkeitUmsetzung
Infrastructure as CodeBicep mit Modulen und .bicepparam
MarketplacecreateUiDefinition.json, generierte mainTemplate.json
Identitätsbasierter ZugriffenableManagedIdentity + Rollen
Serverless-SkalierungY1 / EP1–EP3, functionScaleLimit
Run-from-PackageWEBSITE_RUN_FROM_PACKAGE=1
ContainerDockerfile, compose.yml
VersionierungMajor.Minor.yyyy.MMdd, manueller Bump
CI/CDAzure Pipelines unter cicd/
Lokale EntwicklungPodman-Testumgebung mit DBs und HTTP-Mocks

Qualitätssicherung

FähigkeitUmsetzung
Unit-TestsDataBridge.Test, xUnit v3, Moq
CoverageMicrosoft Testing Platform Code Coverage
Resolver-Vollständigkeit*TargetValueMapperCoverageTests
DokumentationHugo/Docsy unter docs/, veröffentlicht per Pipeline

2 - MCP für die Testumgebung

Den MCP-Server der lokalen Podman-Testumgebung als eigenen Server databridge-dev in Claude Code nutzen — parallel zur Cloud-Instanz.

Die DataBridge in der Podman-Testumgebung bringt denselben MCP-Server mit wie jede andere Instanz. Damit kann ein KI-Assistent die Beispiel-Jobs und -Mappings lesen und die Metadaten der Testdatenbanken abfragen, ohne dass dafür etwas in Azure laufen muss.

Zwei Server nebeneinander

Die .mcp.json im Repository definiert zwei DataBridge-Server:

ServerZielURL
databridgeEine Azure-Instanz (bzw. ohne Konfiguration der lokale Host)${DATABRIDGE_MCP_URL:-http://localhost:7062}/api/mcp?code=${DATABRIDGE_FUNCTION_KEY:-local}
databridge-devDie Podman-Testumgebunghttp://localhost:7062/api/mcp?code=local
{
  "mcpServers": {
    "databridge": {
      "type": "http",
      "url": "${DATABRIDGE_MCP_URL:-http://localhost:7062}/api/mcp?code=${DATABRIDGE_FUNCTION_KEY:-local}"
    },
    "databridge-dev": {
      "type": "http",
      "url": "http://localhost:7062/api/mcp?code=local"
    }
  }
}
  • databridge zeigt auf eine Azure-Instanz, sobald DATABRIDGE_MCP_URL und DATABRIDGE_FUNCTION_KEY gesetzt sind — üblicherweise im Block env der persönlichen .claude/settings.local.json (siehe MCP-Server → Nutzung).
  • databridge-dev zeigt immer auf die Testumgebung. Der Function Key ist dort fest auf local gesetzt (dev/functions-secrets/host.json), es muss also nichts konfiguriert werden.

In Claude Code erscheinen die Tools entsprechend mit den Präfixen mcp__databridge__… und mcp__databridge-dev__…. Im Prompt genügt es zu sagen, welcher Server gemeint ist, z. B. „frag über databridge-dev die Tabellen von oracle-dev ab“.

Port 7062 ist derselbe wie beim Debuggen aus der IDE. Läuft gleichzeitig ein lokaler Function Host aus Visual Studio, Rider oder VS Code, belegt er den Port — dann entweder den Host oder die Testumgebung stoppen, oder in dev/.env einen anderen DATABRIDGE_PORT wählen und die URL von databridge-dev entsprechend anpassen.

Was sich damit testen lässt

Alle Tools aus MCP-Server → Operationen stehen zur Verfügung. Mit den Beispieldaten sind vor allem diese sinnvoll:

ToolBeispielErgebnis
list_jobs—Die generierten Jobs, z. B. oracle-to-mssql-account
get_jobrowKey: "mssql-to-postgres-opportunity"Quelle, Ziel, Mapping und Watermark des Jobs
list_mappings—dev-account, dev-opportunity, dev-opportunityproduct
get_mappingrowKey: "dev-account"Die 19 Feldzuordnungen von Account
list_connector_tablesconnectionName: "postgres-dev"account, opportunity, opportunityproduct
get_connector_table_schemaconnectionName: "oracle-dev", tableName: "Account"Spalten mit .NET-Typ, Nullable, Länge, Precision/Scale und Primary Key
list_connector_tablesconnectionName: "bc-dev" (apiPath optional, leer = v2.0)customers, opportunities, opportunityLines (aus der EDMX des Mocks, nur mit dev/up.sh … bc)
get_connector_table_schemaconnectionName: "nav-dev", tableName: "Customers"8 Spalten, Primärschlüssel No
get_connector_table_schemaconnectionName: "openapi-dev", tableName: "customers"8 Spalten aus der OpenAPI-Spec des Mocks
get_connector_table_schemaconnectionName: "bc-dev", tableName: "v2.0/opportunities" (apiPath weggelassen)Funktioniert ebenso — das Tool trennt den API-Pfad selbst ab, wenn er noch Teil von tableName ist

Die Service-Bus-Tools (list_queue_statuses, get_queue_status, peek_*) funktionieren, sobald das Profil servicebus läuft (dev/up.sh servicebus oder automatisch über getchanges/process, siehe Service Bus). Ohne dieses Profil liefern sie einen Verbindungsfehler, weil unter ExportQueueServiceBus dann nur der Dummy-Connection-String steht.

Weil in allen Datenbanken dieselben Tabellen liegen, eignet sich die Umgebung gut, um die Typ-Abbildung der Connectoren zu vergleichen: dieselbe Spalte, z. B. IsActive oder CreatedOnUtc, einmal je Connection abfragen.

Ohne Claude Code aufrufen

Der Endpoint spricht Standard-MCP über HTTP und lässt sich auch direkt aufrufen, z. B. zum schnellen Prüfen nach einer Code-Änderung:

Invoke-RestMethod 'http://localhost:7062/api/mcp?code=local' -Method Post `
  -ContentType 'application/json' -Headers @{ Accept = 'application/json, text/event-stream' } `
  -Body '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_connector_table_schema","arguments":{"connectionName":"mssql-dev","tableName":"Account"}}}'
curl -s 'http://localhost:7062/api/mcp?code=local' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_connector_table_schema","arguments":{"connectionName":"mssql-dev","tableName":"Account"}}}'

Log

Jeder MCP-Aufruf schreibt eine Zeile ins Log der DataBridge, mit dem Namen der Operation und — wo vorhanden — dem gelesenen Job, Mapping bzw. der Connection und Tabelle:

MCP list_jobs
MCP get_job: job=oracle-to-mssql-account
MCP get_mapping: mapping=dev-opportunity
MCP get_connector_table_schema: connection=oracle-dev, table=Account

In der Testumgebung sieht man sie mit:

podman compose -f dev/compose.dev.yml logs -f databridge | Select-String "MCP "
podman compose -f dev/compose.dev.yml logs -f databridge | grep "MCP "

3 - Podman-Testumgebung

Lokale Testumgebung mit Podman: DataBridge-Container, Azurite, wahlweise MSSQL, PostgreSQL, MySQL und Oracle, HTTP-Mocks für Business Central, NAV und OpenAPI sowie ein Service-Bus-Emulator — inklusive Beispieldaten, Connections, Jobs und Mappings.

Die Testumgebung unter dev/ startet eine vollständig vorkonfigurierte DataBridge auf dem eigenen Rechner — ohne Azure-Ressourcen und ohne Kosten. Sie ist gedacht, um Connectoren gegen echte Datenbanken und gegen HTTP-Mocks (Business Central, NAV, OpenAPI) zu testen, ohne vorher Mappings und Jobs von Hand anzulegen.

Was gestartet wird

flowchart LR
    subgraph db["Datenbanken (wählbar)"]
        mssql[(MSSQL)]
        pg[(PostgreSQL)]
        my[(MySQL)]
        ora[(Oracle)]
    end
    init["&lt;db&gt;-init<br/>Schema + Testdaten"] --> db
    seed["seed<br/>connections, jobs, mappings"] --> azurite[(Azurite<br/>Table Storage)]
    azurite -- "Konfiguration" --> bridge["databridge<br/>Function App :7062"]
    db -- "SQL-Connectoren" --> bridge
    wiremock["wiremock :8089 (wählbar)<br/>BC / NAV / OpenAPI"] -- "HTTP-Connectoren" --> bridge
    subgraph sb["Service Bus (wählbar: servicebus/getchanges/process)"]
        sbdb[(servicebus-db)] --> emulator["servicebus<br/>Emulator :5672 / :5300"]
        emulator --> explorer["servicebus-explorer<br/>Web-UI :8090 (wählbar)"]
    end
    bridge -- "GetChanges legt Nachrichten ab" --> emulator
    emulator -- "Process*QueueItems liest" --> bridge

Die Pfeile zeigen, wer wen versorgt: Init und Seed befüllen Datenbanken und Azurite. Die DataBridge liest ihre Konfiguration aus Azurite und greift über ihre Connectoren auf die Datenbanken und auf WireMock zu. Ist das Profil servicebus aktiv, legt GetChanges erkannte Änderungen auf die Queues des Emulators, Process*QueueItems liest sie von dort. Die Connectoren lesen und schreiben.

ContainerAufgabeLäuft
mssql, postgres, mysql, oracleDatenbankserver, nur die gewähltendauerhaft
<db>-initLegt je Datenbank Schema und Testdaten aneinmalig pro Start
azuriteEmulator für Azure Table/Blob/Queue Storagedauerhaft
wiremockHTTP-Mock für Business Central, NAV und eine OpenAPI-Beispiel-API, nur die gewählten (siehe HTTP-Mocks)dauerhaft, nur wenn mindestens ein Mock gewählt ist
servicebusService-Bus-Emulator, nur wenn servicebus/getchanges/process gewählt sinddauerhaft, nur wenn gewählt
servicebus-dbEigener SQL Server als Speicher des Emulators (nicht die Dev-Datenbank mssql)dauerhaft, nur wenn servicebus gewählt ist
servicebus-explorerGrafische Web-Oberfläche für den Emulator (Service Bus einsehen)dauerhaft, nur wenn servicebus gewählt ist
seedSchreibt connections, jobs und mappings nach Azuriteeinmalig pro Start
databridgeDie DataBridge selbst, gebaut aus dem aktuellen Quellcode (src/Dockerfile). Fehlt im Host-Modus (--host)dauerhaft, außer im Host-Modus

Die DataBridge startet erst, wenn alle Init-Container und der Seed erfolgreich durchgelaufen sind. Timer- und Queue-Trigger (GetChanges, Process*QueueItems) sind standardmäßig deaktiviert, lassen sich aber gezielt zuschalten (siehe Verarbeitung schalten). HTTP-Funktionen und der MCP-Server laufen immer normal.

Voraussetzungen

  • Podman 5 oder neuer.
  • Compose-Provider für podman compose: docker-compose 2.20 oder neuer (empfohlen) bzw. ein aktuelles podman-compose. Benötigt werden depends_on mit condition und required: false.
  • Windows/macOS: eine eingerichtete podman machine. up.ps1 und down.ps1 -Reset starten sie bei Bedarf selbst. Die PowerShell-Skripte laufen unter PowerShell 7 und Windows PowerShell 5.1.
  • Arbeitsspeicher: abhängig von der Auswahl, siehe Speicherbedarf.
  • Freie Ports: 7062 (DataBridge — im Host-Modus stattdessen vom lokalen Prozess belegt), 10000–10002 (Azurite) sowie die Ports der gewählten Datenbanken (siehe Datenbanken). Mit servicebus/getchanges/process zusätzlich 5672 (AMQP) und 5300 (Management) für den Service-Bus-Emulator — diese beiden Ports sind fest und nicht über dev/.env änderbar — sowie SERVICEBUS_EXPLORER_PORT (Standard 8090) für dessen Web-Oberfläche.

Speicherbedarf

Gemessene Werte im laufenden Betrieb (podman stats, gerundet):

ContainerRAMLäuft
databridge + azurite~0,3 GBimmer
postgres~0,1 GBwenn gewählt
mysql~0,5 GBwenn gewählt
mssql~1,5 GBwenn gewählt
oracle~2,5 GBwenn gewählt
wiremock (alle Mocks in einem Container)~0,2 GBwenn ein Mock gewählt ist
servicebus + servicebus-db~0,5 GB + ~1,5 GBwenn servicebus/getchanges/process gewählt sind (siehe Service Bus)
servicebus-explorer~0,2 GB (ungefähr, .NET-Web-App)wenn servicebus/getchanges/process gewählt sind

Die Init-Container und seed beenden sich nach dem Start und belegen danach keinen Speicher.

up all (ohne Service Bus) kommt damit auf rund 5 GB, mit getchanges/process/servicebus zusätzlich rund 2 GB für den Emulator und seine SQL-DB — macOS/Windows-Leser bitte beachten: servicebus-db läuft zusätzlich zu mssql, wenn beide gewählt sind (getrennte Container, siehe Service Bus). Unter Windows und macOS laufen alle Container in der VM der podman machine, die zusätzlich Kernel und Dateicache belegt und Speicher nur zögerlich an das Host-System zurückgibt. Der Prozess vmmem bzw. die VM wächst daher deutlich darüber hinaus und kann den Rechner spürbar ausbremsen. Unter Linux laufen die Container ohne VM und der Stack merklich flüssiger.

Im Normalfall nur das starten, was man gerade testet, also die Datenbank bzw. den Mock des Connectors, an dem man arbeitet (z. B. dev\up.ps1 postgres oder dev\up.ps1 mssql bc). Die Auswahl lässt sich dauerhaft über DATABRIDGE_DBS / DATABRIDGE_MOCKS in dev/.env festlegen. all ist für den gelegentlichen Gesamttest gedacht und braucht einen Rechner mit 16 GB oder mehr.

Unter Windows ist der größte Posten meist nicht der Container selbst, sondern der Dateicache der VM (Images, Datenbankdateien, Volumes). Linux zählt ihn als verfügbar, Windows sieht ihn aber als belegt, weil WSL ihn nicht von selbst zurückgibt. vmmemWSL kann so auf 13 GB und mehr wachsen, obwohl die Container nur rund 5 GB brauchen. Prüfen lässt sich das mit podman machine ssh free -h (Spalte buff/cache).

Cache sofort freigeben (laufende Container bleiben unberührt):

podman machine ssh "sync; echo 3 | sudo tee /proc/sys/vm/drop_caches"

Dauerhaft in %USERPROFILE%\.wslconfig die VM begrenzen und ungenutzten Cache automatisch an Windows zurückgeben lassen (danach wsl --shutdown):

[wsl2]
memory=8GB

[experimental]
autoMemoryReclaim=gradual

Beim ersten Start lädt Podman die Images (mehrere GB, vor allem Oracle und MSSQL) und baut die DataBridge. Das dauert einige Minuten; folgende Starts sind deutlich schneller.

Starten und beenden

Alle Befehle werden aus dem Repository-Stammverzeichnis aufgerufen.

dev\up.ps1                          # Auswahl aus DATABRIDGE_DBS / DATABRIDGE_MOCKS in dev\.env
dev\up.ps1 mssql oracle              # nur die genannten Datenbanken, keine Mocks
dev\up.ps1 mssql bc openapi          # Datenbanken und Mocks gemischt
dev\up.ps1 bc                        # nur Mocks – Datenbanken dann aus DATABRIDGE_DBS in dev\.env
dev\up.ps1 all                       # alle vier Datenbanken und alle drei Mocks
dev\up.ps1 mssql process             # Datenbank + Verarbeitung an (Service-Bus-Emulator automatisch mit)
dev\up.ps1 mssql getchanges          # Datenbank + Timer-Trigger GetChanges an
dev\up.ps1 servicebus                # nur den Emulator dazu, ohne Verarbeitung
dev\up.ps1 -HostMode mssql process   # Host-Modus: kein databridge-Container, Debugger auf Port 7062

dev\down.ps1                         # alles stoppen, Daten bleiben erhalten (VM aus: nichts zu tun)
dev\down.ps1 -Reset                  # alles stoppen und alle Daten löschen (startet die VM bei Bedarf)
dev/up.sh                    # Auswahl aus DATABRIDGE_DBS / DATABRIDGE_MOCKS in dev/.env
dev/up.sh mssql oracle       # nur die genannten Datenbanken, keine Mocks
dev/up.sh mssql bc openapi   # Datenbanken und Mocks gemischt
dev/up.sh bc                 # nur Mocks – Datenbanken dann aus DATABRIDGE_DBS in dev/.env
dev/up.sh all                # alle vier Datenbanken und alle drei Mocks
dev/up.sh mssql process      # Datenbank + Verarbeitung an (Service-Bus-Emulator automatisch mit)
dev/up.sh mssql getchanges   # Datenbank + Timer-Trigger GetChanges an
dev/up.sh servicebus         # nur den Emulator dazu, ohne Verarbeitung
dev/up.sh --host mssql process  # Host-Modus: kein databridge-Container, Debugger auf Port 7062

dev/down.sh                  # alles stoppen, Daten bleiben erhalten
dev/down.sh -v               # alles stoppen und alle Daten löschen (kompletter Reset)

Auswahlregeln:

  • Argumente gehen vor dev/.env. Datenbanken (mssql, postgres, mysql, oracle), Mocks (bc, nav, openapi) und Toggles (getchanges, process, servicebus) dürfen in beliebiger Reihenfolge gemischt werden.
  • Wer Argumente angibt, bekommt nur die genannten Mocks bzw. Toggles. dev/up.sh mssql startet also ohne WireMock und ohne Verarbeitung, auch wenn DATABRIDGE_MOCKS/ DATABRIDGE_GETCHANGES/DATABRIDGE_PROCESS in dev/.env gesetzt sind.
  • Mindestens eine Datenbank läuft immer mit, weil Seed und Jobs eine brauchen. Nennt man nur Mocks oder Toggles, kommen die Datenbanken aus DATABRIDGE_DBS.
  • getchanges oder process aktivieren automatisch das Profil servicebus. servicebus allein ist erlaubt, um die Queues z. B. per MCP zu inspizieren, ohne dass etwas verarbeitet wird.
  • Unbekannte Namen brechen mit einer Fehlermeldung ab.

Was up im Einzelnen tut

  1. Prüft, ob podman und podman compose verfügbar sind (unter Windows/macOS auch die podman machine). Unter Windows räumt up.ps1 vor dem Start der podman machine eine liegengebliebene Socket-Datei weg (siehe Fehlersuche) und wartet, bis podman compose die VM erreicht.
  2. Legt dev/.env aus dev/.env.example an, falls sie fehlt.
  3. Stoppt Datenbank-Container, die nicht gewählt sind, WireMock, wenn kein Mock gewählt ist, den Service-Bus-Emulator, wenn weder servicebus noch getchanges/process gewählt sind, und im Host-Modus (--host) den databridge-Container. Daten-Volumes bleiben erhalten.
  4. Baut die DataBridge aus dem aktuellen Quellcode und startet alle gewählten Container.
  5. Läuft der Service-Bus-Emulator mit, wartet up zusätzlich, bis dessen Management-API antwortet (kein Docker-Healthcheck möglich, siehe Service Bus).
  6. Ohne Host-Modus: startet die DataBridge nach dem Seed neu, weil sie ihre Konfiguration nur beim Start liest. Im Host-Modus: generiert stattdessen src/DataBridge/local.settings.json (siehe Debuggen) und beendet sich, ohne auf einen Container zu warten.
  7. Ohne Host-Modus: wartet, bis GetInfo antwortet, und gibt MCP-Endpoint und Zugangsdaten aus.

up kann beliebig oft aufgerufen werden. Nach einer Code-Änderung genügt ein erneutes up: Das Image wird neu gebaut, unveränderte Schichten kommen aus dem Cache.

Konfiguration (dev/.env)

VariableStandardBedeutung
DATABRIDGE_DBSmssqlDatenbanken, wenn up ohne Argument aufgerufen wird (kommagetrennt)
DEV_DB_PASSWORDDataBridge_Dev_2026Ein Passwort für alle Datenbanken (SA, App-User, Root)
DATABRIDGE_MOCKSleerMocks, wenn up ohne Argument aufgerufen wird (kommagetrennt: bc,nav,openapi). Leer heißt: kein WireMock, keine Mock-Connections und -Jobs
DATABRIDGE_GETCHANGESoffan/off — Timer-Trigger GetChanges, wenn up ohne Argument aufgerufen wird (siehe Verarbeitung schalten)
DATABRIDGE_PROCESSoffan/off — die drei Process*QueueItems-Funktionen, wenn up ohne Argument aufgerufen wird
DATABRIDGE_PORT7062Port der DataBridge
WIREMOCK_PORT8089Host-Port von WireMock (9090 ist unter Fedora oft von Cockpit belegt)
SERVICEBUS_EXPLORER_PORT8090Host-Port der Service-Bus-Explorer-Weboberfläche (siehe Service Bus einsehen)
MSSQL_PORT, POSTGRES_PORT, MYSQL_PORT, ORACLE_PORT1433, 5432, 3306, 1521Host-Ports der Datenbanken

Das Passwort enthält bewusst keine Sonderzeichen außer _, damit es in allen Connection-Strings und Kommandozeilen-Clients ohne Quoting funktioniert. Es muss die Komplexitätsregeln von SQL Server erfüllen (mindestens 8 Zeichen, 3 von 4 Zeichenklassen). dev/.env ist nicht eingecheckt.

Datenbanken

ArgumentConnectionImageHost-PortBenutzerDatenbank / Schema
mssqlmssql-dev (MSSQL)mssql/server:2022-latest1433saDataBridgeTest, Schema dbo
postgrespostgres-dev (PGSQL)postgres:175432databridgedatabridgetest, Schema public
mysqlmysql-dev (MYSQL)mysql:8.43306databridgedatabridgetest
oracleoracle-dev (ORACLE)gvenzl/oracle-free:23-slim-faststart1521databridgeService FREEPDB1, Schema DATABRIDGE

Mit den Host-Ports kann man sich auch mit eigenen Werkzeugen verbinden (SSMS, Azure Data Studio, DBeaver, psql, SQL Developer …). Azurite ist im Azure Storage Explorer über UseDevelopmentStorage=true erreichbar — Details siehe Azurite einsehen.

Hinweise je Datenbank:

  • PostgreSQL legt unquotierte Bezeichner in Kleinschreibung an. Tabellen und Spalten heißen dort account, accountid usw. Die DataBridge vergleicht Namen ohne Rücksicht auf Groß-/Kleinschreibung, Account funktioniert also genauso.
  • MySQL unterscheidet unter Linux bei Tabellennamen Groß-/Kleinschreibung. Die Tabellen heißen exakt Account, Opportunity und OpportunityProduct.
  • Oracle legt unquotierte Bezeichner in Großschreibung an (ACCOUNT, ACCOUNTID). Es wird Oracle 23ai Free verwendet, weil erst diese Version BOOLEAN, JSON und CREATE TABLE IF NOT EXISTS kennt.

Beispieldaten

In jeder gewählten Datenbank werden dieselben drei Tabellen mit denselben Daten angelegt. Die Skripte liegen unter dev/<db>/init/:

  • 01_schema.sql legt die Tabellen an, nur wenn sie fehlen.
  • 02_data.sql löscht die Testdaten und fügt sie neu ein, bei jedem Start. Wer in einer Testdatenbank Daten verändert hat, bekommt mit dem nächsten up den Ausgangszustand zurück.

Schemaänderungen an den Skripten wirken erst nach einem Reset (down -v), weil bestehende Tabellen nicht verändert werden.

Tabellen

erDiagram
    Account ||--o{ Opportunity : "AccountId"
    Opportunity ||--o{ OpportunityProduct : "OpportunityId"

Die Spalten decken bewusst möglichst viele Datentypen ab. Jede Datenbank verwendet dafür ihren nativen Typ:

Account — Firmen (3 Datensätze)

SpalteMSSQLPostgreSQLMySQLOracle
AccountId (PK)uniqueidentifieruuidchar(36)varchar2(36)
AccountNumber (eindeutig)varchar(20)varchar(20)varchar(20)varchar2(20)
Namenvarchar(160)varchar(160)varchar(160)nvarchar2(160)
Descriptionnvarchar(max)texttextnclob
CountryCodechar(2)char(2)char(2)char(2)
NumberOfEmployeesintintegerintnumber(10)
IndustryCodesmallintsmallintsmallintnumber(5)
Ratingtinyintsmallinttinyintnumber(3)
Revenuemoneynumeric(19,4)decimal(19,4)number(19,4)
CreditLimitdecimal(18,2)numeric(18,2)decimal(18,2)number(18,2)
Latitudefloatdouble precisiondoublebinary_double
Longituderealrealfloatbinary_float
IsActivebitbooleanbooleanboolean
FoundedOndatedatedatedate
OpeningTimetime(0)time(0)timeinterval day to second
LastContactAtdatetime2(3)timestamp(3)datetime(3)timestamp(3)
CreatedOnUtcdatetimeoffset(3)timestamptz(3)timestamp(3)timestamp(3) with time zone
Logovarbinary(max)byteablobblob
Attributes (JSON)nvarchar(max)jsonbjsonjson
ModifiedOn (Watermark)datetime2(3)timestamp(3)datetime(3)timestamp(3)

Opportunity — Verkaufschancen (4 Datensätze)

SpalteMSSQLPostgreSQLMySQLOracle
OpportunityId (PK)uniqueidentifieruuidchar(36)varchar2(36)
AccountId (FK)uniqueidentifieruuidchar(36)varchar2(36)
Namenvarchar(200)varchar(200)varchar(200)nvarchar2(200)
EstimatedValuedecimal(19,4)numeric(19,4)decimal(19,4)number(19,4)
Probabilitytinyintsmallinttinyintnumber(3)
EstimatedCloseDatedatedatedatedate
StatusCodeintintegerintnumber(10)
IsWonbitbooleanbooleanboolean
ActualCloseAtdatetime2(0)timestamp(0)datetimetimestamp(0)
ExternalRefbigintbigintbigintnumber(19)
ModifiedOn (Watermark)datetime2(3)timestamp(3)datetime(3)timestamp(3)

OpportunityProduct — Positionen der Verkaufschancen (6 Datensätze)

SpalteMSSQLPostgreSQLMySQLOracle
OpportunityProductId (PK, Identity)bigint identitybigint identitybigint auto_incrementnumber(19) identity
OpportunityId (FK)uniqueidentifieruuidchar(36)varchar2(36)
LineNumberintintegerintnumber(10)
ProductNamenvarchar(200)varchar(200)varchar(200)nvarchar2(200)
Quantitydecimal(18,4)numeric(18,4)decimal(18,4)number(18,4)
PricePerUnitdecimal(19,4)numeric(19,4)decimal(19,4)number(19,4)
DiscountPercentfloatdouble precisiondoublebinary_double
TaxRaterealrealfloatbinary_float
IsOptionalbitbooleanbooleanboolean
DeliveryDatedatedatedatedate
ModifiedOn (Watermark)datetime2(3)timestamp(3)datetime(3)timestamp(3)

Datensätze

Die Werte sind in allen Datenbanken identisch, einschließlich der Schlüssel. So lassen sich Ergebnisse zwischen den Datenbanken direkt vergleichen.

AccountNummerLandBesonderheit
Müller GmbHACC-1000DEAlle Felder gefüllt, inkl. Logo (Binärdaten) und JSON
Schmidt & Söhne KGACC-1001ATUmlaute und & im Namen, keine Beschreibung
Weber AGACC-1002CHInaktiv, viele leere Felder (NULL)
VerkaufschanceAccountStatusPositionen
Neue FertigungslinieMüller GmbHoffen3 (davon 1 optional)
Wartungsvertrag 2027Müller GmbHgewonnen1
Ersatzteile Q4Schmidt & Söhne KGoffen1
RahmenvertragWeber AGverloren1

Die Schlüssel sind feste GUIDs (a0000000-…-000000000001 für Accounts, b0000000-…-000000000001 für Verkaufschancen) bzw. die Positionsnummern 1–6. Die Identity-Spalte von OpportunityProduct ist so gesetzt, dass neue Zeilen ab 7 weiterzählen.

Connections, Jobs und Mappings

Der Container seed schreibt die Konfiguration nach Azurite — passend zu den gewählten Datenbanken. Die Vorlagen liegen unter dev/seed/:

Azurite einsehen

Anders als der Service-Bus-Emulator bringt Azurite keine eigene Web-Oberfläche mit. Der übliche Weg ist der kostenlose Microsoft Azure Storage Explorer (Windows/macOS/Linux):

  1. Links im Baum unter „Local & Attached“ → „Storage Accounts“ — Azurite ist dort meist automatisch als „(Emulator - Default Ports) (Key)“ gelistet.
  2. Falls nicht: Rechtsklick auf „Storage Accounts“ → „Connect to Azure Storage“ → „Connection string“ und den Connection-String einfügen, den up.sh/up.ps1 beim Start ausgeben (Zeile Azurite) — er verwendet den öffentlich dokumentierten Azurite-Well-known-Key, ist also kein Geheimnis.
  3. Unter „Tables“ liegen connections, jobs, mappings und watermark — Zeilen lassen sich dort auch bearbeiten, z. B. um einen Job per Hand auf IsActive: true zu setzen (siehe Verarbeitung schalten), ganz ohne MCP.

Alternativen: die VS-Code-Erweiterung „Azure Storage“ (derselbe Explorer-Baum direkt in der IDE) oder az storage … --connection-string "…" für Skripte.

DateiInhalt
connections.jsonEine Connection je Datenbank (Name, Typ, Connection-String mit ${DEV_DB_PASSWORD})
tables.jsonDie drei Tabellen mit Schlüssel, Watermark-Feld und den gemappten Feldern
seed.pyErzeugt daraus die Einträge und gleicht die Azure Tables ab

Einträge, die nicht mehr zur Auswahl passen, entfernt der Seed. Wer von all auf mssql wechselt, hat danach also auch nur noch die MSSQL-Connection und ihre Jobs.

Connections

Je gewählter Datenbank eine Connection: mssql-dev, postgres-dev, mysql-dev, oracle-dev (siehe Datenbanken). Je gewähltem Mock kommt bc-dev, nav-dev bzw. openapi-dev hinzu (siehe HTTP-Mocks).

Jobs

Für jede Kombination aus Quell- und Zieldatenbank entsteht ein Job je Tabelle. Die Namen folgen dem Muster <quelle>-to-<ziel>-<tabelle>:

Gewählte DatenbankenKombinationenJobsBeispiel
1 (z. B. mssql)Datenbank auf sich selbst3mssql-to-mssql-account
226mssql-to-postgres-opportunity
3618mysql-to-oracle-opportunityproduct
4 (all)1236oracle-to-mssql-account

Alle Jobs haben diese Einstellungen:

FeldWert
Source / TargetTabellenname, z. B. Account
Mappingdev-<tabelle>, z. B. dev-account
WatermarkType / WatermarkFielddatetime / ModifiedOn
PriorityStandard
IsActivefalse

Mappings

Weil Tabellen- und Spaltennamen in allen Datenbanken gleich sind, gibt es nur ein Mapping je Tabelle, das alle Jobs gemeinsam nutzen. Jedes Feld wird auf das gleichnamige Feld abgebildet:

MappingTabelleFelderSchlüssel (TargetKey)
dev-accountAccount19 (alle Spalten außer ModifiedOn)AccountId (PrimaryKey)
dev-opportunityOpportunity10OpportunityId (PrimaryKey)
dev-opportunityproductOpportunityProduct10OpportunityProductId (PrimaryKey)

ModifiedOn wird nicht gemappt, weil es das Watermark-Feld ist und die Zieldatenbank es selbst setzt.

HTTP-Mocks (WireMock)

Der Container wiremock spielt bis zu drei HTTP-Systeme gleichzeitig. Die DataBridge merkt keinen Unterschied, sie bekommt nur eine andere URL in der Connection. Die Regeln (Stubs) sind so gebaut, dass der jeweilige Connector vollständig durchläuft: Verbindungstest, Metadaten, Lesen mit Paging, Upsert (Lookup → PATCH oder POST) und Delete.

ConnectionTypTabelleAuthMetadaten
bc-devD365BCv2.0/customersOAuth2 Client Credentials gegen /oauth/token (Mock)EDMX wiremock/__files/bc/metadata.xml
nav-devD365NAVCustomersBasic (dev/dev)EDMX wiremock/__files/nav/metadata.xml
openapi-devOPENAPIcustomersAPI-Key-Header X-API-Key: dev-api-keySpec wiremock/__files/openapi/spec.json

Wie ein Container drei Systeme spielt

WireMock ist ein einzelner HTTP-Server mit einer Liste von Regeln (Stubs). Zu jeder Anfrage sucht er die passende Regel und schickt deren Antwort. Welches System gespielt wird, entscheidet allein das Pfad-Präfix der URL aus der Connection:

PräfixSystemConnection-URL (aus Sicht der DataBridge)
/bc/...Business Centralhttp://wiremock:8080/bc/v2.0/sandbox
/nav/...NAVhttp://wiremock:8080/nav/ODataV4/Company('CRONUS')/
/openapi/...OpenAPIhttp://wiremock:8080/openapi
/oauth/tokenToken-Endpoint (gemeinsam)tokenUrl von bc-dev

Innerhalb des Compose-Netzes heißt der Container wiremock (Port 8080). Vom eigenen Rechner aus erreicht man ihn unter localhost:8089.

Nur die gewählten Mocks werden geladen. Die Regeln liegen je System in einem eigenen Ordner:

dev/wiremock/
├── mappings/            Regeln
│   ├── common/          immer geladen (OAuth-Token)
│   ├── bc/  nav/  openapi/
└── __files/             Antwortdateien, auf die Regeln per bodyFileName verweisen
    ├── bc/metadata.xml  nav/metadata.xml  openapi/spec.json

Der Ordner ist schreibgeschützt nach /mocks gemountet. Beim Start kopiert der Container common und die Ordner der Mocks aus DATABRIDGE_MOCKS in sein Arbeitsverzeichnis und startet erst dann WireMock. Ein nicht gewählter Mock antwortet deshalb mit 404. Der Seed legt Connections, Jobs und Mappings nur für die gewählten Mocks an und entfernt die der anderen. Ändert sich die Auswahl, baut up den Container neu auf.

Aufbau einer Regel: request beschreibt, worauf sie passt, response, was sie antwortet.

{
  "name": "BC customer by key - existing record (update path)",
  "priority": 1,
  "request":  { "method": "GET", "urlPath": "/bc/.../customers(a0000000-0000-0000-0000-000000000001)" },
  "response": { "status": 200, "jsonBody": { "id": "a0000000-…", "number": "ACC-1000" } }
}
  • Treffer: über urlPath (exakt), urlPathPattern (Regex), queryParameters (z. B. $skiptoken fehlt → Seite 1) und headers (z. B. If-Match: *, X-API-Key).
  • Priorität: Passen mehrere Regeln, gewinnt die kleinste priority. So liefert der bekannte Kunde einen Datensatz, alle anderen Schlüssel fallen auf die allgemeine 404-Regel zurück. Daraus ergibt sich „vorhanden → PATCH, unbekannt → POST“, ohne dass WireMock sich etwas merkt.
  • Templates ("transformers": ["response-template"]): {{{request.body}}} gibt den Body zurück (POST, PATCH). {{request.baseUrl}} baut den absoluten Link auf die nächste Seite, passend zur aufgerufenen Adresse (wiremock:8080 oder localhost:8089).

Datenbestand, Jobs und Mappings

Jeder Mock bildet alle drei Beispieltabellen ab — Account (Kunden, acht Felder: Guid, String, Decimal, Int32, Boolean, Date, Zeitstempel), Opportunity und OpportunityProduct (jeweils eigene Entität, z. B. bei BC opportunities/opportunityLines). Paging ist bewusst aktiv (maxPageSize: 2, zwei Seiten mit drei Datensätzen): BC/NAV über @odata.nextLink, OpenAPI über meta.next_page.

Fester Datenbestand für Upsert-Tests: Zu jeder Tabelle existiert ein bekannter Datensatz mit fester ID (z. B. AccountId a0000000-…-0001 bzw. AccountNumber ACC-1000 für Account). Ein Lookup liefert ihn, danach folgt ein PATCH. Jeder andere Schlüssel liefert 404, danach folgt ein POST. POST und PATCH geben den Request-Body unverändert zurück. Das reicht für Response-Jobs.

Ausnahme OpportunityProduct: OpportunityProductId ist in allen Datenbanken eine Ganzzahl (Identity), BC (id), NAV (SystemId) und OpenAPI (id) erwarten dort aber eine Guid. Das Feld wird deshalb auf ein zusätzliches, nur im Mock vorhandenes Textfeld gemappt (BC externalLineNo, NAV External_Line_No, OpenAPI external_line_no). Die Jobs nach BC und OpenAPI haben keinen Ziel-Schlüssel und legen jede Position per POST neu an, der NAV-Job sucht weiterhin über Line_No.

Jobs und Mappings: Je gewählter Datenbank, Mock und Tabelle (sofern der Mock sie kennt) entstehen zwei inaktive Jobs:

JobMappingRichtung
<db>-to-<mock>-<tabelle>dev-<tabelle>-to-<mock>SQL-Tabelle → Mock-Entität (Schreibpfad des Connectors)
<mock>-to-<db>-<tabelle>dev-<mock>-to-<tabelle>Mock-Entität → SQL-Tabelle (Lesepfad, mit Watermark-Filter)

Für Account lauten die Jobnamen unverändert <db>-to-<mock>-account (Kompatibilität zu bestehenden Notizen/Screenshots). Die Feldzuordnung je Tabelle steht in dev/seed/mocks.json unter entities.<Account|Opportunity|OpportunityProduct>.fields, z. B. für Account.Name → displayName (BC), Name (NAV), name (OpenAPI).

Was hat die DataBridge wirklich geschickt? Das Anfragen-Protokoll von WireMock zeigt jeden Request samt Body:

(Invoke-RestMethod localhost:8089/__admin/requests).requests |
  Select-Object @{n='m';e={$_.request.method}}, @{n='url';e={$_.request.url}}, @{n='body';e={$_.request.body}}, @{n='status';e={$_.response.status}}
Invoke-RestMethod localhost:8089/__admin/requests/unmatched   # Requests, für die es keinen Stub gibt
Invoke-RestMethod -Method Delete localhost:8089/__admin/requests   # Protokoll leeren
curl -s localhost:8089/__admin/requests | jq '.requests[] | {m: .request.method, url: .request.url, body: .request.body, status: .response.status}'
curl -s localhost:8089/__admin/requests/unmatched     # Requests, für die es keinen Stub gibt
curl -s -X DELETE localhost:8089/__admin/requests     # Protokoll leeren

Damit lässt sich ein Mapping inhaltlich prüfen: Man vergleicht den PATCH- oder POST-Body mit den Beispieldaten aus 02_data.sql.

Eigene Stubs: JSON-Dateien unter dev/wiremock/mappings/<system>/ ablegen und WireMock neu starten (podman compose -f dev/compose.dev.yml restart wiremock). Beim Neustart werden die Dateien neu kopiert. Worauf die Connectoren bestehen, steht als Kommentar im name der Stubs. Die wichtigsten Punkte:

  • BC/NAV: POST muss genau 201 liefern, PATCH genau 200 (nicht 204).
  • BC/NAV: Jede Antwort braucht einen JSON-Body, auch ein 404.
  • nextLink muss absolut sein.
  • Jeder gelieferte Datensatz braucht das Watermark-Feld als Zeitstempel.

Typische Abläufe

Nach einer Code-Änderung testen: erneut dev/up.sh (bzw. mit denselben Datenbanken wie zuvor). Das Image wird neu gebaut, die DataBridge neu gestartet.

Seed-Vorlagen geändert (dev/seed/*.json): erneut dev/up.sh. Der Seed läuft bei jedem Start und gleicht die Tabellen ab.

Schema-Skripte geändert (dev/<db>/init/01_schema.sql): dev/down.sh -v, dann dev/up.sh.

Mock-Regeln geändert (dev/wiremock/**): podman compose -f dev/compose.dev.yml restart wiremock. Ein neues up ist nicht nötig, weil die DataBridge die Mocks erst bei Bedarf anfragt. Ausnahme: Metadaten und die OpenAPI-Spec cacht sie. Nach einer Änderung an __files deshalb auch databridge neu starten.

Einen Connector gegen den Mock prüfen:

dev\up.ps1 mssql bc
Invoke-RestMethod -Method Delete localhost:8089/__admin/requests   # Protokoll leeren
# ... Aktion auslösen, z. B. per MCP get_connector_table_schema auf bc-dev ...
(Invoke-RestMethod localhost:8089/__admin/requests).requests |
  Select-Object @{n='m';e={$_.request.method}}, @{n='url';e={$_.request.url}}, @{n='status';e={$_.response.status}}
Invoke-RestMethod localhost:8089/__admin/requests/unmatched   # Anfragen ohne passende Regel
dev/up.sh mssql bc
curl -s -X DELETE localhost:8089/__admin/requests          # Protokoll leeren
# ... Aktion auslösen, z. B. per MCP get_connector_table_schema auf bc-dev ...
curl -s localhost:8089/__admin/requests | jq '.requests[] | {m: .request.method, url: .request.url, status: .response.status}'
curl -s localhost:8089/__admin/requests/unmatched         # Anfragen ohne passende Regel

Logging

Die Testumgebung setzt kein APPLICATIONINSIGHTS_CONNECTION_STRING — es gibt also keine Application Insights, an die etwas gesendet werden könnte. Das gepufferte Sampling aus Betrieb → Logging (Flush-Regeln, TELEMETRY_INFO_SAMPLE_RATE, TELEMETRY_CONTROL) betrifft ausschließlich diesen Versand und greift hier nicht: alles, was die DataBridge loggt, landet unverändert und sofort im Container-Log (stdout/stderr).

src/DataBridge/host.json setzt für die Kategorien DataBridge und Function den Log-Level Trace — lokal ist also die ausführlichste Stufe aktiv, inklusive der Einträge, die produktiv nie über die Konsole hinauskommen (siehe „Trace-Level erreicht Application Insights nie“).

podman compose -f dev/compose.dev.yml logs -f databridge          # laufend mitlesen
podman compose -f dev/compose.dev.yml logs seed mssql-init        # abgeschlossene Init-Container
podman compose -f dev/compose.dev.yml logs -f databridge | Select-String "mssql-to-postgres-account"
podman compose -f dev/compose.dev.yml logs -f databridge          # laufend mitlesen
podman compose -f dev/compose.dev.yml logs seed mssql-init        # abgeschlossene Init-Container
podman compose -f dev/compose.dev.yml logs -f databridge | grep "mssql-to-postgres-account"

Ohne -f gibt der Befehl das bisherige Log aus und beendet sich; mit -f bleibt er offen und zeigt neue Zeilen laufend an (Strg+C zum Beenden). Weil jede Funktionsausführung ihren Job- bzw. Verbindungsnamen loggt, lässt sich damit gezielt nach einem Job filtern, ohne operation_Id oder Application Insights zu brauchen.

Beim Debuggen im Host-Modus läuft die DataBridge nicht im Container — dort erscheinen dieselben Log-Zeilen stattdessen direkt in der IDE (Rider-Run-Fenster, Visual-Studio-Ausgabefenster bzw. das Terminal von func start), siehe Debuggen → Gegen Podman.

Service Bus

Profil servicebus startet einen Azure Service Bus Emulator mit eigenem SQL Server als Speicher (servicebus-db, nicht die Dev-Datenbank mssql). Damit laufen Jobs erstmals lokal von Anfang bis Ende: GetChanges erkennt Änderungen, legt sie auf die passende Queue, Process*QueueItems verarbeitet sie.

ContainerPortsZweck
servicebus5672 (AMQP), 5300 (Management/Health)Der Emulator selbst
servicebus-dbinternSQL Server als Speicher des Emulators
servicebus-explorerSERVICEBUS_EXPLORER_PORT (Standard 8090)Grafische Web-Oberfläche, siehe Service Bus einsehen

Die sieben Queues (sbqexport, sbqhighexport, sbqlowexport, sbqignored, sbqtransitivehigh, sbqtransitivestandart, sbqtransitivelow) sind in dev/servicebus/config.json mit denselben Eigenschaften wie in Azure vorkonfiguriert (LockDuration PT30S, DeadLetteringOnMessageExpiration true, MaxDeliveryCount 3 bzw. 6 für die transitiven Queues) — mit einer Ausnahme: DefaultMessageTimeToLive steht auf PT1H (1 Stunde) statt der in Azure üblichen P14D, weil der Emulator maximal 1 Stunde unterstützt und mit einem längeren Wert gar nicht erst startet.

Aktivieren: dev/up.sh servicebus (nur der Emulator) oder automatisch über getchanges/process (siehe Verarbeitung schalten). Die DataBridge bekommt dann den Connection-String Endpoint=sb://servicebus;...;UseDevelopmentEmulator=true; statt des Dummy-Strings. Mit dem Profil läuft automatisch auch servicebus-explorer mit — eine grafische Web-Oberfläche für die Queues, siehe Service Bus einsehen.

Grenzen des Emulators: kein AMQP over WebSockets, keine Entra-ID-Authentifizierung. Beides nutzt die DataBridge lokal nicht.

Verarbeitung schalten

Standardmäßig sind GetChanges (Timer) und die drei Process*QueueItems-Funktionen deaktiviert — die Umgebung startet, ohne dass etwas verarbeitet wird. Zwei unabhängige Schalter:

Variable (dev/.env)ArgumentWirkung
DATABRIDGE_GETCHANGESgetchangesAzureWebJobs.GetChanges.Disabled
DATABRIDGE_PROCESSprocessAzureWebJobs.Process{Standard,High,Low}PriorityQueueItems.Disabled

Werte an/off. Beide aktivieren automatisch das Profil servicebus (siehe oben). Die Durable-Functions-Kette (StartGetChangesOrchestration, GetChangesOrchestrator, GetChangesActivity, JobState) bleibt in der Testumgebung immer deaktiviert.

Einen Job manuell ausführen (ExecuteJob)

Der schnellste Weg, einen Job Ende-zu-Ende zu testen, ohne auf den Timer zu warten: die HTTP-Function ExecuteJob löst für einen Job genau das aus, was GetChanges sonst für alle aktiven Jobs im Timer-Takt tut — Änderungen erkennen und auf die passende Service-Bus-Queue legen. Ist process an, greift Process*QueueItems (läuft dauerhaft im Hintergrund) die Nachricht Sekunden später automatisch ab und schreibt ins Ziel.

  1. dev/up.sh mssql process (Windows: dev\up.ps1 mssql process) — Datenbank, Verarbeitung und Service-Bus-Emulator starten. getchanges wird dafür nicht gebraucht.
  2. Job-Namen ermitteln — Muster <quelle>-to-<ziel>-<tabelle> (siehe Jobs), z. B. mssql-to-mssql-account bei nur einer Datenbank, oder per MCP list_jobs nachsehen.
  3. Auslösen:
Invoke-RestMethod -Method Post 'http://localhost:7062/api/ExecuteJob?code=local' `
  -ContentType 'application/json' -Body '{"JobName":"mssql-to-mssql-account"}'
curl -s -X POST 'http://localhost:7062/api/ExecuteJob?code=local' \
  -H 'Content-Type: application/json' \
  -d '{"JobName":"mssql-to-mssql-account"}'

Nachschauen, ob’s geklappt hat:

  • Log (Logging) zeigt beide Schritte durch — GetChanges erkennt X Änderungen, ProcessStandardQueueItems verarbeitet die Nachricht.
  • Bei einem Mock-Job zusätzlich das WireMock-Protokoll (siehe „Was hat die DataBridge wirklich geschickt?“), ob der PATCH/POST ankam.
  • Per MCP list_queue_statuses/get_queue_status, ob die Nachricht auf der Queue schon verarbeitet ist (Anzahl zurück auf 0).

Debuggen

Für Breakpoints und Step-Through läuft die DataBridge lokal als Prozess statt im Container — sowohl gegen diesen Podman-Stack (dev/up.sh --host) als auch gegen eine echte Azure-Instanz. Eigener Artikel: Debuggen.

Fehlersuche

ProblemUrsache und Lösung
up bricht mit Port-Konflikt abEin lokaler Datenbankserver oder Azurite belegt den Port. Port in dev/.env ändern oder den anderen Dienst stoppen.
up meldet „Timeout“Das Skript zeigt die letzten Logzeilen aller Container. Meist ist ein Init-Container an einem SQL-Fehler gescheitert.
Init-Container kann /init nicht lesen (Fedora/RHEL)SELinux. Die Mounts in compose.dev.yml tragen dafür das Flag :z — bei eigenen Mounts ergänzen.
Skripte scheitern unter Windows mit merkwürdigen FehlernZeilenenden. .gitattributes erzwingt LF für dev/**/*.sh, *.sql und *.py; bei einem vorhandenen Checkout die Dateien neu auschecken.
PowerShell zeigt bei podman compose rot „Executing external compose provider …“ (NativeCommandError)Nur ein Hinweis von Podman auf stderr, kein echter Fehler. Windows PowerShell 5.1 zeigt ihn trotzdem als Fehler an. up.ps1/down.ps1 schalten ihn selbst ab und kapseln Aufrufe, bei denen podman compose seinen Fortschritt auf stderr schreibt (z. B. „Restarting“). Bei direkten Aufrufen vorher $env:PODMAN_COMPOSE_WARNING_LOGS = 'false' setzen oder dauerhaft in %APPDATA%\containers\containers.conf unter [engine] compose_warning_logs = false eintragen.
Windows: podman machine start meldet win-sshproxy.exe failed to start, danach scheitert podman compose mit „failed to connect to the docker API at npipe:////./pipe/podman-machine-default“Ohne win-sshproxy gibt es keine Docker-API-Pipe, über die docker-compose spricht. Ursache ist eine liegengebliebene Socket-Datei %TEMP%\podman\podman-machine-default-api.sock (im Windows-Ereignisprotokoll, Quelle „.NET Runtime“: bind: Only one usage of each socket address). Sie bleibt bei jedem podman machine stop liegen und lässt sich als Unix-Socket weder per Remove-Item noch per del löschen. up.ps1 und down.ps1 benennen den Ordner deshalb vor dem VM-Start in %TEMP%\podman.stale-<Zeitstempel> um; Podman legt ihn neu an. Von Hand: VM stoppen, Ordner umbenennen, VM starten. Die podman.stale-*-Ordner können jederzeit gelöscht werden.
Windows PowerShell 5.1 zeigt Umlaute verstümmelt („für“)PowerShell 5.1 liest Skripte ohne BOM als ANSI. up.ps1 und down.ps1 sind deshalb als UTF-8 mit BOM gespeichert — beim Bearbeiten so lassen. PowerShell 7 ist davon nicht betroffen.
401 beim Aufruf der APIDer Function Key fehlt. Er ist fest auf local gesetzt (?code=local).
Oracle braucht langeDer erste Start legt die Datenbank an. Der Init-Container wartet bis zu 2,5 Minuten auf den App-User.
Geänderte Daten sind nach up wegGewollt: Die Testdaten werden bei jedem Start zurückgesetzt.
WireMock startet nicht, Port-Konflikt auf 8089WIREMOCK_PORT in dev/.env ändern. 9090 ist unter Fedora oft von Cockpit belegt.
Mock antwortet mit 404 auf allesDer Mock ist nicht gewählt (dev/up.sh … bc), oder die Anfrage passt auf keine Regel. /__admin/requests/unmatched zeigt, welche Anfragen keinen Treffer hatten, mit einem Vergleich zur nächstliegenden Regel.
CheckList meldet für openapi-dev „could not find table customers“Bekannter Fehler im OpenAPI-Connector: GetTableNames liefert Spec-Pfade (/v1/customers) statt Ressourcennamen. Das liegt nicht am Mock.
up hängt bei „Warte auf Service-Bus-Emulator“Der Emulator startet erst, wenn servicebus-db (SQL Server als Speicher) healthy ist — das dauert beim ersten Start etwas. Bei echtem Timeout: podman compose -f dev/compose.dev.yml logs servicebus servicebus-db prüfen, häufig ein Fehler in dev/servicebus/config.json.
MCP-Tools list_queue_statuses/get_queue_status liefern einen VerbindungsfehlerProfil servicebus läuft nicht (kein servicebus/getchanges/process beim letzten up-Aufruf angegeben), ExportQueueServiceBus zeigt dann auf den Dummy-Connection-String.

4 - Debuggen

Die DataBridge lokal mit Breakpoints debuggen — entweder gegen die Podman-Testumgebung oder gegen eine echte Azure-Instanz.

Für Breakpoints und Step-Through läuft die DataBridge nicht im Container, sondern lokal als Prozess in der IDE. Zwei grundsätzlich verschiedene Varianten, je nachdem wogegen die lokal laufende DataBridge sprechen soll:

Gegen Podman debuggen

Azurite, Podman-Datenbanken, Service-Bus-Emulator, WireMock — alles lokal, keine Azure-Kosten, jederzeit zurücksetzbar.

→ Zum Artikel

Gegen echtes Azure debuggen

Echter Storage Account, echter Service Bus, echte Quell-/Zielsysteme — für Probleme aus einer konkreten Umgebung.

→ Zum Artikel

Gemeinsamkeiten

Beide Varianten teilen sich denselben Code und dieselbe launchSettings.json (Rider/Visual Studio/VS Code starten die DataBridge über dasselbe Projekt src/DataBridge/DataBridge.csproj auf Port 7062). Der Unterschied steckt ausschließlich in src/DataBridge/local.settings.json — welche Connection-Strings dort stehen, entscheidet, wogegen die DataBridge beim Start läuft. Die Datei ist nicht eingecheckt (.gitignore) und wird in beiden Varianten unterschiedlich befüllt:

  • Podman: generiert durch dev/up.sh --host aus der Vorlage dev/local.settings.dev.json.
  • Cloud: von Hand aus src/DataBridge/sample.local.settings.json kopiert und mit echten Werten gefüllt.

Beide Varianten steuern außerdem, ob GetChanges (Timer) und die Process*QueueItems-Funktionen laufen, über dieselben App-Settings (AzureWebJobs.<FunctionName>.Disabled) — nur der Weg dorthin unterscheidet sich (Toggle beim Start vs. Wert von Hand in local.settings.json). Details dazu in den beiden Artikeln.

4.1 - Gegen Podman debuggen

Die DataBridge lokal mit Breakpoints gegen die Podman-Testumgebung debuggen — konkret in Rider, Visual Studio und VS Code, inklusive Steuerung von GetChanges/Process*QueueItems und Einblick in den Service-Bus-Emulator.

Voraussetzung ist die Podman-Testumgebung. Der databridge-Container würde sonst Port 7062 belegen, deshalb startet up.sh/up.ps1 alles außer der DataBridge selbst:

dev\up.ps1 -HostMode mssql process
dev/up.sh --host mssql process

--host (bzw. -HostMode) lässt sich mit allen anderen Argumenten kombinieren (Datenbanken, Mocks, getchanges, process, servicebus) und startet alles außer dem databridge-Container selbst (dafür gibt es das Profil databridge, das sonst automatisch aktiv ist). Der Befehl generiert zusätzlich src/DataBridge/local.settings.json aus der Vorlage dev/local.settings.dev.json (Azurite über UseDevelopmentStorage=true, Service-Bus-Emulator über localhost statt über den Compose-Servicenamen servicebus, sowie die Toggles aus dev/.env, siehe GetChanges/Process*QueueItems steuern).

Damit die Connections in Azurite ebenfalls auf den Host statt auf die Compose-Servicenamen zeigen, setzt --host/-HostMode beim Seed-Container DATABRIDGE_RUNTIME=host (sonst container); seed.py ersetzt dann mssql/postgres/mysql/oracle/wiremock durch localhost mit dem passenden Host-Port aus dev/.env.

Zum Zurückwechseln auf den Container-Betrieb reicht ein Aufruf ohne --host/-HostMode — das generiert local.settings.json nicht neu, startet aber wieder den databridge-Container auf demselben Port. Die generierte Datei vorher löschen oder ignorieren, sie wird nicht mehr gelesen, solange kein lokaler Prozess läuft.

In der IDE starten

Alle drei IDEs nutzen dasselbe Projekt src/DataBridge/DataBridge.csproj und dasselbe Profil DataBridge aus src/DataBridge/Properties/launchSettings.json (startet mit --port 7062, passend zum MCP-Endpoint und zu curl-Aufrufen gegen localhost:7062).

Rider

  1. Lösung src/DataBridge.slnx öffnen (falls noch nicht offen).
  2. Oben rechts das Run-Profil DataBridge auswählen (Rider erkennt das Azure-Functions-Projekt automatisch und bietet das Profil aus launchSettings.json an).
  3. Auf das Käfer-Symbol (Debug) statt Play klicken, oder Shift+F9.
  4. Breakpoints wie gewohnt im Editor setzen (z. B. in ProcessQueueMessageService oder einem Connector).

Visual Studio

  1. src/DataBridge.slnx öffnen, DataBridge als Startprojekt setzen (Rechtsklick im Projektmappen-Explorer → Als Startprojekt festlegen, falls nicht schon aktiv).
  2. Im Dropdown neben dem grünen Play-Pfeil das Profil DataBridge wählen.
  3. F5 (Debuggen starten) statt Ctrl+F5.
  4. Breakpoints wie gewohnt setzen.

VS Code

VS Code braucht die Azure Functions Core Tools (func) und die Erweiterung Azure Functions (oder C# Dev Kit), um Attach/Debug über launchSettings.json anzubieten. Ohne Erweiterung reicht auch der direkte Weg:

  1. Terminal in src/DataBridge öffnen.
  2. func start (nutzt automatisch local.settings.json und den Port aus host.json/der Umgebung — bei Bedarf func start --port 7062 explizit angeben).
  3. Mit der Erweiterung C# Dev Kit: Debug: Attach to .NET Process ausführen und den laufenden func-/dotnet-Prozess auswählen. Mit der Azure Functions-Erweiterung genügt stattdessen F5 direkt im Projektordner, das startet func start automatisch im Debug-Modus.
  4. Breakpoints wie gewohnt setzen.

Danach in jeder IDE:

  • Der MCP-Server databridge-dev aus .mcp.json funktioniert unverändert (http://localhost:7062/api/mcp?code=local) — func start prüft den Function Key nicht, siehe MCP für die Testumgebung.
  • Eine Anfrage auslösen (z. B. POST http://localhost:7062/api/ExecuteJob?code=local, per Bruno, curl oder MCP run_job) und der Breakpoint hält an.

Logging

dev/local.settings.dev.json setzt kein APPLICATIONINSIGHTS_CONNECTION_STRING — es gibt also auch im Host-Modus keine Application Insights, an die etwas gesendet werden könnte. Alles, was die DataBridge loggt, geht direkt in die Konsole des lokalen Prozesses:

  • Rider: das Run-Fenster (bzw. Debug-Fenster mit Breakpoints) am unteren Rand.
  • Visual Studio: das Ausgabefenster (Ansicht → Ausgabe, Quelle „Debug“) sowie bei F5 das Konsolenfenster der Function-Host-Runtime.
  • VS Code: das Terminal, in dem func start läuft, bzw. der Debug Console-Tab bei Attach.

src/DataBridge/host.json setzt für die Kategorien DataBridge und Function den Log-Level Trace — dieselbe ausführlichste Stufe wie im Container (siehe Lokale Testumgebung → Logging). Weil hier keine Application Insights mitläuft, spielen die dortigen Sampling-/Puffer-Regeln (TELEMETRY_INFO_SAMPLE_RATE, TELEMETRY_CONTROL) keine Rolle — sie wirken nur auf den Versand an Application Insights, nicht auf die Konsolenausgabe.

GetChanges/Process*QueueItems steuern

Zwei unabhängige Schalter entscheiden, ob im Host-Modus etwas automatisch verarbeitet wird:

SchalterArgument bei up.sh --host/up.ps1 -HostModeWirkung
DATABRIDGE_GETCHANGESgetchangesAzureWebJobs.GetChanges.Disabled in der generierten local.settings.json
DATABRIDGE_PROCESSprocessAzureWebJobs.Process{Standard,High,Low}PriorityQueueItems.Disabled
dev\up.ps1 -HostMode mssql process             # Verarbeitung an, Timer aus
dev\up.ps1 -HostMode mssql getchanges process  # beides an
dev\up.ps1 -HostMode mssql                     # beides aus – Verarbeitung nur manuell per ExecuteJob
dev/up.sh --host mssql process              # Verarbeitung an, Timer aus
dev/up.sh --host mssql getchanges process   # beides an
dev/up.sh --host mssql                      # beides aus – Verarbeitung nur manuell per ExecuteJob

Beide Toggles aktivieren automatisch das Profil servicebus (siehe Service Bus), weil ohne Emulator weder GetChanges noch Process*QueueItems sinnvoll etwas zu tun hätten. Die Durable-Functions-Kette (StartGetChangesOrchestration, GetChangesOrchestrator, GetChangesActivity, JobState) bleibt immer deaktiviert.

Weil local.settings.json bei jedem --host-Aufruf neu generiert wird, reicht ein erneutes up.sh --host … mit anderen Toggles, um umzuschalten — die IDE muss dafür neu gestartet werden (Azure-Functions-Runtime liest local.settings.json nur beim Start).

Typischer Ablauf, um einen Job Ende-zu-Ende mit Breakpoints zu verfolgen:

  1. dev/up.sh --host mssql process (Windows: dev\up.ps1 -HostMode mssql process) — Datenbank, Verarbeitung und Service-Bus-Emulator, aber ohne Container-DataBridge.
  2. In der IDE debuggen starten (s. o.), Breakpoint z. B. in ProcessQueueMessageService setzen.
  3. Job manuell per ExecuteJob auslösen — Details, Job-Namen-Muster und Beispielaufruf unter Lokale Testumgebung → Einen Job manuell ausführen. IsActive: true ist dafür nicht nötig.
  4. getchanges zusätzlich nur nötig, wenn der Timer selbst (statt ExecuteJob) die Änderungen erkennen soll — dann muss der Job vorher per update_job auf IsActive: true gesetzt werden.

Service Bus einsehen

Drei Wege, in den Service-Bus-Emulator hineinzuschauen, ohne selbst Code zu schreiben:

Web-Oberfläche servicebus-explorer (empfohlen, auch zum aktiven Testen). Läuft automatisch mit, sobald das Profil servicebus aktiv ist (also auch über getchanges/process) — kein separater Start nötig. Zeigt Queues, Nachrichtenzahlen (aktiv/dead-lettered) und einzelne Nachrichten inkl. Body in einer grafischen Oberfläche:

http://localhost:8090   (Port konfigurierbar über SERVICEBUS_EXPLORER_PORT in dev/.env)

Es handelt sich um das Community-Tool Service-Bus-Emulator-Explorer von Tom Biddulph (nicht von Microsoft, kein Bestandteil des offiziellen Emulators) — als eigener Container servicebus-explorer im Compose-Stack mitgeliefert, per fertigem Image (ghcr.io/tombiddulph/service-bus-emulator-explorer), spricht intern mit servicebus über dieselben Connection-Strings wie die DataBridge selbst.

Anders als die MCP-Diagnose-Tools kann die Web-Oberfläche auch selbst Nachrichten auf eine Queue oder ein Topic legen (eigener Body, Content-Type, Custom Properties, Session-ID) — praktisch, um Process*QueueItems gezielt mit einer handgebauten Nachricht zu testen, ohne vorher GetChanges über einen echten Job laufen zu lassen.

MCP (kein zusätzliches Werkzeug nötig, gut für Automatisierung/Scripting). Der MCP-Server databridge-dev bietet schreibgeschützte Diagnose-Tools: list_queue_statuses/get_queue_status (Nachrichtenzahlen je Queue) und peek_* für die Metadaten wartender Nachrichten (u. a. Body-Größe, keine Inhalte — siehe MCP für die Testumgebung). Das läuft über den Admin-Client auf Port 5300, den ServiceBusService beim Emulator automatisch anhängt (siehe Service Bus).

Rohes REST/AMQP, z. B. per curl. Die Management-API des Emulators liegt auf http://localhost:5300; für eigene Skripte, die weder die Weboberfläche noch MCP nutzen sollen, reicht derselbe Connection-String wie oben.

4.2 - Gegen echtes Azure debuggen

Die DataBridge lokal mit Breakpoints gegen eine echte Azure-Instanz debuggen — konkret in Rider, Visual Studio und VS Code, inklusive Steuerung von GetChanges/Process*QueueItems.

Hier läuft kein Podman-Stack mit — die lokal gestartete DataBridge verbindet sich direkt mit einem echten Storage Account, einem echten Service Bus Namespace und den echten Quell-/Zielsystemen (BC, CE, NAV, SQL). Das ist der klassische Weg, um ein Problem aus einer konkreten Umgebung (z. B. dsyr-wysatest) lokal nachzuvollziehen oder mit dem Debugger zu untersuchen.

local.settings.json vorbereiten

  1. src/DataBridge/sample.local.settings.json nach src/DataBridge/local.settings.json kopieren (falls noch nicht vorhanden — die Datei ist in .gitignore, geht also nicht versehentlich in einen Commit).
  2. Die Platzhalter durch echte Werte aus dem Azure Portal bzw. Key Vault ersetzen: AzureWebJobsStorage (Storage-Account-Connection-String), ExportQueueServiceBus (Service-Bus-Connection-String) sowie die Connection-Strings/Secrets der Quell- und Zielsysteme, je nachdem was getestet werden soll. Diese Werte niemals committen und nicht in Chats/Tickets einfügen.
  3. Wenn enableManagedIdentity in der Zielumgebung aktiv ist (siehe Identity-Verbindungen bzw. SettingsService), fehlen dort Connection-Strings ganz — stattdessen AzureWebJobsStorage__accountName und ExportQueueServiceBus__fullyQualifiedNamespace setzen und lokal per az login angemeldet sein, damit DefaultAzureCredential greift. Das erfordert eine Rollenzuweisung für den eigenen Benutzer auf Storage und Service Bus (Data-Rollen, nicht nur Management), sonst schlägt der Verbindungstest beim Start fehl.

In der IDE starten

Identisch zum Podman-Host-Modus — alle drei IDEs nutzen dasselbe Projekt src/DataBridge/DataBridge.csproj und dasselbe Profil DataBridge aus src/DataBridge/Properties/launchSettings.json (startet mit --port 7062).

Rider

  1. Lösung src/DataBridge.slnx öffnen.
  2. Run-Profil DataBridge auswählen, dann auf das Käfer-Symbol (Debug) klicken bzw. Shift+F9.
  3. Breakpoints setzen, z. B. im Connector des Systems, das gerade untersucht wird.

Visual Studio

  1. src/DataBridge.slnx öffnen, DataBridge als Startprojekt setzen.
  2. Profil DataBridge im Dropdown neben dem Play-Pfeil wählen, dann F5.
  3. Breakpoints setzen.

VS Code

  1. Terminal in src/DataBridge öffnen, func start ausführen (liest local.settings.json automatisch).
  2. Mit C# Dev Kit: Debug: Attach to .NET Process auf den laufenden func-Prozess. Mit der Azure Functions-Erweiterung genügt F5 direkt im Projektordner.
  3. Breakpoints setzen.

Der MCP-Server databridge (nicht databridge-dev) aus .mcp.json zeigt per Voreinstellung auf http://localhost:7062 — passt also automatisch, solange kein DATABRIDGE_MCP_URL gesetzt ist, das auf eine andere Instanz zeigt.

Logging

Hier läuft Logging doppelt, weil local.settings.json einen echten APPLICATIONINSIGHTS_CONNECTION_STRING enthält (aus sample.local.settings.json übernommen und mit echten Werten gefüllt, siehe oben):

  1. Direkt in der IDE-Konsole — wie im Host-Modus gegen Podman: Rider-Run-Fenster, Visual-Studio-Ausgabefenster bzw. das func start-Terminal in VS Code. host.json setzt Trace für DataBridge/Function, also die volle Ausführlichkeit, unabhängig von Application Insights.
  2. In der echten Application Insights-Instanz — mit denselben Regeln wie in Produktion: gepuffertes Sampling, TELEMETRY_INFO_SAMPLE_RATE, TELEMETRY_CONTROL, siehe Betrieb → Logging. Trace-Einträge erreichen Application Insights dabei nie — nur die IDE-Konsole zeigt sie.

GetChanges/Process*QueueItems steuern

Anders als im Podman-Host-Modus gibt es hier keinen up.sh-Schalter — die App-Settings stehen direkt in local.settings.json und werden von Hand gesetzt:

{
  "Values": {
    "AzureWebJobs.GetChanges.Disabled": "true",
    "AzureWebJobs.ProcessStandardQueueItems.Disabled": "true",
    "AzureWebJobs.ProcessHighPriorityQueueItems.Disabled": "true",
    "AzureWebJobs.ProcessLowPriorityQueueItems.Disabled": "true"
  }
}

"true" deaktiviert die jeweilige Funktion, "false" bzw. Weglassen aktiviert sie. Vor dem Debuggen bewusst entscheiden:

  • Nur einen bestimmten Job untersuchen, sonst nichts verarbeiten: alle vier auf "true" lassen und stattdessen gezielt POST /api/ExecuteJob aufrufen (löst GetChanges für einen einzelnen Job aus, unabhängig vom Timer).
  • Den echten Timer-/Queue-Ablauf beobachten: die passenden Einträge auf "false" setzen. Das bedeutet aber, dass die lokal debuggte Instanz zusätzlich zu jeder produktiv laufenden Instanz derselben Umgebung Änderungen erkennt bzw. Queue-Nachrichten verarbeitet.

Podman- und Cloud-Settings parallel halten

src/DataBridge/local.settings.json ist für beide Debug-Varianten dieselbe Datei — wer öfter zwischen Podman-Host-Modus und Cloud-Debugging wechselt, verliert sonst bei jedem up.sh --host die mühsam eingetragenen echten Zugangsdaten (siehe Warnhinweis oben). Zwei Optionen:

Eigene Kopie + Copy-Befehl (empfohlen, geringer Aufwand). Einmalig eine zweite, selbst gepflegte Datei anlegen, die up.sh/up.ps1 nie anfassen — z. B. src/DataBridge/local.settings.cloud.json (Kopie von sample.local.settings.json mit echten Werten, siehe oben). Diese Namensform (local.settings.*.json) ist bereits über src/DataBridge/.gitignore ausgeschlossen, landet also nicht versehentlich in einem Commit. Vor dem Cloud-Debuggen einmal darüberkopieren:

Copy-Item src\DataBridge\local.settings.cloud.json src\DataBridge\local.settings.json -Force
cp src/DataBridge/local.settings.cloud.json src/DataBridge/local.settings.json

Der Rückweg ist kein manueller Schritt: Der nächste dev/up.sh --host/dev\up.ps1 -HostMode überschreibt local.settings.json ohnehin automatisch wieder aus der Podman-Vorlage (dev/local.settings.dev.json).

Getrennter Checkout/Git-Worktree. Für häufigen, unterbrechungsfreien Wechsel: einen zweiten Checkout des Repos ausschließlich fürs Cloud-Debuggen anlegen, mit eigener, dauerhaft gefüllter local.settings.json, komplett getrennt vom Ordner, in dem dev/up.sh --host läuft. Mehr Verwaltungsaufwand beim Einrichten, dafür kein Kopieren mehr im Alltag — einfach die passende IDE im jeweils anderen Ordner öffnen.