Dynamische Formel-/Regelauswertung mit C# Skripten

Für bereits zwei Projekte gab es Anforderungen von Anwendern, das

  • bestimmte Regeln entweder noch nicht klar definiert sind
  • oder während des Betriebs änderbar sein sollen

Hier musste eine Lösung gefunden werden, es werden jetzt vom Anwender definierbare Skripte dynamisch kompiliert und ausgewertet.

Fachlicher Hintergrund

Projekt FiVe

  1. Es erfolgt eine Prüfung von Eigenschaften für ein ausgewähltes Materialstück, z.B. die aktuelle Breite
  2. Der eingegebene Wert muss geprüft werden, allerdings abhängig von anderen Eigenschaften des Materials (Materialtyp, Soll-Breite, Toleranzen)
  3. Die Regeln müssen anpassbar sein

Projekt BOAT

  1. Es erfolgt eine Ermittlung von Tarifen für Leistungen zu Vorgängen, dabei kann ein Vorgang mehrere Leistungen haben, diese haben wiederum abhängige Daten
    1. Vorgang mit Schiff A
    2. Leistung „Schiff festgemacht“
    3. Leistung „Gangway bereitgestellt“
    4. Leistung „Schiff losgemacht“
  2. Die Ermittlung der Tarife basiert dabei auch noch auf der Nutzung von allgemeinen wiederverwendbaren Formeln, es geht also schon in Richtung „Programmierung“

Umsetzung

Lösungsweg

  1. Der Anwender kann Skripte in C# eingeben die dann dynamisch zur Laufzeit kompiliert und ausgewertet werden
  2. Dabei werden von der Anwendung für die Skripte feste Eingabewerte/Parameter definiert, im ersten Projekt wird z.B. immer das aktuelle Material übergeben
  3. Das Objektmodell muss dem Anwender entsprechend bekannt sein, es ist also auch eine entsprechende Dokumentation notwendig

Skript erzeugen

Jetzt auch mal zum Code, beispielhaft am einfacheren Projekt FiVe, dazu gibt es ein paar vorbereitende Arbeiten:

Definition der auszuführenden Methode mit festen Namen und Typen , der Anwender kann hier nur den Rumpf der späteren Funktion eingeben, also alles innerhalb der geschweiften Klammern:

Definition des Ergebnistyps RangeResult entsprechend im Objektmodell

Zusätzlich müssen noch einige ergänzende Definitionen für eine Klasse, Namespaces etc. erfolgen, so das am Ende folgendes Standardskript entsteht:

Ein vollständiges Skript mit den Ergänzungen aus den Benutzerdaten (in der Datenbank erfasst) als Beispiel:

An dieser Stelle haben wir während der Auswertung der Regel nur den Text für das Skript erstellt, jetzt fehlen uns noch der eigentlich spannende Teil, die dynamische Kompilierung und Auswertung. Dazu wird zum einen der in .Net ohnehin vorhandene C# Compiler genutzt, außerdem wird dann per Reflection die Methode aufgerufen.

Kompilieren/Assembly erzeugen

Das Kompilieren des Skripts erzeugt eine Assembly (DLL) im Speicher die dann weiter verwendet werden kann. Wichtig ist dabei noch das die entsprechenden anderen genutzten Assemblies (vor allem mit dem Objektmodell) referenziert werden:

Skript ausführen

Das ausführen des Skripts erfolgt dann recht simpel mit .NET Reflection, da die Klasse und der Methodenname ja fest definiert sind. Da wir verschiedene Rückgabewerte haben können wird hier ein object zurückgegeben der dann in der aufrufenden Methode entsprechend umgewandelt wird. Um außerdem zu vermeiden dass unser übergebenes Material verändert wird (was zu recht interessanten Effekten in der Anwendung führen könnte), wird vor der Übergabe eine Kopie des Objekts erzeugt.

Alles zusammen

Das folgende Skript zeigt nochmal den kompletten Ablauf, hier mit generischen Typparametern da wird verschiedene Arten von Ergebnissen haben können.

Umsetzung im Projekt BOAT

Im Projekt BOAT ist die Umsetzung noch um einige „Spezialitäten“ komplexer geworden, ich werde diese hier aber nur kurz anreißen:

  1. Es gibt verschiedene Bereiche mit Regelauswertungen, wie die Ermittlung der Tarifgruppen, Ermittlung des Tarifs, Auswertung des Tarifs mit 5 verschiedenen auf einander basierenden Formeln.
  2. Die Auswertung der Regeln erfolgt aus Basis der Leistungen, diese gibt es aber in 6 verschiedenen Ausprägungen (aka Klassen).
  3. Es gibt eine globale Liste mit Feiertagen die übergeben werden muss (für eine spezielle immer vorhandene Funktion „IsHoliday“)
  4. Es gibt Skripte die andere allgemeine Skripte aufrufen, das heißt es können neue Probleme auftreten wie z.B. Namenskollision von Methoden. Außerdem ist zur Laufzeit nicht klar welche allgemeinen Skripte verwendet werden 8zumineest nicht ohne aufwändige Analyse), d.h. es müssen immer alle allgemeinen Skripte verfügbar sein.
  5. Für die allgemeinen Skripte ist der Rückgabewert auswählbar (aus eine Liste mit .NET Datentypen)
  6. Die Eingabe der Skripte erfolgt über das User Interface, das heißt entsprechende Unterstützung wäre schön. Hier ist nur ein Syntax Highlighting umgesetzt, Dinge wie Intellisense o.ä. nicht.

Es folgt noch ein Beispiel für ein entsprechendes komplettes Skript. Um die Implementierung und vor allem die Formelerstellung zu vereinfachen, wird hier statt der Übergabe der aktuellen Daten als Parameter an jede Methode, mit Klassenvariablen gearbeitet. Die allgemeinen Skriptteile werden als Properties der Klasse hinzugefügt.

Fazit

Die gefundene Lösung ist sehr flexibel, insbesondere wenn der Kunde noch nicht genau weiß wie die Regeln später aussehen werden, bzw. er sie gerne selbst ändern will.

Es gibt aber natürlich auch einige Nachteile:

  1. Das entsprechende KnowHow für die Programmierung und Skriptsprache muss beim Anwender vorhanden sein, diese Aufgabe wird aber in der Regel nicht von „normalen“ Anwender erledigt.
  2. Das Objektmodell muss entsprechen gut dokumentiert sein, so dass der Anwender entsprechend korrekte Skripte erstellen kann
  3. Die Performance wird durch die dynamische Kompilierung nicht besser, je nach Art und Anzahl der Formeln/Regeln ist dies auch spürbar.
  4. Durch die fehlende Kontrolle über die Inhalte muss die Fehlerbehandlung entsprechend implementiert werden, es können jetzt auch Kompilierungsfehler zur Anwendungslaufzeit auftreten oder auch Endlossschleifen/Rekursionen.
  5. Es muss darauf geachtet werden das die Eingabewerte aus dem Objektmodell nicht änderbar sind, z.B. durch entsprechende kopieren vor dem Methodenaufruf (macht es auch nicht schneller).
Verfasst von Matthias Jankowiak am 12. Dezember 2016