[PL] Pomysł na Workflow

Ameryki nie odkryję jak stwierdzę, że Workflow Foundation to potężna machina.
Temat tłuczemy już od jakiegoś czasu i w Polsce udało się już parę ciekawych przykładów wykorzystania Workflow Foundation pokazać choćby na premierze Heroes Happen Here.

Gdy poznajemy Workflow Foundation to pierwsze pytanie dla praktyka jakie się pojawia to jak zaimplementować Workflow elastycznie. W ramach paczki mamy narzędzia do modelowania, uruchamiania i usług uruchomieniowych. Procesy nigdy nie są stałe więc w kwestii tego pierwszego chciałbym móc łatwo zmieniać już raz stworzone procesy i/lub dodawać nowe.

Tutaj znowu mamy parę elementów, na które warto zwrócić uwagę. Dzieląc na warstwy łatwo wyodrębnimy część prezentacyjną, czyli jakiś ludzki edytor, który moglibyśmy ofiarować nie tylko naszym klientom, ale i naszym konsultantom i wdrożeniowcom przygotowującym system do realnej pracy. Druga warstwa to sama organizacja dynamicznego wgrywania przepływów, ich organizacji w systemie co ma wpływ na trzecią czyli sam proces migracji pomiędzy nazwijmy to wersją deweloperską (rozwijaną) oraz produkcyjnym status quo.

Wchodząc głebiej w temat pewnie jeszcze bardziej byśmy go sobie skomplikowali, aby na końcu dojść do wniosku, że nawet z tak potężnym frameworkiem jak Workflow Foundation temat przepływów zrealizowany poważnie i z architektonicznym rozmachem, to nie taka trywialna sprawa.

Uproszczając trochę dam wam zalążek czegoś co można wydaje mi się z sensem wykorzystać. Dotyczy głównie tej drugiej warstwy czyli organizacji przepływów (tworzenie/modelowanie, wgrywanie i dynamiczne uruchamianie).

Gdy w Visual Studio spojrzymy na diagram Workflow Foundation to możemy wyodrębnić trzy podejścia do samego modelu:
1) 100% XML w postaci odłamu języka Xaml czyli xoml (i ew. kodem osadzonym w znaczniku <x:code>)
2) XML + kod (gdzie mamy plik xoml z definicją oraz plik xoml.cs z kodem)
3) 100% kod i generowanie przepływu z "palca"

WorkflowRuntime z każdym z tych typów radzi sobie wyśmienicie poprzez:

WorkflowRuntime.CreateWorkflow();

W standardowej sytuacji, gdy mamy twardo skodowany workflow w Visual Studio to powinien on mieć twardy typ po którym tworzymy instancję ala:

WorkflowRuntime.CreateWorkflow(typeof(MySuperWorkflow));

Nie będę opowiadał o workflow tworzonym 100% z poziomu kodu ponieważ nie jest to najlatwiejsza droga do stworzenia sympatycznych narzedzi modelowania dla użytkowników końcowych. Wydaje mi się, że standardowa kontrolka do modelowania w Visual Studio oraz klasy służące do serializacji Workflow same w sobie narzucają zainteresowanie się plikami *.xoml. Chyba, że ktoś lubi sobie narzucać więcej pracy niż potrzeba. Ja nie.

Więc mamy pliki .xoml lub ewentualnie pliki .xoml.cs. Przy rozdzieleniu istotne jest aby pamiętać o zgodności sygnatur pomiędzy plikami. Mając takowe pliki możemy próbować surowo wgrać je ze "źródeł". Służy do tego alternatywna odmiana metody CreateWorkflow, której parametrem wejściowym jest XmlReader. Wtedy zamiast powyższego w skrócie uzyskamy coś ala:

XmlReader xomlDefinition = XmlReader.Create(@"MySuperWorkflow.xoml");
WorkflowRuntime.CreateWorkflow(xomlDefinition);

Jeśli wszystko się zgadza nasza instancja powinna się bez problemu utworzyć. Tutaj jednak pojawia się dużo problemów związanych z walidacją. Otóż przy wykonaniu tej metody Workflow sobie w tle wszystko kompiluje do .NET Framework i weryfikuje zgodność wszystkich potrzebnych zależności. Wierzę, że przy pierwszej próbie wykorzystania XmlReadera wielu z was dostanie wiele razy zwrot w postaci WorkflowValidationFailedException, tam najczęściej zdarzają się błedy, które w detalach tej klasy są w miarę przejrzyście opisane.

Od razu polecam do naszych powyższych dwóch linijek dodać try catch analogiczny do poniższego:

catch (WorkflowValidationFailedException e)
{
     Console.ForegroundColor = ConsoleColor.Red;
     Console.Write("ERROR: ");
     Console.ResetColor();
     Console.WriteLine(e.Message);
     Console.WriteLine(" # Validations failed: " + e.Errors.Count + " found.");
     foreach (ValidationError err in e.Errors)
     {
         Console.WriteLine(" * E" + err.ErrorNumber + ": " + err.ErrorText);
     }
}

Zwracane komunikaty warto uzupełnić o lekturę opisującą każdy błąd. Naprawa nie jest trywialna, zwłaszcza przy skompilikowanych referencjach. Dlatego proponuję prostszą drogę - kompilacja.

Kompilacja, która i tak się odbywa, a my możemy mieć przynajmniej nad nią kontrolę w procesie edycji, modelowania, wbudowywania w system i wykonywania. Jak to miało by wyglądać w szczególe? Już opisuję:

Mamy nasz workflow w pliku xoml z ewentualnym dodatkiem w postaci partial class w xoml.cs. I teraz parę linijek kodu z komentarzem:

        WorkflowCompiler compiler = new WorkflowCompiler();                

Tutaj stworzyłem obiekt kompilatora Workflow

        WorkflowCompilerParameters parms =
            new WorkflowCompilerParameters(
                new string[] { @"CustomRefAssembly01.dll", @"CustomRefAssembly02.dll"
                },"CachedDynamicWorkflows.dll", false);

Tutaj stworzyłem parametry kompilacji, która zawiera informację o referencjach (CustomRefAssembly*.dll), nazwę stworzonej dll z skompilowanymi workflowami, oraz czy to ma być wersja release czy debug.

        WorkflowCompilerResults res = compiler.Compile(parms,
                new string [] { @"XmlOnlyWorkflow.xoml" , //tylko xoml
                                @"XmlAndCodeWorkflow.xoml", //wersja z odseparowanym kodem
                                @"XmlAndCodeWorkflow.xoml.cs", //i partial class
                } );

No i oczywiście powyżej kompilacja wszystkich plików, które przekazuję w tablicy i wynikowo mają być zapisane w tej jednej pojedynczej DLLce z dodatkowym assembly. Teraz poniżej czas na sprawdzenie wyników:

if (res.Errors.Count == 0)
{
    instance = runtime.CreateWorkflow(res.CompiledAssembly.GetTypes()[0]);
    // lub może bardziej czytelnie
    instance = runtime.CreateWorkflow(
               res.CompiledAssembly.GetType("MySuperWorkflow"));
}
else
{
    Console.WriteLine("Workflow compilation failed! Details:");
    Console.WriteLine("Errors = " + res.Errors.Count);
    foreach (WorkflowCompilerError compilerErr in res.Errors)
    {
        Console.WriteLine("E" + compilerErr.ErrorNumber
                     + ": " + compilerErr.ErrorText);
    }
    Console.WriteLine("[End of Compilation]");
}

Generalnie WorkflowCompilerResults.Errors zawiera komplet informacji o udanej/nieudanej kompilacji. Jeśli wszystko się powiodło to w WorkflowCompilerResults.CompiledAssembly mamy skompilowany obiekt System.Reflection.Assembly, który jest już załadowany i gotowy do wykorzystania.

Teraz co w powyższym takiego pięknego jeśli miałbym spojrzeć na to bardziej architektonicznie. Otóż wyobraźmy sobie nasz system, który tworzymy. Powiedzmy jakiś ERPik. Rozdzielamy go na dwie części. Pierwsza to faktyczna warstwa uruchomieniowa, z która ma do czynienia nasz kochany użytkownik. Druga to część administracyjno-deweloperska.

W tym panelu administracyjnym mamy repozytorium wszystkich workflow z referencją do źródeł, które gdzieś przetrzymujemy. Każdy z nich możemy edytować i dodawać kod .NET Framework w locie. Dodając do kompilacji odpowiednie referencje w edytorze skryptów do Workflow można dodać referencję do jakiegoś namespace'a z obiektami biznesowymi, do których nasz workflow powiniene mieć dostęp w swoim kodzie. Na koniec możemy kompilować cały system, wrzucić gdzieś z boku dllkę i zacząć testy całego środowiska a jak wszystko jest okay to dllka komponent potem odblokowujemy użytkownikom. Pakiet źródeł można przetrzymywać w jakimś jednym pliku archiwum lub luźno, z boku wersjonować, tworzyć swoje standardy i modyfikacje i mieć przekompilowane wersje z procesami dostosowanymi do poszcezgólnych potrzeb konkretnych grup klientów, z którymi nauczyliśmy się mieć do czynienia. Wszystko bez ruszania podstawowego kernela, który można sobie na boku dalej rozwijać.

Wyobrażam sobie taki skompilowany pakiet modeli do workflow w czymś co umownie nazywam Workflow Server. Tam w ramach odpowiednio zaimplementowanego procesu wrzucałbym nową wersję modeli wymagającą zatrzymania/pauzy serwisu, uwzględnienia przepływów już uruchomionych i zamrożonych wersją właśnie przez swój status. Sprawa znowu się zaczyna kompilować, ale jak widzicie sam fakt dynamicznie wgranego modelu workflow w Workflow Foundation to sprawa trywialna wymagająca dosłownie paru linijek kodu. Nic odkrywczego niby, w końcu pewnego rodzaju nakładka na System.Reflection oraz System.CodeDom, ale zawsze jest to jakiś pomysł na workflow w praktyce. No.. przynajmniej zalążek.

Technorati Tagi: Polish posts,coding,.NET Framework,Workflow Foundation