Die Entwicklung eines Konnektors in .Net
Diese Anleitung beschreibt, wie ein Syncler-Konnektor in .NET implementiert, konfiguriert und betrieben wird. Ein Konnektor kapselt die Anbindung an ein externes System (API, Datenbank, Service) und stellt dessen Schema, Lese-/Schreiboperationen sowie optional Abfragen für Syncs und Reports bereit.
Überblick
- Jeder Konnektor ist eine .NET-Klasse, die von
ConnectionBaseerbt. - Die Klasse wird in einer .NET Class Library entwickelt, die das
SynclerCommon-Assembly referenziert. - Bereitgestellt wird die erzeugte DLL im
connections-Verzeichnis der Syncler-Installation.
Wichtig: DLLs werden nur beim Systemstart geladen. Nach einem Update ist daher ein Neustart erforderlich.
Lebenszyklus & Laufzeitverhalten
- Bei Verwendung erzeugt Syncler pro Anforderung eine Instanz Ihres Konnektors (mehrere Instanzen können parallel existieren).
- Gemeinsam genutzte Laufzeitwerte (z. B. OAuth-Tokens) nicht in der Instanz selbst puffern, sondern in der Parameter-Tabelle speichern.
- Wird die Instanz nicht mehr benötigt, wird sie verworfen; volatile Zustände gehen verloren.
Bereitgestellte Ressourcen der Basisklasse
Ihre abgeleitete Klasse hat u. a. Zugriff auf:
SisDatabaseWrapper Database– Datenbankzugriff des aktuellen TenantsSisTenantConfig Config– Konfiguration der InstanzSisTenantInstance Instance– Instanzkontext inkl. Übersetzungen
Diese Objekte unterstützen z. B. Persistenz (Parameter), Übersetzung, Protokollierung und Konfigurationszugriffe.
UI-Konfiguration per Attribute
Die Konfigurationsoberfläche des Systems wird deklarativ über Attribute an Klasse und Eigenschaften gesteuert:
- Seitenaktivierung an der Klasse
PageCommon– zeigt die Standardseite mit Eigenschaftskonfiguration.
- Lokalisierung & Beschriftung
LocalizedCategory– Kategorie (übersetzt)LocalizedDisplayName– Anzeigename (übersetzt)LocalizedDescription– Hilfetext/Anleitung (übersetzt)
- Validierung & Darstellung
Required– PflichtfeldMultiline– mehrzeilige EingabeCommaSeparatedValues– CSV-EingabeKeyValueList– Liste vonName=Wert(Feldnotation)JsonFormat– erwartetes JSON-Schema (clientseitige Validierung)LocalizedEnumDescription– für Enums (übersetzt)OnlyMaster– nur On-Prem verfügbarRequiresSchemaRefresh– Änderung erzwingt Schema-Neuaufbau
Beispiel:
[PageCommon] public class MyConnector : ConnectionBase { [LocalizedCategory("CONNECTION.CREDENTIALS")] [LocalizedDisplayName("CONNECTION.APIKEY")] [LocalizedDescription("CONNECTION.APIKEY.DESC")] [Required] public string ApiKey { get; set; } [LocalizedCategory("CONNECTION.ENDPOINT")] [LocalizedDisplayName("CONNECTION.BASEURL")] [LocalizedDescription("CONNECTION.BASEURL.DESC")] [Required, JsonFormat("{ 'type': 'string', 'format': 'uri' }")] public string BaseUrl { get; set; } [LocalizedCategory("CONNECTION.OPTIONS")] [LocalizedDisplayName("CONNECTION.SCOPES")] [LocalizedDescription("CONNECTION.SCOPES.DESC")] [CommaSeparatedValues] public string Scopes { get; set; } }
Sicherheitsaspekte
- Geheimnisse (API-Key, Client Secret) in Konfiguration/Parametern verschlüsselt speichern.
- Nie Secrets in Logs oder Protokolle schreiben.
- Zugriff auf externe Dienste immer TLS/HTTPS; Header gezielt setzen.
Validierung
Vor Speichern oder Nutzung kann Syncler Validate() aufrufen:
- Prüfen Sie Pflichtangaben, Erreichbarkeit, Minimalrechte, ggf. Tokenverfügbarkeit.
- Rückgabe:
nullbei Erfolg, ansonsten Fehlertext (wird im UI angezeigt). - Hinweis: Bei direkter API-Nutzung kann
Validate()nicht aufgerufen werden – defensiv programmieren.
public override Exception? Validate() { if (string.IsNullOrWhiteSpace(ApiKey)) return new Exception("API key is required."); if (string.IsNullOrWhiteSpace(BaseUrl)) return new Exception("Base URL is required."); // Optional: Ping/Health-Check return null; }
Schema-Design & GetConnectionSchema
Konnektoren liefern Schemaobjekte (Entitäten/Objekttypen), inkl.:
- ID-Felder (ermöglichen Datensatz-Abbildungen, Einzelabrufe, konfliktsichere Syncs).
- Änderungsinformation (z. B.
updated_at), die Delta-Logik & Konfliktmanagement ermöglicht. - Schachtelungen (Objektgruppen, Positionslisten).
Implementierung: GetConnectionSchema() gibt ein Dictionary zurück
Name → XML-Serialisierung von SisSchemaObject.
- Wird beim Neuanlegen und bei Schema-Aktualisierung aufgerufen.
- Nach Ausführung wird die Verbindung erneut gespeichert (z. B. für Seiteneffekte wie OAuth-Setup).
Hinweise zum Schema:
- Beim Schreiben muss Ihr Konnektor die aktuellen Daten (inkl. generierter IDs & Änderungsinfo) zurückgeben.
- Für Positionslisten (geschachtelte Daten) spezielle Syncs berücksichtigen.
Schema-basiertes Lesen: GetData
Ein Leseaufruf enthält das Schemaobjekt plus eine Parameterliste. Ihre Implementierung muss die folgenden Fälle unterstützen:
- Einzelabruf per ID:
SisParam_GetDataById(Feldnotation möglich) - Listenabruf mit Filter:
SisParam_GetDataByWhere - Grenzwerte/Deltas:
LAST_SYNC_DATE,LAST_SYNC_VERSION - Test-/Limitabruf:
SisParam_GetDataLimit - Streaming:
SisParam_ProcessObjects→ Datensätze progressiv viaProcessMethodliefern - Stilles Lesen:
SisParam_NoMessage→ keine UI-Nachrichten senden
Abbruchbehandlung (Cancellation):
- Bei Benutzerabbruch wird
CancellationPendinggesetzt.
Prüfen Sie dies an sinnvollen Punkten und werfen SieSisOperationCanceledException.
Beispiel (Ausschnitt):
public override List<SisObject> GetData(SisSchemaObject SchemaObject, List<SisParam> GetParams) { if (CancellationPending) throw new SisOperationCanceledException(); List<SisObject> ReturnObjects = []; var IdParam = GetParams.Find(item => item.Name == SisParam_GetDataById.ParameterName); var WhereParam = GetParams.Find(item => item.Name == SisParam_GetDataByWhere.ParameterName); var LimitParam = GetParams.Find(item => item.Name == SisParam_GetDataLimit.ParameterName); // ... Anfrage an Zielsystem bauen (inkl. Delta/Filter) ... // Falls Streaming gefordert: ProcessMethod?.Invoke(ReturnObjects); // Sonst Rückgabe aggregiert return ReturnObjects; }
Abfrage-basiertes Lesen (optional)
Für Abfrage-Syncs und Reports (Syncler-Focus) können Sie Query-Funktionen implementieren:
GetQuerySchema(SisQuery Query)– liefert Spaltenschema zur Abfrage.GetQueryData(SisQuery Query, SisDataMapping DataMapping)– liefert Abfrageergebnis.
Spezielle Platzhalter (werden vom aufrufenden Sync/Ablauf ersetzt):
#Mandant#– globaler Mandantenwert#UserService#– aktuelles Benutzerkennzeichen (Syncler-Focus)#SourceId#,#TargetId#,#OpportunityId#– Kontext-IDs (Datensatz-Abbildung)#LastVersion#,#LastDatetime#– Delta-Parameter#FlowFilter#– Laufzeitfilter aus Abläufen
Reports (Focus):
- Sie erhalten sortierte/gefilterte/paginierte SQL-Statements + Count-Statement (Gesamtanzahl).
- Antwortzeiten optimieren (Indexe/Server-seitige Filter, schlanke Projektionen).
Schema-basiertes Schreiben: SetData
Setzen/Ändern/Löschen eines Datensatzes für ein Schemaobjekt:
- Parameter: aktuelles Objekt (zu schreiben) + Schemaobjekt
- Rückgabe: aktueller Datensatz nach der Änderung
→ muss generierte IDs und Änderungsinformationen enthalten (wichtig für Datensatz-Abbildungen & Konfliktmanagement). - Löschen: Flag
DELETEam Objekt (z. B.obj.GetParam("DELETE", false) == true).
Teilfehler nach Anlage:
- Über
out InnerExceptionmelden → es wird trotzdem eine Datensatz-Abbildung erzeugt, damit keine erneute Neuanlage erfolgt.
Die Ausführung gilt als fehlerhaft und kann wiederholt werden.
Nur Änderungen übertragen:
- Spalten/Objekte sind als geändert markiert (
col.IsUpdated()).
Übertragen Sie nur diese Änderungen (spart Volumen, reduziert Konflikte).
Beispiel (Ausschnitt):
public override SisObject SetData(SisObject SetObject, SisSchemaObject SchemaObject, out Exception? InnerException) { InnerException = null; bool isDelete = SetObject.GetParam("DELETE", false); try { if (isDelete) { // DELETE im Zielsystem ... return SetObject; } else { // INSERT/UPDATE – nur geänderte Felder nutzen // Antwort MUSS ID + Updated enthalten return ReadBackFromTarget(SetObject); } } catch (Exception ex) { // Bereits erzeugte Entität? → InnerException setzen, damit Abbildung entsteht if (!SetObject.IsNewObject) { InnerException = ex.Message; return SetObject; // Abbildung kann erzeugt werden } throw; } }
Sync-Ausführung & Ladepfade (Load*-Methoden)
Schema-basierte Syncs rufen Ihr Quellsystem nicht direkt für Quellenlisten per GetData auf.
Stattdessen verwendet Syncler Load-Methoden, die Sie implementieren und intern GetData nutzen:
LoadAllData– alle Datensätze lesen- Wird genutzt, wenn am Sync „Immer alle Datensätze abfragen“ aktiv ist oder keine Änderungsinfo vorliegt.
LoadChangedData– nur geänderte Datensätze lesen- Grenzwert:
LAST_SYNC_DATE/LAST_SYNC_VERSION
- Grenzwert:
LoadAdhocData– einzelnen Datensatz gezielt lesen- ID steht in
Process.Action.Params(Key =SourceObject)
- ID steht in
LoadLockedData– wegen Sperren zurückgestellte Datensätze erneut lesen- IDs in
LockedObjects
- IDs in
LoadRepeatData– Wiederholungen auf Basis einer vorherigen AusführungProcess.Action.GetParam<int>("PREVIOUS_ACTION_ID"),PreviousAction.RepeatRecordIdsint RepeatCount = SharedUtils.GetRepeatCount(o.Name);if (RepeatCount <= Process.MaxRepeatCount) RepeatIds.Add(o.GetValue());
LoadPlannedData– geplante FehlerwiederholungenProcess.Action.PlannedRecordIds
LoadFailedData– direkte Fehlerwiederholung- über
PREVIOUS_ACTION_ID,PreviousAction.FailedRecordIds
- über
LoadSuccessfulData– Nachfolger-Syncs mit kontextbezogenen Datensätzen- über
PREVIOUS_ACTION_ID
- über
Zielabfrage (GetData) wird für Einzelabrufe verwendet (z. B. Read-Target für Aktualisierung).
Beispiel: Minimaler Konnektor
using SIS.Common.Models; namespace SIS.Connection { [PageCommon(1)] public class MyApiConnector : ConnectionBase { [LocalizedCategory("CREDENTIALS"), LocalizedDisplayName("API Key"), Required] public string ApiKey { get; set; } [LocalizedCategory("ENDPOINT"), LocalizedDisplayName("Base URL"), Required] public string BaseUrl { get; set; } public override Exception? Validate() { if (string.IsNullOrWhiteSpace(ApiKey)) return new Exception("API key is required."); if (string.IsNullOrWhiteSpace(BaseUrl)) return new Exception("Base URL is required."); // Optional: Ping/Health-Check return null; } public override Dictionary<string, string> GetConnectionSchema() { // SisSchemaObject → XML serialisieren und zurückgeben var dict = new Dictionary<string, string>(); dict["account"] = BuildAccountSchemaXml(); dict["contact"] = BuildContactSchemaXml(); return dict; } public override List<SisObject> GetData(SisSchemaObject SchemaObject, List<SisParam> GetParams) { if (CancellationPending) throw new SisOperationCanceledException(); List<SisObject> ReturnObjects = []; var IdParam = GetParams.Find(item => item.Name == SisParam_GetDataById.ParameterName); var WhereParam = GetParams.Find(item => item.Name == SisParam_GetDataByWhere.ParameterName); var LimitParam = GetParams.Find(item => item.Name == SisParam_GetDataLimit.ParameterName); // ... Anfrage an Zielsystem bauen (inkl. Delta/Filter) ... // Falls Streaming gefordert: ProcessMethod?.Invoke(ReturnObjects); // Sonst Rückgabe aggregiert return ReturnObjects; } override SisObject SetData(SisObject SetObject, SisSchemaObject SchemaObject, out Exception? InnerException) { InnerException = null; // INSERT/UPDATE/DELETE durchführen // Objekt mit ID + Updated zurückgeben return current; } // … LoadAllData/LoadChangedData/… implementieren und intern GetData nutzen } }
Persistenz von Laufzeitparametern
Nutzen Sie die Parameter-Tabelle, um Laufzeitwerte mandanten- und verbindungsbezogen zu speichern:
// Schreiben var tokenParam = new SisParam { Name = "AccessToken", Value = tokenString }; Database.SaveParameter(tokenParam, connectionId: this.Id.ToString()); // Lesen var list = Database.GetParameterList("AccessToken", connectionId: this.Id.ToString()); var token = list.FirstOrDefault()?.GetValue<string>();
Vorteil: Thread-sicher, instanzunabhängig, über Neustarts hinweg verfügbar.
Logging & Nachrichten
- Nachrichten (flüchtig, UI):
MessageMethod(...)/ systemnah:InsertMessage(...) - Protokolle (persistiert):
InsertLog(...)
Testen & Qualität
- Unit-Tests gegen Mock-APIs/-Daten.
- Timeouts/Retry für externe Calls; Rate Limits beachten.
- Große Datenmengen mit Paging/Delta testen; Speicherverbrauch beobachten.
- Fehlerfälle (Netzwerk, 4xx/5xx, Teilerfolg nach Anlage) gezielt durchspielen.
Deployment & Versionierung
- DLL in
connections-Ordner kopieren. - Systemdienst neu starten, damit der Konnektor geladen wird.
- Versionierung dokumentieren (Breaking Changes → Schema-Refresh erforderlich).
- Bei Schemaänderung Eigenschaften mit
RequiresSchemaRefreshkennzeichnen.
Best Practices (Checkliste)
- Schema enthält ID und Änderungsfeld (
updated). - Nur geänderte Felder schreiben (
isupdated). - Einzel-/Listen-/Delta-Pfad korrekt implementiert.
- Cancellation sauber behandeln.
- Parameter-Store für Tokens/Zustände nutzen.
- Validate() mit sinnvollen Checks.
- Logging vollständig, ohne Secrets.
- Fehlerpfade:
InnerExceptionnutzen, Wiederholungen ermöglichen. - Abfragen: Platzhalter & Performance beachten.
- Dokumentation der unterstützten Parameter & Limits.
Häufig verwendete Parameter (Auszug)
SisParam_ProcessObjects– Streaming viaProcessMethodSisParam_GetDataById– Einzelabruf (Feldnotation möglich)SisParam_GetDataByWhere– Filterbedingung (Listenabruf)SisParam_GetDataLimit– Anzahl begrenzen (Tests)LAST_SYNC_DATE,LAST_SYNC_VERSION– Grenzwerte/DeltasSisParam_NoMessage– keine UI-Nachrichten erzeugen
Zusätzlich je nach Szenario:
#Mandant#,#UserService#,#SourceId#,#TargetId#,#OpportunityId#,#FlowFilter#(für Abfragen/Abläufe, werden von Syncler ersetzt).