Datenaustausch zwischen Client und Server in C# mit SignalR

Bei der Entwicklung von Anwendungen bestehend aus einem Server und einem oder mehreren Clients tritt folgender Ablauf sehr häufig auf:

  1. Ein Client fordert über die API des Servers einen Datensatz an.
  2. Der Server holt diesen Datensatz aus z.B. einer Datenbank.
  3. Der Server entfernt aus diesem Datensatz alle Werte, die für den Client nicht relevant sind oder (z.B. aus Datenschutzgründen) nicht an den Client übertragen werden sollen. Oder er kopiert nur die für den Client relevanten Daten aus dem Datensatz in eine neue Datenstruktur.
  4. Der Datensatz oder die neue Datenstruktur wird an den Client gesendet.
  5. Der Client bzw. sein Anwender ändert einige Werte des Datensatzes und sendet ihn zurück an den Server.
  6. Der Server muss nun den Datensatz aus der Datenbank und die vom Client gesendeten Daten zusammenführen und wieder in der Datenbank speichern. Dabei sind alle Werte, welche vom Client nicht zurück gesendet wurden (also den Wert null haben) nicht zwangsläufig vom Client geändert worden, sondern wurden möglicherweise bereits vorher vom Server entfernt. Diese dürfen natürlich nun nicht auch in der Datenbank gelöscht werden.

Dem Client soll also nur eine Untermenge der tatsächlich in einem Datensatz befindlichen Daten bekannt sein. Das hat zudem den Vorteil, dass eine Erweiterung des Datensatzes in neueren Versionen des Servers keine Auswirkungen auf den Client hat(sofern dieser nicht auch mit den neuen Daten arbeiten soll).

Im Folgenden wird ein generischer Ansatz vorgestellt werden, der eine nachträgliche  Änderung der Datenstrukturen mit minimalen Aufwand ermöglicht.

Unsere Beispielanwendung

Gehen wir davon aus, wir möchten die Mitarbeiter einer Firma in einer Datenbank verwalten. Die Webapplikation speichert von jedem Mitarbeiter eine Id, den Vor- und Nachnamen, seine Abteilung, sein Geburtsdatum und sein Gehalt. Es soll eine Client-Anwendung geben, welche sich via SignalR mit der Webapplikation auf dem Server verbindet und die Mitarbeiter-Datensätze herunterladen kann. Der Anwender des Clients kann einen Mitarbeiter nun bearbeiten, z.B. seine Abteilung ändern. Anschließend werden diese Änderungen wieder an die Webapplikation übertragen und in der Datenbank gespeichert. Aus Gründen des Datenschutzes, dürfen die Daten für Geburtstag und Gehalt jedoch nicht mit an den Client übertragen werden. Der Client kennt also nur einen Teil der über einen Mitarbeiter gespeicherten Daten. Vor dem Übertragen eines Mitarbeiter-Datensatzes an den Client müssen also die Vertraulichen Daten entfernt werden. Nach dem Zurücksenden eines Datensatzes vom Client zum Server muss dieser mit den vertraulichen Daten wieder zusammengeführt werden.

Webapplikation, Client und eine gemeinsame DLL

Um das Problem elegant zu lösen, erstellen wir zunächst drei Projekte: Die Webapplikation, eine Client-Anwendung (z.B. für den Desktop) und eine DLL, welche von den beiden anderen Projekten referenziert wird.

In der Webapplikation erstellen wir eine Klasse namens Employee, welche alle Daten über einen Mitarbeiter speichert und z.B. mit dem Entity Framework auf eine Datenbanktabelle abgebildet wird. In der DLL erstellen wir eine Klasse namens EmployeeSubset. Diese Klasse enthält alle Daten eines Mitarbeiters, welche auch dem Client bekannt sein sollen. Für die Kommunikation zwischen Server und Client verwenden wir nun ausschließlich die Klasse EmployeeSubset.

Der Server muss also alle Employee-Objekte aus der Datenbank in EmployeeSubset-Objekte umwandeln können und EmployeeSubset-Objekte vom Client mit den dazu gehörenden Employee-Objekten aus der Datenbank zusammenführen können.

Auf den ersten Blick liegt es Nahe, zwischen den Klassen Employee und EmployeeSubset eine Vererbung einzurichten. In diesem Fall könnten Employee-Objekte einfach als EmployeeSubset gecastet werden. Aus folgenden Gründen führt dies jedoch nicht zum Ziel:

  • Nur weil ein Objekt auf einen anderen Typ gecastet wird, ändert sich dadurch nicht der Typ des Objekts selbst. SignalR wird das Objekt weiterhin als Employee-Objekt erkennen und den kompletten Datensatz zum Client übertragen.
  • In einer Anwendung mit mehreren unterschiedlichen Clients, die alle eine andere Subset-Klasse benötigen, müsste die Employee-Klasse von allen diesen Klassen erben. In C# kann jedoch immer nur eine Klasse an eine andere Klasse vererben.
  • Zum Zusammenführen von Employee-Objeken und EmployeeSubset-Objekten würde uns eine Vererbung keine Vorteile bringen.

Objekte durch Reflection umwandeln und zusammenführen

Im Folgendem sehen wir einen Ansatz, wie man die Umwandlung zwischen den Klassen Employee und EmployeeSubset mittels Reflection durchführen können. Die Methode CreateSubset (welche eine Extension Method ist und daher in einer static Klasse stehen muß) sucht nach allen schreibbaren Properties in der Subset-Klasse T und kopiert den Inhalt einer gleichnamigen Property aus dem als source-Parameter übergebenen Objekt in das Subset-Objekt.
Die Methode ImportSubset sucht alle lesbaren Properties in dem Subset-Parameter. Wenn eine Property mit dem gleichen Namen in dem target

        public static T CreateSubset<T>(this object source) where T : new()
        {
            T subset = new T();
            Type srcType = source.GetType();
            foreach (PropertyInfo p in typeof(T).GetProperties().Where(p => p.CanWrite))
            {
                PropertyInfo sp = srcType.GetProperty(p.Name);
                if (sp == null) throw new Exception("The property '" + p.Name + "' of type '" + typeof(T).FullName + "' does not exist in type '" + srcType.FullName + "'!");
                p.SetValue(subset, sp.GetValue(source));
            }
            return subset;
        }

        public static void ImportSubset(this object target, object subset)
        {
            Type tgtType = target.GetType();
            foreach (PropertyInfo p in subset.GetType().GetProperties().Where(p => p.CanRead))
            {
                PropertyInfo tp = tgtType.GetProperty(p.Name);
                if (tp == null) throw new Exception("The property '" + p.Name + "' of type '" + subset.GetType().FullName + "' does not exist in type '" + tgtType.FullName + "'!");
                tp.SetValue(target, p.GetValue(subset));
            }
        }

Wir können diese Methode also wie folgt verwenden:

            Employee employee = new Employee() { // Beispiel-Employee erstellen
                Id = 1,
                Forename = "Max",
                Surname = "Muster",
                Department = "Entwicklung",
                Birthday = new DateTime(1990, 6, 5),
                Salary = 65000
            };

            // Erstellen eines Subsets durch den Server
            EmployeeSubset subset = employee.CreateSubset<EmployeeSubset>();  

            // Hier würde das Subset z.B. durch SignalR an den Client übertragen werden.

            // Der Client verändert das Subset:
            subset.Department = "Vertrieb";

            // Hier würde das geänderte Subset zurück an den Server gesendet.

            // Der Server führt den ursprünglichen Datensatz mit dem Subset zusammen
            employee.ImportSubset(subset);

            // employee.Department hat nun ebenfalls den Wert "Vertrieb"

Die Vorteile dieses Vorgehens:

  • Das Erstellen und Zusammenführen des Subsets erfordert nur noch je eine Zeile Programmcode.
  • Die Datensätze Employee und EmployeeSubset lassen sich mit sehr wenig Aufwand erweitern: Es reicht dazu aus, einfach neue Properties in die Klassen einzufügen. Wenn in beiden Klassen neue Properties mit gleichem Namen und Typ eingefügt werden, werden diese automatisch bei der Subset-Erstellung und dem Zusammenführen berücksichtigt.