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.
DataBridge-Container, Azurite und wahlweise MSSQL, PostgreSQL, MySQL und Oracle — mit
Beispieldaten, Connections, Jobs und Mappings, gestartet mit einem Befehl.
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.
Die Inhalte entsprechen dem Code-Stand von Version 2.3. Quellen sind src/, bicep/
und cicd/ im Repository. Wo eine Aussage eine Annahme ist, steht das im Text.
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
Funktion
Beschreibung
Change Detection
Periodisches Pollen der Quellsysteme (Timer GetChanges, optional Durable-Orchestrierung)
Asynchrone Entkopplung
Erkannte Änderungen landen als Nachrichten in priorisierten Queues (Low / Standard / High)
Source-Operation-Handler werden per Reflection registriert; Connectoren über ConnectionTypes gemappt; Resolver-Registries je Connector als statische FrozenDictionary
Observability first
Gepuffertes Logging, Requests und Exceptions nie gesampelt, Custom Metrics ohne Sampling
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.
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
Stellschraube
Wirkung
appServiceSku
Consumption vs. Premium (kein Cold Start, mehr RAM/CPU)
functionScaleLimit
Maximale parallele Instanzen
maxConcurrentCalls (host.json)
Parallel verarbeitete Nachrichten je Instanz
Priorisierte Queues
Drei getrennte Trigger für unabhängige Lastpfade
maxPageSize in der Connection, MaxBatchSize in der Activity
Seitengröße beim Abruf
Order und Priority am Job
Reihenfolge und Priorität
Service Bus Basic
Die SKU Basic unterstützt keine Topics, Sessions, Partitionierung und keine Duplicate
Detection. Die Architektur nutzt ausschließlich Queues. Ein Wechsel auf Topics oder Sessions
würde ein SKU-Upgrade und Code-Änderungen erfordern.
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
Ressource
Typ
SKU / Eigenschaften
Modul
Storage Account st<name>
Microsoft.Storage/storageAccounts
StorageV2, Standard_LRS, TLS 1.2
storageaccount
Table Service mit vier Tabellen
…/tableServices/tables
connections, jobs, mappings, watermark
storageaccount
Application Insights appi<name>
Microsoft.Insights/components
Retention 90 Tage, klassisch (ohne eigenen Log-Analytics-Workspace)
applicationinsights
App Service Plan asp<name>
Microsoft.Web/serverfarms
Y1 (capacity 0) oder EP1–EP3, Windows
appserviceplan
Service Bus Namespace sb<name>
Microsoft.ServiceBus/namespaces
Basic
servicebus
Sieben Queues
…/namespaces/queues
siehe unten
servicebus
Authorization Rule sbexportFunction
…/authorizationRules
Send + Listen
servicebus
Function App func<name>
Microsoft.Web/sites
kind functionapp, FTPS only, netFrameworkVersion 10.0, System-Identity immer aktiv
azurefunction
Grafana-Dashboard
Microsoft.Dashboard/dashboards
eingebettete JSON-Definition
dashboard
Action Group + fünf Metric Alerts
Microsoft.Insights/actionGroups, metricAlerts
nur mit enableAlerts
metricalerts, dlqmetricalert
Rollenzuweisungen
Microsoft.Authorization/roleAssignments
nur mit enableManagedIdentity
roleassignments
Queues
Alle Queues haben deadLetteringOnMessageExpiration = true, TTL defaultMessageTimeToLive
(Default P14D) und lockDuration (Default PT30S).
Ablage bei Konflikten im Ziel (HTTP 409); kein Trigger
maxDeliveryCount × 2
sbqignored
Veraltete Nachrichten bei SkipOutdatedMessages; kein Trigger
maxDeliveryCount
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
Setting
Quelle
APPLICATIONINSIGHTS_CONNECTION_STRING
Output des AI-Moduls
AzureWebJobsStorage bzw. AzureWebJobsStorage__accountName
Key-basiert per listKeys() oder identitätsbasiert, je nach enableManagedIdentity
ExportQueueServiceBus bzw. ExportQueueServiceBus__fullyQualifiedNamespace
analog für den Service Bus
TARGET_BASED_SCALING_ENABLED
0, nur im schlüsselbasierten Modus (ohne enableManagedIdentity)
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:
Rolle
Ressource
Grund
Storage Table Data Contributor
Storage Account
Konfigurationstabellen, Durable-Tabellen
Storage Blob Data Owner
Storage Account
Durable-Hub-Blobs
Storage Queue Data Contributor
Storage Account
Durable-Hub-Queues
Azure Service Bus Data Sender
Namespace
Enqueue
Azure Service Bus Data Receiver
Namespace
Dequeue
Azure Service Bus Data Owner
Namespace
Queue-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.
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.
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.
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.
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.
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.
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.
Feld
Default
Beschreibung
BaseAddress
erforderlich
Basis-URL
Url
erforderlich
relativer Pfad inkl. Query
Method
GET
nur GET implementiert
Authentication
NTLM
NTLM mit UserName/Password, sonst ohne Credentials
ODataMaxVersion, ODataVersion
4.0
OData-Header
Prefer
odata.include-annotations="*"
Prefer-Header
MediaTypeWithQuality
application/json
Accept-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
Function
Bindung
Konfiguration
GetChanges
Timer
%GetChangesTimerExpression%, Bicep-Default */15 * * * * *; RunOnStartup nur im Debug-Build
SourceKey nutzt der DeadLetterMover zur Deduplizierung. OperationId ist die
W3C-Root-ID und erlaubt die Korrelation mit Application Insights.
Typisierung der Attribute
Alle Werte werden heute als String plus Typname transportiert. Eine Umstellung auf echte
JSON-Typen mit symmetrischen Resolvern ist entworfen, aber nicht umgesetzt. Sie wäre ein
Breaking Change für bereits gequeute Nachrichten.
Fehlerverhalten
Situation
Verhalten
ExecuteJob ohne JobName
Exception → 500
Initialisierungsfehler
Validate liefert 500 mit Log; Timer- und Queue-Trigger setzen die Initialisierung zurück und versuchen es beim nächsten Aufruf erneut
Verarbeitungsfehler in ProcessQueueItems
Abandon 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 SkipOutdatedMessages
Kopie nach sbqignored, Original abgeschlossen
Wiederholte Zustellung
ProcessQueueMessageService wartet DeliveryCount² · 1000 ms vor dem Versuch
EndpointNotFoundException, CommunicationException in GetChanges
Reset 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
Datenart
Ort
Lebensdauer
Konfiguration
Table Storage connections, jobs, mappings
persistent, manuell gepflegt
Watermarks (Watermark, LastCronRun)
Table Storage watermark
persistent je Job
Nachrichten
Service-Bus-Queues
transient, TTL P14D
Durable-State
Azure Storage DurableFunctionsHub
laufzeitgebunden
Telemetrie
Application Insights
90 Tage
Fachdaten
nur 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.
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.
Hugo/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:
Server
Ziel
URL
databridge
Eine Azure-Instanz (bzw. ohne Konfiguration der lokale Host)
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“.
Session neu starten
Claude Code verbindet sich nur beim Start einer Session mit den MCP-Servern. Läuft die
Testumgebung beim Start noch nicht, meldet databridge-dev einen Verbindungsfehler. Dann
dev/up.sh abwarten und die Session neu starten. Beim ersten Mal fragt Claude Code, ob der
Projekt-Server databridge-dev erlaubt werden soll.
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:
Tool
Beispiel
Ergebnis
list_jobs
—
Die generierten Jobs, z. B. oracle-to-mssql-account
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:
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:
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.
Nur für Entwicklung
Die Umgebung nutzt feste Passwörter und einen festen Function Key (local). Sie ist nicht für
den Betrieb gedacht und darf nicht aus dem Internet erreichbar sein.
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.
Container
Aufgabe
Läuft
mssql, postgres, mysql, oracle
Datenbankserver, nur die gewählten
dauerhaft
<db>-init
Legt je Datenbank Schema und Testdaten an
einmalig pro Start
azurite
Emulator für Azure Table/Blob/Queue Storage
dauerhaft
wiremock
HTTP-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
Schreibt connections, jobs und mappings nach Azurite
einmalig pro Start
databridge
Die 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):
Container
RAM
Läuft
databridge + azurite
~0,3 GB
immer
postgres
~0,1 GB
wenn gewählt
mysql
~0,5 GB
wenn gewählt
mssql
~1,5 GB
wenn gewählt
oracle
~2,5 GB
wenn gewählt
wiremock (alle Mocks in einem Container)
~0,2 GB
wenn ein Mock gewählt ist
servicebus + servicebus-db
~0,5 GB + ~1,5 GB
wenn 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).
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\.envdev\up.ps1 mssql oracle # nur die genannten Datenbanken, keine Mocksdev\up.ps1 mssql bc openapi # Datenbanken und Mocks gemischtdev\up.ps1 bc # nur Mocks – Datenbanken dann aus DATABRIDGE_DBS in dev\.envdev\up.ps1 all # alle vier Datenbanken und alle drei Mocksdev\up.ps1 mssql process# Datenbank + Verarbeitung an (Service-Bus-Emulator automatisch mit)dev\up.ps1 mssql getchanges # Datenbank + Timer-Trigger GetChanges andev\up.ps1 servicebus # nur den Emulator dazu, ohne Verarbeitungdev\up.ps1 -HostMode mssql process# Host-Modus: kein databridge-Container, Debugger auf Port 7062dev\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/.envdev/up.sh mssql oracle # nur die genannten Datenbanken, keine Mocksdev/up.sh mssql bc openapi # Datenbanken und Mocks gemischtdev/up.sh bc # nur Mocks – Datenbanken dann aus DATABRIDGE_DBS in dev/.envdev/up.sh all # alle vier Datenbanken und alle drei Mocksdev/up.sh mssql process # Datenbank + Verarbeitung an (Service-Bus-Emulator automatisch mit)dev/up.sh mssql getchanges # Datenbank + Timer-Trigger GetChanges andev/up.sh servicebus # nur den Emulator dazu, ohne Verarbeitungdev/up.sh --host mssql process # Host-Modus: kein databridge-Container, Debugger auf Port 7062dev/down.sh # alles stoppen, Daten bleiben erhaltendev/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
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.
Legt dev/.env aus dev/.env.example an, falls sie fehlt.
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.
Baut die DataBridge aus dem aktuellen Quellcode und startet alle gewählten Container.
Läuft der Service-Bus-Emulator mit, wartet up zusätzlich, bis dessen Management-API antwortet
(kein Docker-Healthcheck möglich, siehe Service Bus).
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.
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)
Variable
Standard
Bedeutung
DATABRIDGE_DBS
mssql
Datenbanken, wenn up ohne Argument aufgerufen wird (kommagetrennt)
DEV_DB_PASSWORD
DataBridge_Dev_2026
Ein Passwort für alle Datenbanken (SA, App-User, Root)
DATABRIDGE_MOCKS
leer
Mocks, wenn up ohne Argument aufgerufen wird (kommagetrennt: bc,nav,openapi). Leer heißt: kein WireMock, keine Mock-Connections und -Jobs
DATABRIDGE_GETCHANGES
off
an/off — Timer-Trigger GetChanges, wenn up ohne Argument aufgerufen wird (siehe Verarbeitung schalten)
DATABRIDGE_PROCESS
off
an/off — die drei Process*QueueItems-Funktionen, wenn up ohne Argument aufgerufen wird
DATABRIDGE_PORT
7062
Port der DataBridge
WIREMOCK_PORT
8089
Host-Port von WireMock (9090 ist unter Fedora oft von Cockpit belegt)
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
Argument
Connection
Image
Host-Port
Benutzer
Datenbank / Schema
mssql
mssql-dev (MSSQL)
mssql/server:2022-latest
1433
sa
DataBridgeTest, Schema dbo
postgres
postgres-dev (PGSQL)
postgres:17
5432
databridge
databridgetest, Schema public
mysql
mysql-dev (MYSQL)
mysql:8.4
3306
databridge
databridgetest
oracle
oracle-dev (ORACLE)
gvenzl/oracle-free:23-slim-faststart
1521
databridge
Service 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.
Die Spalten decken bewusst möglichst viele Datentypen ab. Jede Datenbank verwendet dafür ihren
nativen Typ:
Account — Firmen (3 Datensätze)
Spalte
MSSQL
PostgreSQL
MySQL
Oracle
AccountId (PK)
uniqueidentifier
uuid
char(36)
varchar2(36)
AccountNumber (eindeutig)
varchar(20)
varchar(20)
varchar(20)
varchar2(20)
Name
nvarchar(160)
varchar(160)
varchar(160)
nvarchar2(160)
Description
nvarchar(max)
text
text
nclob
CountryCode
char(2)
char(2)
char(2)
char(2)
NumberOfEmployees
int
integer
int
number(10)
IndustryCode
smallint
smallint
smallint
number(5)
Rating
tinyint
smallint
tinyint
number(3)
Revenue
money
numeric(19,4)
decimal(19,4)
number(19,4)
CreditLimit
decimal(18,2)
numeric(18,2)
decimal(18,2)
number(18,2)
Latitude
float
double precision
double
binary_double
Longitude
real
real
float
binary_float
IsActive
bit
boolean
boolean
boolean
FoundedOn
date
date
date
date
OpeningTime
time(0)
time(0)
time
interval day to second
LastContactAt
datetime2(3)
timestamp(3)
datetime(3)
timestamp(3)
CreatedOnUtc
datetimeoffset(3)
timestamptz(3)
timestamp(3)
timestamp(3) with time zone
Logo
varbinary(max)
bytea
blob
blob
Attributes (JSON)
nvarchar(max)
jsonb
json
json
ModifiedOn (Watermark)
datetime2(3)
timestamp(3)
datetime(3)
timestamp(3)
Opportunity — Verkaufschancen (4 Datensätze)
Spalte
MSSQL
PostgreSQL
MySQL
Oracle
OpportunityId (PK)
uniqueidentifier
uuid
char(36)
varchar2(36)
AccountId (FK)
uniqueidentifier
uuid
char(36)
varchar2(36)
Name
nvarchar(200)
varchar(200)
varchar(200)
nvarchar2(200)
EstimatedValue
decimal(19,4)
numeric(19,4)
decimal(19,4)
number(19,4)
Probability
tinyint
smallint
tinyint
number(3)
EstimatedCloseDate
date
date
date
date
StatusCode
int
integer
int
number(10)
IsWon
bit
boolean
boolean
boolean
ActualCloseAt
datetime2(0)
timestamp(0)
datetime
timestamp(0)
ExternalRef
bigint
bigint
bigint
number(19)
ModifiedOn (Watermark)
datetime2(3)
timestamp(3)
datetime(3)
timestamp(3)
OpportunityProduct — Positionen der Verkaufschancen (6 Datensätze)
Spalte
MSSQL
PostgreSQL
MySQL
Oracle
OpportunityProductId (PK, Identity)
bigint identity
bigint identity
bigint auto_increment
number(19) identity
OpportunityId (FK)
uniqueidentifier
uuid
char(36)
varchar2(36)
LineNumber
int
integer
int
number(10)
ProductName
nvarchar(200)
varchar(200)
varchar(200)
nvarchar2(200)
Quantity
decimal(18,4)
numeric(18,4)
decimal(18,4)
number(18,4)
PricePerUnit
decimal(19,4)
numeric(19,4)
decimal(19,4)
number(19,4)
DiscountPercent
float
double precision
double
binary_double
TaxRate
real
real
float
binary_float
IsOptional
bit
boolean
boolean
boolean
DeliveryDate
date
date
date
date
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.
Account
Nummer
Land
Besonderheit
Müller GmbH
ACC-1000
DE
Alle Felder gefüllt, inkl. Logo (Binärdaten) und JSON
Schmidt & Söhne KG
ACC-1001
AT
Umlaute und & im Namen, keine Beschreibung
Weber AG
ACC-1002
CH
Inaktiv, viele leere Felder (NULL)
Verkaufschance
Account
Status
Positionen
Neue Fertigungslinie
Müller GmbH
offen
3 (davon 1 optional)
Wartungsvertrag 2027
Müller GmbH
gewonnen
1
Ersatzteile Q4
Schmidt & Söhne KG
offen
1
Rahmenvertrag
Weber AG
verloren
1
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):
Links im Baum unter „Local & Attached“ → „Storage Accounts“ — Azurite ist dort meist
automatisch als „(Emulator - Default Ports) (Key)“ gelistet.
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.
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.
Datei
Inhalt
connections.json
Eine Connection je Datenbank (Name, Typ, Connection-String mit ${DEV_DB_PASSWORD})
tables.json
Die drei Tabellen mit Schlüssel, Watermark-Feld und den gemappten Feldern
seed.py
Erzeugt 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 Datenbanken
Kombinationen
Jobs
Beispiel
1 (z. B. mssql)
Datenbank auf sich selbst
3
mssql-to-mssql-account
2
2
6
mssql-to-postgres-opportunity
3
6
18
mysql-to-oracle-opportunityproduct
4 (all)
12
36
oracle-to-mssql-account
Alle Jobs haben diese Einstellungen:
Feld
Wert
Source / Target
Tabellenname, z. B. Account
Mapping
dev-<tabelle>, z. B. dev-account
WatermarkType / WatermarkField
datetime / ModifiedOn
Priority
Standard
IsActive
false
Jobs sind inaktiv
Die Jobs dienen als Testobjekte für list_jobs/get_job und als Vorlage. Sie laufen nicht, und
die DataBridge prüft inaktive Jobs beim Start nicht — deshalb steht bei ihnen isComplete: false.
Das ist kein Fehler. Aktiviert man einen Job, wird er beim nächsten Start geprüft.
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:
Mapping
Tabelle
Felder
Schlüssel (TargetKey)
dev-account
Account
19 (alle Spalten außer ModifiedOn)
AccountId (PrimaryKey)
dev-opportunity
Opportunity
10
OpportunityId (PrimaryKey)
dev-opportunityproduct
OpportunityProduct
10
OpportunityProductId (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.
Connection
Typ
Tabelle
Auth
Metadaten
bc-dev
D365BC
v2.0/customers
OAuth2 Client Credentials gegen /oauth/token (Mock)
EDMX wiremock/__files/bc/metadata.xml
nav-dev
D365NAV
Customers
Basic (dev/dev)
EDMX wiremock/__files/nav/metadata.xml
openapi-dev
OPENAPI
customers
API-Key-Header X-API-Key: dev-api-key
Spec 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:
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.
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:
Job
Mapping
Richtung
<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 gibtInvoke-RestMethod -Method 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.
Grenzen
Die Mocks merken sich nichts. Ein POST legt nichts an, ein anschließender GET liefert weiter 404.
Filter werden nicht ausgewertet. Die Listen-Stubs liefern immer denselben Bestand.
Dataverse (D365CE) ist nicht dabei: Der ServiceClient nutzt das SDK-Protokoll mit OAuth und
lässt sich nicht sinnvoll per HTTP-Stub abbilden.
Mit den Mocks allein (ohne servicebus/getchanges/process) laufen Jobs nicht Ende-zu-Ende.
Nutzbar sind dann Verbindungstest, Metadaten (list_connector_tables,
get_connector_table_schema) und CheckList. Für den vollen Ablauf siehe
Service Bus und Verarbeitung schalten.
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“).
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.
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.
Admin-Client braucht Port 5300
Der ServiceBusAdministrationClient (Queue-Status/MCP-Diagnose) spricht mit dem Emulator über
dessen Management-API auf Port 5300, der normale ServiceBusClient (Senden/Empfangen) über den
AMQP-Port 5672. ServiceBusService hängt den Port deshalb nur beim Erzeugen des Admin-Clients an
den Endpoint an, wenn der Connection-String UseDevelopmentEmulator=true enthält und noch keinen
Port hat. In Azure passiert dort nichts, weil der volle Hostname bereits einen impliziten
Standardport hat und UseDevelopmentEmulator nicht gesetzt ist.
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:
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.
dev/up.sh mssql process (Windows: dev\up.ps1 mssql process) — Datenbank, Verarbeitung und
Service-Bus-Emulator starten. getchanges wird dafür nicht gebraucht.
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.
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"}'
`IsActive` ist hier nicht nötig
Anders als der Timer prüft ExecuteJobIsActive nicht — es sucht den Job nur über den Namen
(ProcessJobsForRetrievalInOrderService.ExecuteByJobName). Die Seed-Jobs bleiben also bewusst
IsActive=false (siehe Jobs) und lassen sich trotzdem einzeln per ExecuteJob testen.
IsActive: true (per MCP update_job oder direkt in Azurite) ist erst nötig, wenn stattdessen der
Timer (getchanges) selbst anhand des Cron-Ausdrucks erkennen soll, welche Jobs fällig sind —
Verarbeitung bleibt so ein bewusster Opt-in, kein Automatismus.
Nachschauen, ob’s geklappt hat:
Log (Logging) zeigt beide Schritte durch — GetChanges erkennt X Änderungen,
ProcessStandardQueueItems verarbeitet die Nachricht.
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
Problem
Ursache und Lösung
up bricht mit Port-Konflikt ab
Ein 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 Fehlern
Zeilenenden. .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 API
Der Function Key fehlt. Er ist fest auf local gesetzt (?code=local).
Oracle braucht lange
Der 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 weg
Gewollt: Die Testdaten werden bei jedem Start zurückgesetzt.
WireMock startet nicht, Port-Konflikt auf 8089
WIREMOCK_PORT in dev/.env ändern. 9090 ist unter Fedora oft von Cockpit belegt.
Mock antwortet mit 404 auf alles
Der 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 Verbindungsfehler
Profil 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.
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).
Überschreibt local.settings.json bei jedem Aufruf
src/DataBridge/local.settings.json wird bei jedem--host/-HostMode-Aufruf neu erzeugt und
dabei kommentarlos überschrieben — auch echte Zugangsdaten, die dort für
Cloud-Debugging eingetragen waren, sind danach weg. Wer beides abwechselnd nutzt,
siehe Podman- und Cloud-Settings parallel halten.
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).
Das richtige Run-Profil auswählen
Rider (und die anderen IDEs) legen für jede Testklasse bzw. jeden Testfall automatisch eine eigene
Run-/Debug-Konfiguration an, sobald man einmal über die Gutter-Icons einen Test gestartet hat —
z. B. „OpenApiConnectorAuthTests“. Diese Konfigurationen bleiben oben im Auswahlmenü stehen und
werden schnell mit dem eigentlichen Funktions-Profil DataBridge verwechselt, gerade wenn Rider
zuletzt einen Test als aktive Konfiguration gemerkt hat. Startet man versehentlich eine
Test-Konfiguration statt DataBridge, laufen die Unit-Tests statt der Function App —
sichtbar u. a. daran, dass Breakpoints in Connectoren/Services nicht mehr halten oder plötzlich
Exceptions aus DataBridge.Test.* im Log auftauchen, die eigentlich von einem Test erwartet
werden (z. B. ServiceClient_WithoutCredentials_ThrowsNotSupported). Vor dem Debuggen also immer
kurz prüfen, dass oben rechts wirklich DataBridge (Projekt-Icon, nicht Test-Icon) ausgewählt
ist.
Rider
Lösung src/DataBridge.slnx öffnen (falls noch nicht offen).
Oben rechts das Run-Profil DataBridge auswählen (Rider erkennt das Azure-Functions-Projekt
automatisch und bietet das Profil aus launchSettings.json an).
Auf das Käfer-Symbol (Debug) statt Play klicken, oder Shift+F9.
Breakpoints wie gewohnt im Editor setzen (z. B. in ProcessQueueMessageService oder einem
Connector).
Visual Studio
src/DataBridge.slnx öffnen, DataBridge als Startprojekt setzen (Rechtsklick im
Projektmappen-Explorer → Als Startprojekt festlegen, falls nicht schon aktiv).
Im Dropdown neben dem grünen Play-Pfeil das Profil DataBridge wählen.
F5 (Debuggen starten) statt Ctrl+F5.
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:
Terminal in src/DataBridge öffnen.
func start (nutzt automatisch local.settings.json und den Port aus host.json/der
Umgebung — bei Bedarf func start --port 7062 explizit angeben).
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.
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:
Schalter
Argument bei up.sh --host/up.ps1 -HostMode
Wirkung
DATABRIDGE_GETCHANGES
getchanges
AzureWebJobs.GetChanges.Disabled in der generierten local.settings.json
dev\up.ps1 -HostMode mssql process# Verarbeitung an, Timer ausdev\up.ps1 -HostMode mssql getchanges process# beides andev\up.ps1 -HostMode mssql # beides aus – Verarbeitung nur manuell per ExecuteJob
dev/up.sh --host mssql process # Verarbeitung an, Timer ausdev/up.sh --host mssql getchanges process # beides andev/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:
dev/up.sh --host mssql process (Windows: dev\up.ps1 -HostMode mssql process) — Datenbank,
Verarbeitung und Service-Bus-Emulator, aber ohne Container-DataBridge.
In der IDE debuggen starten (s. o.), Breakpoint z. B. in ProcessQueueMessageService setzen.
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.
Klassischer Service Bus Explorer funktioniert nicht
Das weit verbreitete Desktop-Tool Service Bus Explorer (Community-Tool von Paolo Salvatori)
unterstützt den Emulator nicht — er ist kein vollwertiger Service-Bus-Namespace und hat andere
Auth-Mechanismen. Der oben verlinkte servicebus-explorer ist eigens für den Emulator gebaut.
Für den Alltag reicht MCP
Für die üblichen Fragen beim Debuggen — „ist die Nachricht überhaupt auf der Queue angekommen?“,
„wie viele Nachrichten liegen in sbqignored?“ — reichen die MCP-Diagnose-Tools. Die
Web-Oberfläche lohnt sich, sobald man Nachrichten manuell anschauen, selbst verschicken oder
erneut zustellen will — die MCP-Tools sind bewusst rein lesend (siehe
MCP-Server) und können das nicht.
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
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).
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.
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.
Podman überschreibt dieselbe Datei
src/DataBridge/local.settings.json ist die einzige Datei, aus der sowohl der Podman-Host-Modus
als auch dieses Cloud-Setup lesen — es gibt keine zweite, von der Function-Runtime automatisch
erkannte Datei. Läuft danach dev/up.sh --host/dev\up.ps1 -HostMode (siehe
Debuggen → Gegen Podman), wird die Datei kommentarlos überschrieben und die
gerade eingetragenen echten Zugangsdaten sind weg. Wer beides abwechselnd nutzt, siehe
Podman- und Cloud-Settings parallel halten weiter
unten.
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).
Das richtige Run-Profil auswählen
Leicht zu verwechseln mit einer automatisch angelegten Test-Konfiguration (Rider legt für jede
Testklasse/jeden Testfall eine eigene an, sobald man sie einmal gestartet hat). Läuft aus Versehen
ein Test statt der Function App, wirken Breakpoints in Connectoren/Services wie „tot“, und im Log
tauchen Exceptions aus DataBridge.Test.* auf, die eigentlich von einem Test erwartet werden.
Details und Beispiel siehe Debuggen → Gegen Podman. Vor dem
Debuggen prüfen, dass oben rechts wirklich DataBridge ausgewählt ist.
Rider
Lösung src/DataBridge.slnx öffnen.
Run-Profil DataBridge auswählen, dann auf das Käfer-Symbol (Debug) klicken bzw.
Shift+F9.
Breakpoints setzen, z. B. im Connector des Systems, das gerade untersucht wird.
Visual Studio
src/DataBridge.slnx öffnen, DataBridge als Startprojekt setzen.
Profil DataBridge im Dropdown neben dem Play-Pfeil wählen, dann F5.
Breakpoints setzen.
VS Code
Terminal in src/DataBridge öffnen, func start ausführen (liest local.settings.json
automatisch).
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.
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):
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.
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.
Läuft im selben Application-Insights-Workspace wie andere Instanzen
Die lokal debuggte Instanz sendet in dieselbe Application-Insights-Ressource wie jede andere
Instanz, die denselben Connection-String nutzt (z. B. die produktiv laufende Function App). In der
Transaktionssuche/Log-Analyse lässt sich die eigene Ausführung am ehesten über den Zeitpunkt und —
sofern gesetzt — cloud_RoleInstance von den übrigen unterscheiden. Für eine gezielte Analyse
TELEMETRY_CONTROL=TRACE_ALL in local.settings.json setzen, den Job manuell auslösen und danach
wieder zurücksetzen (siehe Betrieb → Logging → Tipps für die Fehlersuche).
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:
"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.
Vorsicht bei geteilten Umgebungen
Anders als beim Podman-Stack gibt es hier keinen Reset auf Knopfdruck und keine Testdaten, die
sich beliebig zurücksetzen lassen. Änderungen (Upserts, Watermarks, Queue-Nachrichten) wirken sich
auf die echte Zielumgebung aus und sind für andere sichtbar, die dieselbe Umgebung nutzen. Vor dem
Aktivieren von GetChanges/Process*QueueItems mit dem Team abstimmen, wer die Umgebung gerade
sonst verwendet — im Zweifel zuerst gegen Podman reproduzieren, bevor man gegen eine
geteilte Cloud-Umgebung debuggt.
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:
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.