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 ConnectionBase erbt.
  • 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 Tenants
  • SisTenantConfig Config – Konfiguration der Instanz
  • SisTenantInstance 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 – Pflichtfeld
    • Multiline – mehrzeilige Eingabe
    • CommaSeparatedValues – CSV-Eingabe
    • KeyValueList – Liste von Name=Wert (Feldnotation)
    • JsonFormat – erwartetes JSON-Schema (clientseitige Validierung)
    • LocalizedEnumDescription – für Enums (übersetzt)
    • OnlyMaster – nur On-Prem verfügbar
    • RequiresSchemaRefresh – Ä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: null bei 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 via ProcessMethod liefern
  • Stilles Lesen: SisParam_NoMessage → keine UI-Nachrichten senden

Abbruchbehandlung (Cancellation):

  • Bei Benutzerabbruch wird CancellationPending gesetzt.
    Prüfen Sie dies an sinnvollen Punkten und werfen Sie SisOperationCanceledException.

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 DELETE am Objekt (z. B. obj.GetParam("DELETE", false) == true).

Teilfehler nach Anlage:

  • Über out InnerException melden → 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:

  • LoadAllDataalle 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
  • LoadAdhocDataeinzelnen Datensatz gezielt lesen
    • ID steht in Process.Action.Params (Key = SourceObject)
  • LoadLockedData – wegen Sperren zurückgestellte Datensätze erneut lesen
    • IDs in LockedObjects
  • LoadRepeatDataWiederholungen auf Basis einer vorherigen Ausführung
    • Process.Action.GetParam<int>("PREVIOUS_ACTION_ID"), PreviousAction.RepeatRecordIds
    • int RepeatCount = SharedUtils.GetRepeatCount(o.Name); if (RepeatCount <= Process.MaxRepeatCount) RepeatIds.Add(o.GetValue());
  • LoadPlannedDatageplante Fehlerwiederholungen
    • Process.Action.PlannedRecordIds
  • LoadFailedDatadirekte Fehlerwiederholung
    • über PREVIOUS_ACTION_ID, PreviousAction.FailedRecordIds
  • LoadSuccessfulDataNachfolger-Syncs mit kontextbezogenen Datensätzen
    • über PREVIOUS_ACTION_ID

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

  1. DLL in connections-Ordner kopieren.
  2. Systemdienst neu starten, damit der Konnektor geladen wird.
  3. Versionierung dokumentieren (Breaking Changes → Schema-Refresh erforderlich).
  4. Bei Schemaänderung Eigenschaften mit RequiresSchemaRefresh kennzeichnen.

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: InnerException nutzen, Wiederholungen ermöglichen.
  • Abfragen: Platzhalter & Performance beachten.
  • Dokumentation der unterstützten Parameter & Limits.

Häufig verwendete Parameter (Auszug)

  • SisParam_ProcessObjects – Streaming via ProcessMethod
  • SisParam_GetDataById – Einzelabruf (Feldnotation möglich)
  • SisParam_GetDataByWhere – Filterbedingung (Listenabruf)
  • SisParam_GetDataLimit – Anzahl begrenzen (Tests)
  • LAST_SYNC_DATE, LAST_SYNC_VERSION – Grenzwerte/Deltas
  • SisParam_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).