Workflow Core - eine Business Process Engine für .Net Core

Bild


Hallo allerseits!


Wir haben uns entschlossen, das Thema der Migration des Projekts mithilfe der Windows Workflow Foundation auf .Net Core zu unterstützen , die von Kollegen von DIRECTUM gestartet wurde, da wir vor einigen Jahren mit einem ähnlichen Problem konfrontiert waren und unseren eigenen Weg gegangen sind.


Beginnen wir mit der Geschichte


Unser Flaggschiffprodukt, Avanpost IDM, ist ein System für den Lebenszyklus von Konten und die Verwaltung des Mitarbeiterzugriffs. Er weiß, wie der Zugriff sowohl automatisch auf der Grundlage des Rollenmodells als auch gemäß den Anforderungen verwaltet wird. Zu Beginn der Produktentstehung hatten wir ein recht einfaches Selbstbedienungssystem mit einem einfachen schrittweisen Arbeitsablauf, für den der Motor im Prinzip nicht erforderlich war.


Bild


Als wir jedoch mit großen Kunden konfrontiert wurden, stellten wir fest, dass ein viel flexibleres Tool erforderlich war, da deren Anforderungen an die Prozesse zur Koordinierung der Zugriffsrechte nach den Regeln eines guten, gewichtigen Workflows strebten. Nach der Analyse der Anforderungen haben wir beschlossen, einen eigenen Prozesseditor im BPMN-Format zu entwickeln, der unseren Anforderungen entspricht. Wir werden später über die Entwicklung des Editors mit React.js + SVG sprechen und heute das Backend-Thema diskutieren - die Workflow-Engine oder die Business Process Engine.


Anforderungen


Zu Beginn der Entwicklung des Systems hatten wir folgende Anforderungen an den Motor:


  • Unterstützung für Prozessdiagramme, ein verständliches Format, die Möglichkeit, von unserem Format in das Engine-Format zu senden
  • Prozesszustandsspeicher
  • Unterstützung für die Prozessversionierung
  • Unterstützung für die parallele Ausführung (Verzweigungen) des Prozesses
  • Eine geeignete Lizenz zur Verwendung der Lösung in einem replizierten kommerziellen Produkt
  • Unterstützung für horizontale Skalierung

Nach einer Marktanalyse (für 2014) haben wir uns für eine praktisch nicht alternative Lösung für .Net entschieden: Windows Workflow Foundation.


Windows Workflow Foundation (WWF)


WWF ist die Technologie von Microsoft zum Definieren, Ausführen und Verwalten von Workflows.


Die Basis seiner Logik ist eine Reihe von Containern für Aktionen (Aktivitäten) und die Fähigkeit, aus diesen Containern sequentielle Prozesse zu erstellen. Der Container kann normal sein - ein bestimmter Schritt in dem Prozess, in dem die Aktivität ausgeführt wird. Es kann ein Manager sein, der die Logik der Verzweigung enthält.


Sie können einen Prozess direkt in Visual Studio zeichnen. Das kompilierte Geschäftsprozessdiagramm ist in Haml gespeichert, was sehr praktisch ist - das Format ist beschrieben, es ist möglich, einen selbstgeschriebenen Prozessdesigner zu erstellen. Das ist auf der einen Seite. Andererseits ist Xaml nicht das bequemste Format zum Speichern einer Beschreibung - das kompilierte Schema für einen mehr oder weniger realen Prozess erweist sich nicht zuletzt wegen der Redundanz als riesig. Es ist sehr schwer zu verstehen, aber Sie müssen es verstehen.


Aber wenn man früher oder später Zen mit Schemata verstehen und lesen lernen kann, dann trägt die mangelnde Transparenz des Motors selbst zum Ärger bereits während des Betriebs des Systems durch die Benutzer bei. Wenn der Fehler aus dem Darm von Wf stammt, ist es nicht immer möglich, 100% herauszufinden, was genau der Grund für den Fehler war. Die geschlossene Quelle und die relative Monstrosität helfen dem Fall nicht weiter. Oft waren Fehlerkorrekturen auf Symptome zurückzuführen.


Aus Gründen der Fairness sollte hier klargestellt werden, dass die oben beschriebenen Probleme uns größtenteils aufgrund der starken Anpassung von Wf geplagt haben. Einer der Leser wird mit Sicherheit sagen, dass wir selbst eine Reihe von Problemen geschaffen und diese dann heldenhaft gelöst haben. Es war von Anfang an notwendig, einen selbst gebauten Motor zu bauen. Im Allgemeinen werden sie recht haben.


Unter dem Strich funktionierte die Lösung stabil genug und ging erfolgreich in Produktion. Aber die Umstellung unserer Produkte auf .Net Core hat uns gezwungen, den WWF aufzugeben und nach einer anderen Geschäftsprozess-Engine zu suchen, weil Bis Mai 2019 wurde die Windows Workflow Foundation nicht auf .Net Core migriert. Als wir nach einer neuen Engine suchten - das Thema eines separaten Artikels - haben wir uns letztendlich für Workflow Core entschieden.


Workflow-Kern


Workflow Core ist eine kostenlose Business Process Engine. Es wird unter der MIT-Lizenz entwickelt, d. H. Es kann sicher in der kommerziellen Entwicklung verwendet werden.


Es wird aktiv von einer Person erledigt, mehrere machen periodisch eine Pull-Anfrage. Es gibt Ports für andere Sprachen (Java, Python und einige mehr).


Der Motor ist so leicht positioniert. Tatsächlich ist dies nur ein Host für die sequentielle Ausführung von Aktionen, die nach Geschäftsregeln gruppiert sind.


Das Projekt hat Wiki-Dokumentation . Leider werden nicht alle Funktionen des Motors beschrieben. Es wird jedoch unverschämt sein, eine vollständige Dokumentation anzufordern - das Open Source-Projekt wird von einem Enthusiasten unterstützt. Daher ist das Wiki für den Einstieg ausreichend.


Standardmäßig wird die Speicherung des Prozessstatus in einem externen Speicher (Persistenzspeicher) unterstützt. Anbieter sind Standard für:


  • Mongodb
  • SQL Server
  • PostgreSQL
  • SQLITE
  • Amazon DynamoDB

Schreiben Sie Ihren Provider ist kein Problem. Wir nehmen die Quellen eines Standards und dienen als Beispiel.


Die horizontale Skalierung wird unterstützt, dh, Sie können die Engine auf mehreren Knoten gleichzeitig ausführen, während ein Speicherpunkt für die Prozesszustände vorhanden ist (ein Persistenzspeicher). In diesem Fall sollte sich die Position der internen Task-Warteschlange der Engine im allgemeinen Speicher befinden (RabbitMQ als Option). Um die Ausführung einer Aufgabe durch mehrere Knoten auszuschließen, wird gleichzeitig ein Sperrmanager bereitgestellt. In Analogie zu externen Speicheranbietern gibt es Standardimplementierungen:


  • Azure-Speichervermietungen
  • Redis
  • AWS DynamoDB
  • SQLServer (in der Quelle gibt es, aber nichts wird in der Dokumentation gesagt)

Das Kennenlernen von Neuem beginnt am einfachsten mit einem Beispiel. Also lass es uns tun. Ich werde den Aufbau eines einfachen Prozesses von Anfang an zusammen mit einer Erklärung beschreiben. Ein Beispiel mag unglaublich einfach erscheinen. Ich stimme zu - es ist einfach. Am Anfang.


Lass uns gehen.


Schritt


Ein Schritt ist ein Schritt in dem Prozess, in dem Aktionen ausgeführt werden. Der gesamte Prozess besteht aus einer Abfolge von Schritten. Ein Schritt kann viele Aktionen ausführen, kann beispielsweise für ein Ereignis von außen wiederholt werden. Es gibt eine Reihe von Schritten, die mit der Logik "out of the box" ausgestattet sind:


  • Warte auf
  • Wenn
  • Während
  • Foreach
  • Verzögerung
  • Parallel
  • Zeitplan
  • Recur

Natürlich können Sie den Prozess bei einigen eingebauten Grundelementen nicht aushalten. Wir brauchen Schritte, die Geschäftsaufgaben erledigen. Legen Sie sie daher vorerst beiseite und unternehmen Sie Schritte mit unserer eigenen Logik. Dazu müssen Sie von der StepBody- Abstraktion erben.


public abstract class StepBody : IStepBody { public abstract ExecutionResult Run(IStepExecutionContext context); } 

Die Run- Methode wird ausgeführt, wenn der Prozess in einen Schritt eintritt. Es ist notwendig, die notwendige Logik darin zu platzieren.


  public abstract class StepBody : IStepBody { public abstract ExecutionResult Run(IStepExecutionContext context); } 

Die Schritte unterstützen die Abhängigkeitsinjektion. Dazu genügt es, sie im selben Container wie die erforderlichen Abhängigkeiten zu registrieren.


Offensichtlich benötigt der Prozess einen eigenen Kontext - einen Ort, an dem Zwischenergebnisse der Ausführung hinzugefügt werden können. Wf core hat einen eigenen Kontext für die Ausführung eines Prozesses, der Informationen über seinen aktuellen Status speichert. Sie können über die Kontextvariable der Methode Run () darauf zugreifen. Zusätzlich zum eingebauten können wir unseren Kontext verwenden.


Wir werden die Möglichkeiten zur Beschreibung und Registrierung des Prozesses im Folgenden genauer analysieren. Zunächst definieren wir einfach eine bestimmte Klasse - den Kontext.


  public class ProcessContext { public int Number1 {get;set;} public int Number2 {get;set;} public string StepResult {get;set;} public ProcessContext() { Number1 = 1; Number2 = 2; } } 

In die Variablen Number schreiben wir Zahlen; in die Variable StepResult - das Ergebnis des Schritts.


Wir haben uns für den Kontext entschieden. Sie können Ihren eigenen Schritt schreiben:


  public class CustomStep : StepBody { private readonly Ilogger _log; public int Input1 { get; set; } public int Input2 { get; set; } public string Action { get; set; } public string Result { get; set; } public CustomStep(Ilogger log) { _log = log; } public override ExecutionResult Run(IStepExecutionContext context) { Result = ”none”; if (Action ==”sum”) { Result = Number1 + Number2; } if (Action ==”dif”){ Result = Number1 - Number2; } return ExecutionResult.Next(); } } 

Die Logik ist denkbar einfach: Zwei Zahlen und der Name der Operation kommen zur Eingabe. Das Ergebnis der Operation wird in die Ausgangsvariable Result geschrieben . Wenn die Operation nicht definiert ist, ist das Ergebnis none .


Wir haben uns für den Kontext entschieden, es gibt einen Schritt mit der Logik, die wir auch brauchen. Jetzt müssen wir unseren Prozess in der Engine registrieren.


Beschreibung des Prozesses. Registrierung im Motor.


Es gibt zwei Möglichkeiten, einen Prozess zu beschreiben. Die erste ist die Beschreibung im Code - der Hardcode.


Der Prozess wird über die flüssige Schnittstelle beschrieben . Es ist erforderlich, von der verallgemeinerten IWorkflow <T> -Schnittstelle zu erben, wobei T die Modellkontextklasse ist. In unserem Fall ist dies ein ProcessContext .


Es sieht so aus:


  public class SimpleWorkflow : IWorkflow<ProcessContext> { public void Build(IWorkflowBuilder<ProcessContext> builder) { //    } public string Id => "SomeWorkflow"; public int Version => 1; } 

Die Beschreibung selbst befindet sich in der Build- Methode. Die Felder ID und Version sind ebenfalls erforderlich. Wf core unterstützt die Prozessversionierung - Sie können n Prozessversionen mit derselben Kennung registrieren. Dies ist praktisch, wenn Sie einen vorhandenen Prozess aktualisieren und gleichzeitig vorhandene Aufgaben "live" geben müssen.


Wir beschreiben einen einfachen Prozess:


  public class SimpleWorkflow : IWorkflow<ProcessContext> { public void Build(IWorkflowBuilder<ProcessContext> builder) { builder.StartWith<CustomStep>() .Input(step => step.Input1, data => data.Number1) .Input(step => step.Input2, data => data.Number2) .Input(step => step.Action, data => “sum”) .Output(data => data.StepResult, step => step.Result) .EndWorkflow(); } public string Id => "SomeWorkflow"; public int Version => 1; } 

Wenn es in eine „menschliche“ Sprache übersetzt wird, sieht es ungefähr so aus: Der Prozess beginnt mit dem CustomStep- Schritt. Der Wert des Schrittfeldes Input1 wird aus dem Kontextfeld Number1 übernommen , der Wert des Schrittfeldes Input2 wird aus dem Kontextfeld Number2 übernommen , das Aktionsfeld ist fest auf den Wert "Summe" codiert . Die Ausgabe aus dem Ergebnisfeld wird in das StepResult- Kontextfeld geschrieben. Schließen Sie den Vorgang ab.


Stimmen Sie zu, der Code erwies sich als sehr lesbar, es ist durchaus möglich, es herauszufinden, auch ohne spezielle Kenntnisse in C #.


Fügen Sie unserem Prozess einen weiteren Schritt hinzu, der das Ergebnis des vorherigen Schritts im Protokoll ausgibt:


  public class CustomStep : StepBody { private readonly Ilogger _log; public string TextToOutput { get; set; } public CustomStep(Ilogger log) { //    _log = log; } public override ExecutionResult Run(IStepExecutionContext context) { _log.Debug(TextToOutput); return ExecutionResult.Next(); } } 

Und aktualisiere den Prozess:


  public class SimpleWorkflow : IWorkflow<ProcessContext> { public void Build(IWorkflowBuilder<ProcessContext> builder) { builder.StartWith<CustomStep>() .Input(step => step.Input1, data => data.Number1) .Input(step => step.Input2, data => data.Number2) .Input(step => step.Action, data => “sum”) .Output(data => data.StepResult, step => step.Result) .Then<OutputStep>.Input(step => step.TextToOutput, data => data.StepResult) .EndWorkflow(); } public string Id => "SomeWorkflow"; public int Version => 2; } 

Nach dem Schritt mit der Additionsoperation folgt nun der Schritt des Ausgebens des Ergebnisses in das Protokoll. Als Eingabe übergeben wir die Variable Result und Context, in die das Ergebnis der Ausführung im letzten Schritt geschrieben wurde. Ich werde mir erlauben, zu behaupten, dass eine solche Beschreibung durch einen Code (Hardcode) in realen Systemen von geringem Nutzen wäre. Es sei denn, für einige Office-Prozesse. Es ist viel interessanter, die Schaltung separat speichern zu können. Zumindest müssen wir das Projekt nicht jedes Mal neu zusammensetzen, wenn wir etwas im Prozess ändern oder ein neues hinzufügen müssen. Wf Core bietet diese Funktion durch Speichern des JSON-Schemas. Wir bauen unser Beispiel weiter aus.


Json-Prozessbeschreibung


Weiterhin werde ich keine Beschreibung durch den Code liefern. Dies ist nicht besonders interessant und bläst den Artikel nur auf.


Wf Core unterstützt die Schema-Beschreibung in JSON. Meiner Meinung nach ist json visueller als xaml (ein gutes Thema für holivar in den Kommentaren :)). Die Dateistruktur ist ziemlich einfach:


 { "Id": "SomeWorkflow", "Version": 1, "DataType": "App.ProcessContext, App", "Steps": [ { /*step1*/ }, { /*step2*/ } ] } 

Das DataType- Feld gibt den vollständig qualifizierten Namen der Kontextklasse und den Namen der Assembly an, in der sie beschrieben wird. Steps speichert eine Sammlung aller Schritte im Prozess. Füllen Sie das Steps- Element aus:


 { "Id": "SomeWorkflow", "Version": 1, "DataType": "App.ProcessContext, App", "Steps": [ { "Id": "Eval", "StepType": "App.CustomStep, App", "NextStepId": "Output", "Inputs": { "Input1": "data.Number1", "Input2": "data.Number2" }, "Outputs": { "StepResult": "step.Result" } }, { "Id": "Output", "StepType": "App.OutputStep, App", "Inputs": { "TextToOutput": "data.StepResult" } } ] } 

Schauen wir uns die Struktur der Schrittbeschreibung über json genauer an.


In den Feldern Id und NextStepId wird die Kennung dieses Schritts gespeichert und angezeigt, welcher Schritt der nächste sein kann. Darüber hinaus ist die Reihenfolge der Elemente der Sammlung unwichtig.


StepType ähnelt dem Feld DataType . Es enthält den vollständig qualifizierten Klassennamen des Schritts (einen Typ, der von StepBody erbt und die Schrittlogik implementiert) und den Namen der Assembly. Interessanter sind die Ein- und Ausgänge . Sie werden in Form eines Mappings festgelegt.


Bei Eingaben ist der Name des json-Elements der Name des Klassenfelds unseres Schritts. Der Wert des Elements ist der Name des Feldes in der Klasse, der Kontext des Prozesses.


Bei Ausgaben hingegen ist der Name des json-Elements der Name des Felds in der Klasse, der Kontext des Prozesses. element value ist der Name des Klassenfeldes unseres Schritts.


Warum werden Kontextfelder über Daten angegeben. {Feldname} und bei Ausgabe Schritt {Feldname} ? Da wf core der Elementwert als C # -Ausdruck ausgeführt wird (es wird die Dynamic Expressions-Bibliothek verwendet). Dies ist eine nützliche Sache, mit deren Hilfe Sie eine Geschäftslogik direkt in das Schema einfügen können, wenn der Architekt natürlich eine solche Schande billigt :).


Wir diversifizieren das Schema mit Standardprimitiven. Fügen Sie einen bedingten If- Schritt hinzu und verarbeiten Sie ein externes Ereignis.


Wenn

Primitive If . Hier beginnen die Schwierigkeiten. Wenn Sie es gewohnt sind, in dieser Notation Prozesse zu bpmn und zu zeichnen, finden Sie eine einfache Einrichtung. Gemäß der Dokumentation wird der Schritt wie folgt beschrieben:


 { "Id": "IfStep", "StepType": "WorkflowCore.Primitives.If, WorkflowCore", "NextStepId": "nextStep", "Inputs": { "Condition": "<<expression to evaluate>>" }, "Do": [ [ { /*do1*/ }, { /*do2*/ } ] ] } 

Gibt es kein Gefühl, dass hier etwas nicht stimmt? Ich habe. Die Stufeneingabe wird auf Bedingung - Ausdruck gesetzt. Als nächstes legen wir die Liste der Schritte innerhalb des Do- Arrays (Aktionen) fest. Also, wo ist der falsche Zweig? Warum gibt es kein Do-Array für False? Eigentlich gibt es. Es versteht sich, dass die Verzweigung Falsch lediglich einen weiteren Durchgang entlang des Prozesses darstellt, d. H. Dem Zeiger in NextStepId folgt. Anfangs war ich deswegen ständig verwirrt. Okay, hat es geklärt. Obwohl nicht. Wenn die Prozessaktionen im Fall von True in " Do" gesetzt werden müssen , ist dies der "schöne" json. Und wenn es diese gibt, wenn sie mit einem Dutzend eingeschlossen sind? Alles wird seitwärts gehen. Sie sagen auch, dass das Schema auf XAML schwer zu lesen ist. Es gibt einen kleinen Hack. Nehmen Sie einfach den Monitor breiter. Es wurde ein wenig oben erwähnt, dass die Reihenfolge der Schritte in der Sammlung keine Rolle spielt, der Übergang folgt den Zeichen. Es kann verwendet werden. Fügen Sie einen weiteren Schritt hinzu:


 { "Id": "Jump", "StepType": "App.JumpStep, App", "NextStepId": "" } 

Ratet mal, wohin ich führe? Richtig, wir führen einen Serviceschritt ein, der den Prozess während der Übertragung zu einem Schritt in NextStepId führt .


Aktualisieren Sie unser Schema:


 { "Id": "SomeWorkflow", "Version": 1, "DataType": "App.ProcessContext, App", "Steps": [ { "Id": "Eval", "StepType": "App.CustomStep, App", "NextStepId": "MyIfStep", "Inputs": { "Input1": "data.Number1", "Input2": "data.Number2" }, "Outputs": { "StepResult": "step.Result" } }, { "Id": "MyIfStep", "StepType": "WorkflowCore.Primitives.If, WorkflowCore", "NextStepId": "OutputEmptyResult", "Inputs": { "Condition": "!String.IsNullOrEmpty(data.StepResult)" }, "Do": [ [ { "Id": "Jump", "StepType": "App.JumpStep, App", "NextStepId": "Output" } ] ] }, { "Id": "Output", "StepType": "App.OutputStep, App", "Inputs": { "TextToOutput": "data.StepResult" } }, { "Id": "OutputEmptyResult", "StepType": "App.OutputStep, App", "Inputs": { "TextToOutput": "\"Empty result\"" } } ] } 

Der If- Schritt prüft, ob das Ergebnis des Eval- Schritts leer ist. Wenn es nicht leer ist, zeigen wir das Ergebnis an, wenn es leer ist, die Meldung " Empty result ". Der Jump- Schritt führt den Prozess zum Output- Schritt, der sich außerhalb der Do- Auflistung befindet. Somit haben wir die „Vertikalität“ des Schemas beibehalten. Auf diese Weise kann man auch n Schritte zurückspringen, d.h. einen Zyklus organisieren. Es gibt eingebaute Grundelemente für Schleifen im wf-Kern, aber sie sind nicht immer praktisch. In bpmn werden beispielsweise Schleifen durch If organisiert.


Verwenden Sie diesen Ansatz oder den Standard, es liegt an Ihnen. Für uns war eine solche Organisation bequemer Schritte.


Warte auf

Mit dem WaitFor-Grundelement kann die Außenwelt den Prozess beeinflussen, wenn er bereits ausgeführt wird. Zum Beispiel, wenn im Stadium des Prozesses eine Genehmigung des weiteren Kurses durch einen Benutzer erforderlich ist. Der Prozess bleibt im WaitFor- Schritt stehen, bis er ein von ihm abonniertes Ereignis empfängt.


Primitive Struktur:


 { "Id": "Wait", "StepType": "WorkflowCore.Primitives.WaitFor, WorkflowCore", "NextStepId": "NextStep", "CancelCondition": "If(cancel==true)", "Inputs": { "EventName": "\"UserAction\"", "EventKey": "\"DoSum\"", "EffectiveDate": "DateTime.Now" } } 

Ich werde die Parameter ein wenig erklären.


CancelCondition - eine Bedingung zum Unterbrechen eines Wartens. Bietet die Möglichkeit, das Warten auf ein Ereignis zu unterbrechen und den Prozess fortzusetzen. Wenn ein Prozess beispielsweise gleichzeitig auf n verschiedene Ereignisse wartet (wf core unterstützt die parallele Ausführung von Schritten), ist es nicht erforderlich, auf das Eintreffen aller Ereignisse zu warten. In diesem Fall hilft uns CancelCondition . Wir fügen den Kontextvariablen ein logisches Flag hinzu und setzen das Flag nach Erhalt des Ereignisses auf true - alle WaitFor- Schritte sind abgeschlossen.


EventName und EventKey - Name und Schlüssel des Ereignisses. Felder werden benötigt, um Ereignisse zu unterscheiden, d. H. In einem realen System mit einer großen Anzahl von gleichzeitig arbeitenden Prozessen. Damit der Motor versteht, welches Ereignis für welchen Prozess und welchen Schritt vorgesehen ist.


EffectiveDate - ein optionales Feld, das einen Ereigniszeitstempel hinzufügt. Dies kann nützlich sein, wenn Sie eine Veranstaltung „in die Zukunft“ veröffentlichen müssen. Damit es sofort veröffentlicht wird, können Sie den Parameter leer lassen oder die aktuelle Uhrzeit einstellen.


Es ist nicht in allen Fällen zweckmäßig, einen separaten Schritt zu unternehmen, um die Reaktionen von außen zu verarbeiten, sondern es ist sogar in der Regel überflüssig. Ein zusätzlicher Schritt kann vermieden werden, indem die Erwartung eines externen Ereignisses und die Logik seiner Verarbeitung zum üblichen Schritt hinzugefügt werden. Wir ergänzen den CustomStep- Schritt, indem wir ein externes Ereignis abonnieren:


  public class CustomStep : StepBody { private readonly Ilogger _log; public string TextToOutput { get; set; } public CustomStep(Ilogger log) { _log = log; } public override ExecutionResult Run(IStepExecutionContext context) { //-  return ExecutionResult.WaitForEvent("eventName", "eventKey", DateTime.Now); } } 

Wir haben die standardmäßige WaitForEvent () - Erweiterungsmethode verwendet. Es akzeptiert die zuvor genannten Parameter EventName , EventKey und EffectiveDate . Nach Abschluss der Logik eines solchen Schritts wartet der Prozess auf das beschriebene Ereignis und ruft zum Zeitpunkt der Veröffentlichung des Ereignisses im Motorbus erneut die Methode Run () auf. In der aktuellen Form können wir jedoch nicht zwischen den Zeitpunkten des ersten Eintritts in den Schritt und dem Eintrag nach dem Ereignis unterscheiden. Aber ich möchte irgendwie die Logik vorher-nachher auf der Stufenebene trennen. Und die EventPublished- Flagge hilft uns dabei. Es befindet sich im allgemeinen Kontext des Prozesses. Sie können es folgendermaßen abrufen:


 var ifEvent=context.ExecutionPointer.EventPublished; 

Anhand dieses Flags können Sie die Logik sicher in vor und nach einem externen Ereignis unterteilen.


Eine wichtige Klarstellung - nach der Vorstellung des Motorherstellers kann ein Schritt nur bei einem Ereignis unterschrieben werden und einmalig darauf reagieren. Für manche Aufgaben ist dies eine sehr unangenehme Einschränkung. Wir mussten sogar den Motor „fertig machen“, um von dieser Nuance loszukommen. Wir werden ihre Beschreibung in diesem Artikel überspringen, sonst wird der Artikel niemals enden :). Komplexere Verwendungspraktiken und Beispiele für Verbesserungen werden in den folgenden Artikeln behandelt.


Registrierungsvorgang im Motor. Veröffentlichung der Veranstaltung im Bus.


Also, mit der Umsetzung der Logik der Schritte und Beschreibungen des Prozesses herausgefunden. Was bleibt, ist das Wichtigste, ohne das der Prozess nicht funktioniert - die Beschreibung muss registriert werden.


Wir werden die Standard-Erweiterungsmethode AddWorkflow () verwenden, die ihre Abhängigkeiten in unseren IoC-Container stellt.


Es sieht so aus:


 public static IServiceCollection AddWorkflow(this IServiceCollection services, Action<WorkflowOptions> setupAction = null) 

IServiceCollection - Schnittstelle - ein Vertrag für eine Sammlung von Dienstbeschreibungen. Er wohnt in DI von Microsoft (mehr dazu finden Sie hier )


WorkflowOptions - Grundeinstellungen der Engine. Es ist nicht notwendig, diese selbst einzustellen, Standardwerte sind für den Erstkontakt durchaus akzeptabel. Wir gehen weiter.


Wenn der Vorgang in Code beschrieben wurde, erfolgt die Registrierung folgendermaßen:


 var host = _serviceProvider.GetService<IWorkflowHost>(); host.RegisterWorkflow<SomeWorkflow, ProcessContext>(); 

Wenn der Prozess über json beschrieben wird, muss er wie folgt registriert werden (natürlich muss die json-Beschreibung vom Speicherort vorgeladen werden):


 var host = _serviceProvider.GetService<IWorkflowHost>(); var definitionLoader = _serviceProvider.GetService<IDefinitionLoader>(); var definition = loader.LoadDefinition({*json  *}); 

Außerdem ist der Code für beide Optionen gleich:


 host.Start(); //      host.StartWorkflow(definitionId, version, context); //      /// host.Stop(); / /     

Der Parameter definitionId ist die Prozesskennung. Was ist in das Feld Prozess-ID geschrieben. In diesem Fall ist id = SomeWorkflow .


Der version- Parameter gibt an, welche Version des Prozesses ausgeführt werden soll. Die Engine bietet die Möglichkeit, sofort n Prozessversionen mit einer Kennung zu registrieren. Dies ist praktisch, wenn Sie Änderungen an der Beschreibung des Prozesses vornehmen müssen, ohne bereits ausgeführte Aufgaben zu unterbrechen - neue Aufgaben werden gemäß der neuen Version erstellt, alte Aufgaben werden stillschweigend von der alten übernommen.


Der Kontextparameter ist eine Instanz des Prozesskontexts.


Die Methoden host.Start () und host.Stop () starten und stoppen das Prozesshosting. Wenn in der Anwendung der Start von Prozessen eine angewendete Aufgabe ist und regelmäßig ausgeführt wird, sollte das Hosting gestoppt werden. Wenn die Anwendung den Schwerpunkt auf die Implementierung verschiedener Prozesse legt, kann das Hosting nicht gestoppt werden.


Es gibt eine Methode zum Senden von Nachrichten von der Außenwelt an den Motorbus, der sie dann unter den Teilnehmern verteilt:


 Task PublishEvent(string eventName, string eventKey, object eventData, DateTime effectiveDate = null); 

Die Beschreibung der Parameter war im Artikel höher ( siehe WaitFor-Grundelement ).


Fazit


Wir sind definitiv Risiken eingegangen, als wir uns für das Workflow Core - OpenSource-Projekt entschieden haben, das von einer Person aktiv entwickelt wird, und das auch bei sehr schlechter Dokumentation. Und Sie werden höchstwahrscheinlich keine echten Praktiken für die Verwendung von wf core in Produktionssystemen finden (außer unseren). Nachdem wir eine separate Abstraktionsebene ausgewählt hatten, versicherten wir uns natürlich gegen einen Misserfolg und die Notwendigkeit, zum Beispiel schnell zum WWF zurückzukehren, oder gegen eine selbstgeschriebene Lösung, aber alles verlief recht gut und der Misserfolg kam nicht.


Die Umstellung auf die OpenSource Workflow Core-Engine löste eine Reihe von Problemen, die uns daran hinderten, friedlich im WWF zu leben. Das wichtigste davon ist natürlich die Unterstützung von .Net Core und das Fehlen eines solchen WWF, auch wenn dies geplant ist.


Es folgt die Open Source. Wenn Sie mit dem WWF zusammenarbeiten und verschiedene Fehler aus dem Darm bekommen, wäre es sehr hilfreich, zumindest die Quelle lesen zu können. Ganz zu schweigen davon, etwas an ihnen zu ändern. Hier mit Workflow Core völlige Freiheit (inklusive Lizenzierung - MIT). Wenn plötzlich ein Fehler im Inneren des Motors auftritt, laden Sie einfach die Quellen von github herunter und suchen Sie in aller Ruhe nach der Ursache für das Auftreten. Ja, allein die Möglichkeit, die Engine im Debug-Modus mit Haltepunkten zu starten, erleichtert den Vorgang erheblich.


Natürlich hat Workflow Core zur Lösung einiger Probleme neue mitgebracht. Wir mussten eine erhebliche Menge an Änderungen am Motorkern vornehmen. Aber. Die Arbeit am "Finishing" selbst kostet weniger Zeit als die Entwicklung eines eigenen Motors von Grund auf. Die endgültige Lösung war in Bezug auf Geschwindigkeit und Stabilität durchaus akzeptabel, sodass wir die Probleme mit dem Motor vergessen und uns auf die Entwicklung des Geschäftswerts des Produkts konzentrieren konnten.


PS Wenn sich das Thema als interessant herausstellt, wird es weitere Artikel zu wf core geben, mit einer tieferen Analyse der Engine und Lösungen für komplexe Geschäftsprobleme.

Source: https://habr.com/ru/post/de475902/


All Articles