Anwendungsarchitektur
Projekte, Dependency Injection, host.json, Functions, Service-Schicht, Connector-Basisklassen, Transformations-Pipeline und Domänenobjekte.
Projekte (src/DataBridge.slnx)
| Projekt | Typ | Zweck |
|---|
DataBridge | Azure Functions, net10.0, isolated | Host: Trigger, DI, Telemetrie-Bootstrap, Durable Functions, MCP-Endpunkt |
DataBridge.Core | Class Library | Services, Connectoren, Resolver, Mapper, Source-Operationen, Modelle, Telemetrie |
DataBridge.Test | xUnit v3 + Moq (Microsoft Testing Platform) | Unit-Tests; Ordnerstruktur spiegelt DataBridge.Core |
DeadLetterMover | Konsolenanwendung | Requeue 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
| Bereich | Einstellung | Wert |
|---|
| Allgemein | functionTimeout | 10 min |
| AI-Sampling | excludedTypes | Request;Exception |
| Durable | hubName | DurableFunctionsHub, Azure-Storage-Provider, Distributed Tracing V2 |
| Storage Queues | batchSize / maxDequeueCount | 1 / 5 (nur für Durable relevant) |
| Service Bus | autoCompleteMessages | false |
| Service Bus | maxConcurrentCalls | 12 |
| Service Bus | maxAutoLockRenewalDuration | 5 min |
Functions
| Function | Trigger | Aufgabe |
|---|
GetChanges | Timer %GetChangesTimerExpression% | Initialisiert thread-sicher über InitializationService und ruft ProcessJobsForRetrievalInOrderService.Execute auf |
ProcessStandardQueueItems, ProcessHighPriorityQueueItems, ProcessLowPriorityQueueItems | Service Bus sbqexport, sbqhighexport, sbqlowexport | Verarbeiten je eine Nachricht end-to-end über ProcessQueueMessageService.Execute |
ExecuteJob | HTTP POST | Stößt einen Job per Name über ProcessJobsForRetrievalInOrderService.ExecuteByJobName an |
ExecuteHttpRequest | HTTP POST | Ad-hoc-HTTP-GET mit eigenem HttpClient (NTLM oder anonym), unabhängig von den Connectoren |
GetInfo | HTTP GET | Assembly-Version |
CheckList | HTTP GET | InitializationService.CheckAsync(): Mappings gegen Live-Metadaten der Systeme |
Validate | HTTP GET | Erzwingt die Initialisierung, liefert das Fehlerlog |
GetJobs, GetJobByRowKey | HTTP GET job, job/{rowKey} | Geladene Job-Konfiguration |
GetMappings, GetMappingByRowKey | HTTP GET mapping, mapping/{name} | Geladene Mappings, Suche per Name mit Fallback auf RowKey |
Mcp, McpGet | HTTP POST/GET mcp | MCP-Server; GET antwortet mit 405, weil SSE-Streams nicht unterstützt werden |
StartGetChangesOrchestration, GetChangesOrchestrator, GetChangesActivity, JobState | HTTP, Orchestration, Activity, Entity | Optionale 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 .-> REMDie 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.
| Service | Verantwortung | Wichtige Methoden |
|---|
InitializationService | Initialisierung, Ready/Error-Zustand | EnsureInitializedAsync, CheckAsync, ResetInitialization, GetErrorLogs, HasError |
SettingsService | App-Settings, Connection-Strings | GetConnectionString mit Fallback-Kette ConnectionStrings: → SERVICEBUSCONNSTR_ → SQLCONNSTR_ → CUSTOMCONNSTR_ → App-Setting; GetStorageAccountName, GetServiceBusFullyQualifiedNamespace |
StorageService | Table Storage | ValidateAndInitialize, GetTableEntries<T>, UpdateTableEntry<T> (Upsert Merge, ohne ETag) |
ConnectionService | Connector je Connection | Initialize mit Verbindungstest, IsValidConnection, GetConnector |
MappingService | Mappings gruppiert nach Name | Initialize, GetMapping, ValidateMappings |
JobService | Jobs, Validierung, Cron | Initialize, GetJobsToRun (NCrontab, 6 Felder), GetJobByName |
WatermarkService | Persistenz des Stands | GetWatermarkByJobName, GetAllWatermarks (Bulk), UpdateWatermarkByJobName; Wertlogik in WatermarkValue |
ServiceBusService | Enqueue | SendMessageAsync, AddMessageOrSendAsync (Batch bis 200 oder Größenlimit), FlushBatch; Priorität → Queue |
ProcessJobsForRetrievalInOrderService | Erkennung | Execute (aktive Jobs nach Order), ExecuteByJobName, ExecuteJob |
ProcessQueueMessageService | Verarbeitung | Execute: Deserialisierung, Backoff, Upsert/Delete/Action, Response, Transitive, Ignored |
CheckListService | Diagnose | Check() gegen Live-Metadaten der Connectoren |
MetricsService | Custom Metrics | pre-aggregiert über GetMetric, kein Sampling |
McpToolService | MCP-Tool-Logik | read-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 & SBSConnector-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.
ConnectionTypes | Connector |
|---|
D365CE | CeConnector |
D365BC | BcConnector |
D365NAV | NavConnector |
OPENAPI | OpenApiConnector |
MSSQL, PGSQL, MYSQL, ORACLE | MsSqlConnector, PostgreSqlConnector, MySqlConnector, OracleConnector |
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
| Modell | Zweck |
|---|
ConnectionTableEntry | PartitionKey connection, RowKey = Name, Type, Json = ConnectionSetting |
JobTableEntry / Job | PartitionKey job; Source/Target-Connection, Order, Filter, Mapping, WatermarkType/WatermarkField, Schedule, Action, Priority, ResponseJob, Source/Target (job-eigene Pfade) |
MappingTableEntry / TableMapping + FieldMapping | Feldzuordnung, TargetKey/TargetKeys (mit KeyRank), SourceOperation, TargetOperation |
WatermarkTableEntry | PartitionKey watermark, RowKey = Job-Name, Watermark, LastCronRun |
QueueMessage + QueueMessageProperty | Transportobjekt, siehe Nachrichtenschema |
TableMetadata / ColumnMetadata | Schema-Informationen für MCP und CheckList |
| Enums | ConnectionTypes, 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.