workflow címkéhez tartozó bejegyzések

Workflow Foundation – miért kellene, hogy érdekeljen?

Egyre gyakrabban találkozom azzal a kérdéssel, hogy mikor érdemes bevetni egy alkalmazás megvalósításához a Windows Workflow Foundationt és egyáltalán mi lesz jobb attól, ha egy aránylag komplex komponenssel bővítjük a rendszerünket?

Első ránézésre a WF bizony egy összetett, vízfejű szörnyetegnek tűnik, foglalkozni kell vele, mire az ember megérti, hogy melyik része mire jó. Egyszerű problémák megoldásához nagy overheadként látja az ember és nehéz átlátni, hogy egy konkrét esetben nyerünk-e vele vagy épp kockára tesszük a projekt sikerét azáltal, hogy egy újabb technológiát próbálunk összereszelni a többivel.

Tavaly volt szerencsém részt venni a barcelonai TechEd Developers konferencián, ahol az egyik szünetben ezekről a kérdésekről beszélgettem Matt Winklerrel, a WF csapat egyik szakértőjével. Mivel ezek a kérdések újra és újra előkerülnek, úgy döntöttem, hogy megosztom a videót:

Matt Winkler interjú

A videó első fele a Workflow Foundationről szól, a beszélgetés második felében pedig a .NET Framework 3.5 verziójában megjelent újdonságokról, elsősorban a WF és a Windows Communication Foundation integrációjáról kérdeztem Mattet. (Ez utóbbiról egyébként már volt ezen a blogon egy részletesebb cikk.)

A beszélgetés mindössze 10 perc, aki most kezd a témával foglalkozni, annak bevezetőként tudom ajánlani.

 

Reklámok

Saját activity SharePoint Designerbe

A SharePoint Designer workflow tervezőjével sok feladatot könnyen megoldhatunk, bonyolultabb esetekben azonban hamar kifuthatunk a funkciókból. Szerencsére a lista bővíthető, be lehet varrni saját activity-t az eszközbe.

Ehhez csak a következőket kell tennünk:

1. Készítsük el a szokásos módon a saját activity komponensünket egy Activity Library típusú projektben.

2. Lássuk el a szerelvényt erős névvel és telepítsük a központi szerelvénytárba (GAC).

3. Keressük meg a 12TEMPLATE1033WorkflowWSS.ACTIONS fájlt, készítünk róla másolatot akármi.ACTIONS néven (a kiterjesztés fontos) ugyanabba a mappába és egy Action elemben írjuk le az activity-nket. Például egy mappa másolását elvégző saját FolderCopyActivity így írható le:

    <?xml version="1.0" encoding="utf-8"?>
    <WorkflowInfo>
        <Actions Sequential="then" Parallel="and">
            <Action Name="Mappa másolása"
                ClassName="MyActivityLib.FolderCopyActivity"
                Assembly="MyActivityLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e1b2225a7cb586c4"
                Category="Saját"
                AppliesTo="all">
                <RuleDesigner Sentence="A %1 mappa másolása a %2 mappába">
                    <FieldBind Field="SourceFolder" Text="forrás" DesignerType="TextArea" Id="1" />
                    <FieldBind Field="TargetFolder" Text="cél" DesignerType="TextArea" Id="2" />
                </RuleDesigner>
                <Parameters>
                    <Parameter Name="SourceFolder" Type="System.String, mscorlib" Direction="In" />
                    <Parameter Name="TargetFolder" Type="System.String, mscorlib" Direction="In" />
                </Parameters>
            </Action>
        </Actions>
    </WorkflowInfo>

Érdemes megfigyelni, hogy a Sentence attribútumban egy értelmes mondatot kell megadnunk, ami meg fog jelenni a felhasználói felületen. A % jellel jelölt paraméterek megjelenését a FieldBind elemekben tudjuk részletezni és a Parameters elemben tudjuk azokat az Activity osztály dependency property-jeihez kapcsolni. Célszerű odafigyelni a DesignerType attribútumra, hogy olyat válasszunk, ami később megkönnyíti a paraméter beállítását. A lehetséges értékek listája az MSDN oldalain megtekinthető, ahogy az ACTIONS fájl teljes sémája is.

4. A webalkalmazásunkhoz tartozó web.config fájlban regisztráljuk a típust authorizedType-ként. Ez lényegében olyan, mint a SafeControl szekció, csak éppen a System.Workflow.ComponentModel.WorkflowCompiler elemben található:

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <configuration>
        <!-- ... -->
        <System.Workflow.ComponentModel.WorkflowCompiler>
            <authorizedTypes>
                <!-- ... -->
                <authorizedType 
                    Assembly="MyActivityLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e1b2225a7cb586c4" 
                    Namespace="MyActivityLib" 
                    TypeName="*" 
                    Authorized="True" />
            </authorizedTypes>
        </System.Workflow.ComponentModel.WorkflowCompiler>
    </configuration>

5. Nyissunk rá a webhelyre SharePoint Designerben és frissítsük a nézetet, aminek hatására az eszköz letölti a szerverről az ACTIONS fájlt és meg fog jelenni az új activity-nk workflow tervezése közben.

Adatok bekérése SharePoint Designeres workflowban

SharePointos workflow fejlesztéshez igen jól használható eszköz a SharePoint Designer (SPD), de tagadhatatlan, hogy néha nehéz elsőre eligazodni a felhasználói felületen. Az egyik ilyen “néha” akkor jön velünk szembe, amikor a folyamat futása során adatokat szeretnénk bekérni a felhasználóktól.

Az adatok bekéréséhez és a felhasználókkal történő kommunikációhoz a SharePointos workflow-k a feladat listákat használják. A SharePoint Designerben az Adatgyűjtés a felhasználóról (angolul Collect Data from a User – a webhely nyelvétől függően) műveletet használhatjuk arra, hogy a feladat listába újabb elemet tegyünk:

SharePoint Designer: Adatgyűjtés a felhasználról

Az Adatok (angolul data) linkre kattintva egy Custom Task Wizard varázsló segítségével adhatjuk meg, hogy milyen információkat szeretnénk megtudni a felhasználótól. Az első kérdésnél egy nevet és egy leírást kell megadnunk:

Custom Task Wizard: név és leírás

A varázsló következő lépésében pedig a bekérendő adatokat definiálhatjuk:

Custom Task Wizard: mezők

Érdemes tudnunk, hogy mi történik a háttérben:

  • A SPD létrehoz egy új tartalomtípust azzal a névvel, amit itt megadunk. A tartalomtípus a Workflow Task típusból származik, tehát 0x01080100 kezdetű azonosítója lesz. Az új tartalomtípus azon a webhelyen jön létre, ahol a workflow-t készítjük.
  • A SPD hozzárendeli ezt a tartalomtípust a folyamathoz rendelt feladatlistához.
  • A SPD létrehoz egy ASP.NET alapú űrlapot a http://szervernév/webhelynév/Workflows/folyamatneve/űrlapneve.aspx címen. Ha barátságos folyamat- és űrlap neveket használunk, az URL gusztustalan lesz. (Persze esélyünk sincs barátságos URL-re, mert van pár tucat karakternyi query string a végén.) Az űrlapot hozzárendeli a tartalomtípushoz szerkesztési űrlapként (tehát csak edit módban fog megjelenni, display módban nem). Ezen az űrlapon statikus szövegként, Title és Description címen megjelenik a varázslóban megadott név és leírás:

Feladat szerkesztő űrlap

Miután így definiáltuk az adatokat és az azokat bekérő felületet, és még azt is megadtuk, hogy ki nyerte meg ezt a feladatot, végül egy változót kell megadnunk:

SPD_WFD_04-Variable

A trükk az, hogy itt egy ID típusú változót kell megadnunk, azaz a művelet eredményeként nem közvetlenül az adatokat kapjuk meg, hanem azt, hogy a SharePoint a folyamathoz kapcsolt feladat listában melyik listaelembe mentette el a felhasználó válaszait.

Persze ilyenkor felmerül a kérdés, hogy ezt hogyan tudom visszanyerni? Például ha naplózni szeretném a Vélemény mezőt, így kell felkonfigurálni a Define Workflow Lookup ablakot:

Define Workflow Lookup

Kis segítség a beállítások olvasásához: ha SQL lenne, ezt írhatnánk:

SELECT Vélemény

FROM Feladatok

WHERE Szám = VéleményezésiFeladatID

Általánosabban:

SELECT LookupDetails_Field

FROM LookupDetails_Source

WHERE FindTheListItem_Field = FindTheListItem_Value

A lookup eredménye olyan típusú lesz, mint a felső Field mezőben megadott oszlop.

 

Workflow Services .NET 3.5-ben: SendActivity és ReceiveActivity

Ha valaki alaposan körülnéz a .NET Framework 3.5-ben workflow újdonságok után kutatva, aligha talál túl sok mindent. Mindössze két új activity van, a Send és a Receive activity-k, melyek azonban nagyon hasznosak, hiszen a korábban már annyit szidott ExternalDataExchangeService használatát tehetik feleslegessé. Előnyük, hogy a Windows Communication Foundationre épülnek, tehát ha tudunk WCF-ül, akkor nem fog gondot okozni a használatuk és valóban egyszerűbb lesz az életünk, mint az EDES alapú kommunikációval. Ugyanez a hátrányuk is, tudnunk kell WCF-ül, ha használni akarjuk őket, és így már nem csak a WF-hez, de a WCF-hez is értenünk kell; két, önmagában sem kicsi és egyszerű technológiában kell otthon lennünk.

Nézzünk egy egyszerű (??) példát a két új activity használatára.

WCF szolgáltatás készítése

Létrehoztam egy új WCF Service Library típusú projektet MyServiceLib néven, majd miután kitakarítottam belőle a generált kódot, definiáltam egy ICalcService nevű WCF szolgáltatás interfészt (tipp: Add New Item – WCF Service), még pedig így:

    namespace MyServiceLib
    {
        [ServiceContract]
        public interface ICalcService
        {
            [OperationContract]
            int Add( int first, int second );
        }
    }

A felhasznált attribútumok a WCF névterében, a System.ServiceModel névtérben találhatóak.

Az interfészt természetesen implementáltam is egy CalcService nevű osztályban, elég lényegre törően:

    public class CalcService : ICalcService
    {
        public int Add( int first, int second )
        {
            Console.WriteLine( "CalcService.Add( {0}, {1} )", first, second );
            return first + second;
        }
    }

Nincs az a WCF alkalmazás, amihez ne kellene konfig fájlt matatni, így hát elővettem a generált app.config fájlt és kigyomláltam belőle a Service1 nyomait a system.servicemodel szekcióban. Végül ez maradt:

    <system.serviceModel>
      <services>
        <service name="MyServiceLib.CalcService" behaviorConfiguration="MyServiceLib.CalcServiceBehavior">
          <host>
            <baseAddresses>
              <add baseAddress = "http://localhost:9091/CalcService/" />
            </baseAddresses>
          </host>
          <endpoint address="" binding="wsHttpBinding" contract="MyServiceLib.ICalcService" />
          <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
        </service>
      </services>

      <behaviors>
        <serviceBehaviors>
          <behavior name="MyServiceLib.CalcServiceBehavior">
            <serviceMetadata httpGetEnabled="True"/>
          </behavior>
        </serviceBehaviors>
      </behaviors>
    </system.serviceModel>

Defeiniáltam tehát két endpointot, egyet a szolgáltatás meghívásához, egyet pedig a szolgáltatás leírásának lekérdezéséhez. Jegyezzük meg, hogy a 9091-es porton figyel a szolgáltatás. A Visual Studio 2008 egyik újdonsága, hogy bár ez egy library típusú projekt, mégis elindíthatjuk: létezik ugyanis egy WCF Service Host és egy WCF Test Client alkalmazás, amit elindít a Studio és abban azonnal tesztelhetjük a szolgáltatásunkat anélkül, hogy hoszt alkalmazást készítenénk hozzá.

Megjegyzés: normális esetben ezek hibát fognak jelezni, ugyanis az URL névterek matatása csak rendszergazdáknak engedélyezett és ugyebár nem vagyunk rendszergazdák a gépünkön. Vagy olvassuk el a hibaüzenetben hivatkozott idevágó MSDN cikket, vagy "Végre!" felkiáltással/anyázva/sajnálkozva/siránkozva indítsuk el a Studiot adminként.

Ha sikerrel jártunk, működni fog a szolgáltatásunk a teszt kliensben:

WCF Test Client

WCF hoszt alkalmazás készítése

Mi azért ne elégedjünk meg ennyivel, készítsünk egy saját konzolos hoszt alkalmazást MyServiceHost néven. A VS által generált konzol projekthez adjunk referenciát a szolgáltatás projektünkre és a System.ServiceModel szerelvényre. Ebben a szerelvényben, és egyben névtérben található a hosztolásért felelős ServiceHost osztály.

A hosztolás lényegében a ServiceHost példányosításából és az Open metódus meghívásából áll:

    static void Main( string[] args )
    {
        ServiceHost host = new ServiceHost( typeof( MyServiceLib.CalcService ) );
        host.Open();
        Console.WriteLine( "Service host fut..." );
        Console.ReadLine();
        host.Close();
    }

Most jön a dolog neheze, ugyanis a szolgáltatás meghívásához szükséges paraméterek konfig fájlból jönnek. A lényeg az endpoint, amiben három dolgot kell definiálnunk: address, binding, contract, a WCF ABC-je.

Szerencsés helyzetben vagyunk, a library projektnél már mindezt bekonfiguráltuk, másoljuk át azt az app.configot a konzol projekthez, ugyanis a .NET még mindig nem támogatja DLL-ek esetén a konfig fájlok használatát.

Ezek után a MyServiceHost projektnek gond nélkül fordulnia és futnia kell. Miközben fut, böngészőben megnézhetjük, mi látszik a http://localhost:9091/CalcService/ címen. Itt azonban meghívni nem tudjuk, klienst kell tehát készítenünk.

WCF kliens készítése

A kliens is egy konzol alkalmazás legyen, MyClient néven. Ha fut a service, akkor a Solution Explorerben a jobb gombbal kattintva találhatunk egy Add Service Reference menüpontot:

 Add Service Reference

A megjelenő ablakban a Discover gombra kattintva a Studio könnyen megtalálja a solutionben lévő szolgáltatásokat. Az ablak alján megadhatjuk, hogy az eszköz a MyServices névtérbe generálja a proxy osztályokat.

Egy using MyClient.MyServices; sor után, már bátran meghívhatjuk a szolgáltatásunkat a webszolgáltatásoknál már megszokott egyszerűséggel:

    Console.WriteLine( "Kliens elindult..." );
    CalcServiceClient service = new CalcServiceClient();
    Console.WriteLine( "Eredmény: {0}", service.Add( 5, 6 ) );
    Console.ReadLine();

Ha mindent jól csináltunk, akkor a varázsló generált nekünk egy nagy halom XML-t az app.config fájlba, ez alapján tudja a proxy, hogy hol van a szolgáltatás és hogyan kell meghívni. Le legyünk restek kitakarítani a szemetet, ennyi csak a lényeg, megint csak az ABC:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
        <system.serviceModel>
            <client>
                <endpoint
                        address="http://localhost:9091/CalcService/"
                        binding="wsHttpBinding"
                        contract="MyServices.ICalcService" />
            </client>
        </system.serviceModel>
    </configuration>

Hol van ebben a workflow?

Eddig tehát készítettünk egy egyszerű WCF szolgáltatást, hozzá hoszt alkalmazást és klienst. Hol van itt a Workflow Foundation szerepe?

A WF tipikusan akkor jöhet a képbe, ha hosszú ideig futó üzleti logikánk van, amit távolról szeretnénk elérni. A "hosszú ideig fut" feltételnek a WF, a "távolról elérni" feltételnek pedig a WCF örül. A kettőt egy processzben használni persze nem triviális, itt sem készítünk bonyolult példát, csak ugyanezt a számológépet workflow alapokon.

Workflow készítése ReceiveActivity használatával

Adjunk a solutionünkhöz egy Sequential Workflow Service Library típusú projektet MyWorkflowLib néven. Töröljük ki a Workflow1 és IWorkflow1 fájlokat és helyettük adjunk hozzá a projekthez egy szekvenciális workflowt CalcWorkflow néven. Ebben a workflowban a korábbiakhoz hasonlóan egy összeadást fogunk megvalósítani, ezúttal workflowval.

A Toolbox Windows Workflow v3.5 csoportjában találjuk a ReceiveActivity-t, húzzuk rá a folyamatra design nézetben. Ez az activity képes arra, hogy egy workflow-t WCF szolgáltatásként publikáljon a külvilág felé. Ehhez természetesen szükség van egy kommunikációs interfészre, a contractra.

Két módon járhatunk el:

  • Contract first: már megvan a kommunikációs interfész definíciója, ezt használjuk fel.
  • Workflow first: előbb megépítjük a folyamatot és közben definiáljuk a kommunikációs interfészt.

Mivel mi pont ugyanolyan logikájú folyamatot szeretnénk építeni, mint korábban a CalcService esetén, ezért a contract first utat választjuk és felhasználjuk a korábban létrehozott ICalcService interfészt. De csak az interfészt, a CalcService osztályt nem, hiszen pont azt váltjuk ki egy workflow-val. Az interfész azonos, de azúttal nem egy egyszerű osztály, hanem egy workflow implementálja. Hogy el tudjuk érni az interfészt, adjunk referenciát a MyServiceLib projektre.

A feldobott ReceiveActivity tulajdonság lapján állítsuk a CanCreateInstance tulajdonságot true értékre, jelezvén, hogy a hoszt alkalmazás létrehozhat egy új workflow példányt, ha ilyen (ezt később definiáljuk) üzenet érkezik. A contractot a ServiceOperationInfo tulajdonságnál definiálhatjuk:

ServiceOperationInfo tulajdonság beállítása

Az Add Contract gombra kattintva itt építhetnénk meg a kommunikációs interfészt (workflow first), helyette inkább kattintsunk az Import gombra és válasszuk ki a MyServiceLib projektből az ICalcService interfészt (contract first).

Definiáljunk a CalcWorkflow osztályban tulajdonságokat a bemeneti paraméterek és az eredmény tárolására:

    public int First { get; set; }
    public int Second { get; set; }
    public int Result { get; set; }

Az így létrehozott változókat köthetjük a ReceiveActivity tulajdonságlapján az Add metódus paramétereihez és visszatérési értékéhez:

A ReceiveActivity beállításai 

A ReceiveActivity-be tegyünk egy CodeActivity-t, nevezzük add-nak és írjuk meg a "nagy bonyolultságú" és "hosszú ideig futó" üzleti logikánkat:

    private void add_ExecuteCode( object sender, EventArgs e )
    {
      Console.WriteLine( "CalcWorkflow.add_ExecuteCode: First={0}, Second={1}", First, Second );
      this.Result = this.First + this.Second;
    }

Workflow hoszt készítése

Elkészültünk a folyamattal, amit persze valahol hosztolni kell. Nem használhatjuk a korábbi hoszt kódunkat, hiszen az közvetlenül hosztolta a WCF szolgáltatásunkat, most pedig egy workflowt és ezzel az egész workflow runtime-ot kell hosztolnunk.

Legyen ez is egyszerű konzol alkalmazás, MyWorkflowHost néven, és adjuk hozzá a szükséges szerelvény referenciákat: kell a System.Workflow.*, ahogy azt már WF 3.0-ban megszoktuk, de mell még a System.ServiceModel és – itt a lényeg – a System.WorkflowServices is. Természetesen szükség lesz a MyWorkflowLibre is.

A hosztoláshoz ezúttal nem a korábban megszokott WorkflowRuntime vagy ServiceHost osztályt, hanem egy új, WCF-WF specifikus WorkflowServiceHost osztályt használunk. Ezt kell megpéldányosítani és meghívni rajta az Open metódust. Ha fel akarunk iratkozni a WorkflowRuntime eseményeire, akkor kicsit trükközni kell, ezért álljon itt a teljes kód:

    static void Main( string[] args )
    {
      WorkflowServiceHost host = new WorkflowServiceHost( typeof( MyWorkflowLib.CalcWorkflow ) );

      WorkflowRuntime runtime = 
        host.Description.Behaviors.Find<WorkflowRuntimeBehavior>().WorkflowRuntime;

      runtime.WorkflowCreated += delegate( object sender, WorkflowEventArgs e ) 
        { Console.WriteLine( "WorkflowCreated: t{0}", e.WorkflowInstance.InstanceId ); };
      runtime.WorkflowTerminated += delegate( object sender, WorkflowTerminatedEventArgs e ) 
        { Console.WriteLine( "WorkflowTerminated: t{0}", e.Exception.Message ); };
      runtime.WorkflowCompleted += delegate( object sender, WorkflowCompletedEventArgs e ) 
        { Console.WriteLine( "WorkflowCompleted: t{0}", e.WorkflowInstance.InstanceId ); };

      host.Open();
      Console.WriteLine( "Workflow host fut..." );
      Console.ReadLine();
      host.Close();
    }

Természetesen ide is kell app.config, vegyük át a MyServiceHost projektből és írjuk át néhány helyen:

  • A service tag name attribútuma ne MyServiceLib.CalcService, hanem MyWorkflowLib.CalcWorkflow legyen, hiszen ezt hosztoljuk.
  • Használjuk a 9090-es portot, hogy a korábbi szolgáltatásunk még működhessen.
  • wsHttpBinding helyett használjuk wsHttpContextBindingot, a WorkflowServices ezt szereti.
  • Ha a workflow runtime-ot is szeretnénk konfigurálni, akkor azt a <serviceBehaviors> tag alatt a <behavior> elemben tehetjük meg a WF 3.0-ban megismert XML elemekkel.

Íme a teljes system.serviceModel szekció:

    <services>
      <service name="MyWorkflowLib.CalcWorkflow" behaviorConfiguration="MyServiceLib.CalcServiceBehavior">
        <host>
          <baseAddresses>
            <add baseAddress = "http://localhost:9090/CalcService/" />
              </baseAddresses>
          </host>
         <endpoint address="" binding="wsHttpContextBinding" contract="MyServiceLib.ICalcService" />
         <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MyServiceLib.CalcServiceBehavior">
          <serviceMetadata httpGetEnabled="True"/>
            <!--
              <workflowRuntime name="WorkflowServiceHostRuntime" validateOnCreate="true">
                <services>
                  <add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService"
                       connectionString="Data Source=.SQLExpress;Initial Catalog=WorkflowPersistenceStore;Integrated Security=True"
                       LoadIntervalSeconds="1" UnLoadOnIdle="true"  />
                </services>
              </workflowRuntime>-->
        </behavior>
      </serviceBehaviors>
    </behaviors>

Az alkalmazás ezután futtatható, de előfordulhat, hogy ezt a kivételt kapjuk:

Unhandled Exception: System.InvalidOperationException: Service ‘MyWorkflowLib.CalcWorkflow’ has zero application (non-infrastructure) endpoints. This might be because no configuration file was found for your application, or because no service element matching the service name could be found in the configuration file, or because no endpoints were defined in the service element.

Ellenőrizzük ismét, hogy mindent átírtunk-e, mert bár rengeteg beállítás van a konfig fájlban, a WCF runtime nem találja, amit keres. Valószínűleg a szolgáltatás nevével lesz gond.

Workflow kliens készítése

Módosítsuk a kliens alkalmazásunkat, hogy az új szolgáltatáshoz kapcsolódjon. Szerencsére ehhez csak a konfig fájlt kell módosítani, hizsen azonos contractot használunk. A cím és a binding más:

    <system.serviceModel>
        <client>
            <endpoint
                    address="http://localhost:9090/CalcService/"
                    binding="wsHttpContextBinding"
                    contract="MyServices.ICalcService" />
        </client>
    </system.serviceModel>

Ha ez megvan, a kliensnek rá kell találnia a workflow alapú szolgáltatásunkra.

Workflow készítése SendActivity használatával

Ha már megmaradt a korábbi MyServiceHost alkalmazásunk egy másik porton, miért ne tekinthetnénk azt egy külső szolgáltatásnak és passzolhatnánk tovább a workflowba befutó kéréseket neki? A SendActivity használatával meghívhatunk külső szolgáltatásokat.

Nyissuk meg tehát a korábban elkészített workflowt és dobjunk a Toolboxról egy SendActivity-t a ReceiveActivity-be:

SendActivity egy ReceiveActivity-ben

Itt is a ServiceOperationInfo tulajdonság a kulcs, válasszuk ki a MyServiceLib.ICalcService.Add metódust, ami után a ReceiveActivity-nél látott módon van lehetőségünk a metódus paramétereinek és visszatérési értékeinek kötésére a már létrehozott helyi tulajdonságokhoz.

Hiányolja továbbá a validátor a ChannelToken tulajdonság beállítását. Ha ide tetszőleges sztringet (pl. token), írunk, lenyithatjuk a tulajdonságot és láthatjuk, hogy vár egy EndpointName értéket. Itt van tehát a kutya elhantolva, definiálnunk kell egy endpointot, ami mindent elárul a hosztnak arról, hogy hol van a meghívandó szolgáltatás. Mivel ilyet már épp csináltunk a MyClient projektnél, vegyük át az ottani app.config client szekcióját, aminek ugyan a szerkezete jó, de minden értékét módosítani kell és nevet is kell adni neki:

    <client>
        <endpoint
                name="MyServices_ICalcService"
                address="http://localhost:9091/CalcService/"
                binding="wsHttpBinding"
                contract="MyServiceLib.ICalcService" />
    </client>

A name tulajdonság értékét adjuk meg a Properties ablakban az EndpointName értékeként és máris tesztelhetjük a két hosztot és a klienst egyetlen rendszerként workflow runtime-ostul. (A CodeActivity-ben az összeadást célszerű kikommentezni.)

Összefoglalás

Készítettünk tehát egy konzolos kliens alkalmazást, ami wsHttpContextBindingon keresztül meghívja a 9090-es porton figyelő, workflow alapokon megvalósított számológép szolgáltatást. Mivel ebben nincs összeadás funkció (kikommenteztük), ezért ő továbbhív egy másik külső szolgáltatás felé, ami a 9091-es porton figyel és wsHttpBindingon keresztül érhető el. A dologban az a szép, hogy a WF gondoskodik arról, hogy az üzleti logikánk valóban sokáig futhasson és akár az újraindítást is túlélje, miközben a WCF gondoskodik a rugalmasan konfigurálható kommunikációról.

A megoldás szép, de ha valahol valami elakad vagy finomhangolni kell, akkor bizony irány a WCF+WF mélyvíz….

A cikkhez tartozó forráskód megtalálható az MSDN Kompetencia Központ honlapján, ahol a .NET 3.5 Induló Készletben Benedek Zoltán előadása részletesen érinti a workflow services témakörét is.

 

Workflow Foundation hosztolása ASP.NET-ben

Hear me speak at TechEd Developers 2007 Ha már Lipi volt olyan kedves és Világszám! címmel blogbejegyzést írt a TechEd előadásomról, igazán tartozom némi bővebb információval az itthon maradottak számára.

A dolog úgy kezdődött, hogy a tavaly novemberi MSDN konferencia után jobban beleástam magam a Windows Workflow Foundation és az ASP.NET kapcsolatába, amiről pár rövidebb blogbejegyzést írtam is (lásd hosting és threading). Mélyebbre ásva magam a témába hamar kiderült, hogy WF-et ASP.NET-ben hosztolni bár lehet, csak mazochistáknak ajánlott. Erőltetés a köbön. Sokkal ésszerűbbnek tűnik helyette egy Windows service-t írni és a webalkalmazásból remotingon keresztül kommunikálni vele.

Akárhogy is, nem lehet elmenni amellett, hogy a Microsoft szerint a WF minden fajta CLR AppDomainben hosztolható. Bár ez kétségkívül igaz és a VS által generált projekt típusokban simán működik is, ASP.NET-ben visszatérő fejfájást okozhat. Végigküzdve magam némi kódon rájöttem, hogy nem egyszerűen a hosztolás a gond. A probléma részét képezi az, hogy a webfejlesztők nem akarnak tudni a WF-ről, a workflow fejlesztők pedig nem akarják, hogy az ASP.NET guruk belepiszkáljanak a kódjaikba. A hosztolás technikai igényei mellett figyelembe venni ezt az igényt is, nem egyszerű olyan architektúrát találni, ami ideálisnak mondható. Az ExternalDataExchange-re épülő kommunikáció annyira nyakatekert, hogy nem triviális szétvágni a kódot fejlesztői szerepkörök és felelősség szerint.

Összeszedtem néhány kényes területet, és erről beadtam egy előadás javaslatot a TechEdre, amit el is fogadtak:

  • WF és ASP.NET technológiák összekapcsolása
  • WF és ASP.NET specifikus kódok szétválasztása
  • Szálkezelés
  • Hosszú ideig futó (long running) folyamatok
  • Szekvenciális vagy állapotgép?
  • Hosszú ideig futó activity-k
  • Eseményvezérelt activity-k
  • Kommunikáció
  • Monitorozás

Ha pedig már ott voltam, akkor "jól kihasználtak" és még egy másik előadásra is felkértek a workflow kommunikációs infrastruktúrával kapcsolatban. Ha van rá igény, egyszer szívesen leírom, hogy milyen egy ilyen méretű konferencia előadói szemmel.

Visszatérve az eredeti témára, az Avoiding Pitfalls When Hosting Windows Workflow Foundation in Real World ASP.NET Applications című előadás úgy látszik többeket szíven talált, mert sokan kérték a demó alkalmazás forráskódját, amit az MSDN Kompetencia Központ oldalán meg is osztok minden érdeklődővel. A példa egy ASP.NET standard regisztrációs oldalt egészít ki egy workflow-val, amivel ellenőrizzük a felhasználó e-mail címét és beépítünk még egy manuális jóváhagyást vagy elutasítást is. Nem bonyolult, de sok problémát érint. Lehet benne példát találni nem csak a hosztolásra, hanem a kommunikációra, állapotgépre és hosztolásra is, sőt még arra is, hogy a folyamat aktuális állapotát hogyan jeleníthetjük meg egy weboldalon grafikusan.

Minden visszajelzést szívesen fogadok!

 

WorkflowInstanceId kötése

Aki csinált már olyan workflowt, amelynek kommunikálnia kellett a külvilággal, annak lehet, hogy szüksége volt arra, hogy a workflow egyedi azonosítóját átadja egy activitynek vagy egy külső komponensnek. Az "átadás" nem jelenthet gondot, hiszen a Workflow Designer okos jószág, lehet benne tulajdonságokat kötögetni.

Sajnálatos módon azonban a WorkflowInstanceId tulajdonságot nem kínálja fel, hiába keresünk System.Guid típushoz párt. Mit tehet ilyenkor az egyszeri workflow programozó: definiál egy tulajdonságot, ami majd becsomagolja ezt a hívást, a tulajdonságot pedig public láthatóságra állítja, hogy köthető legyen:

    public Guid CurrentInstanceId
    {
        get
        {
            return this.WorkflowInstanceId;
        }
    }

Ettől kezdve azonban furcsa jelenségek ütik fel a fejüket, mintha minden a feje tetejére állt volna, például:

  • A workflow példány tökéletesen lefut, de az utolsó activity utáni pillanatban mégis elszáll, pedig oda nem is írtunk kódot.
  • A workflow példány az élete végén 100%-ra terheli a processzort.
  • Elszáll az alkalmazásunk, mert eddigi tökéletesen működő helyen azt a hibaüzenetet kapjuk, hogy az adott művelet csak workflow runtime threaden hajtható végre.
  • A Visual Studio debug módban nem képes megmutatni a workflowt, mert szerinte valami érvénytelen művelettel próbálkozunk, amit csak futási időben lehet elvégezni.

Mindennek az oka pedig ez a kis ártatlannak tűnő tulajdonság. De mi lehet a háttérben? Gondoljunk arra, hogy a this.WorkflowInstanceId tulajdonság kiolvasásának akkor van értelme, ha a workflow példány fut és a workflow runtime tud róla. Vannak azonban olyan pillanatok, amik nem esnek ebbe a csoportba, a Workflow Foundation mégis megpróbálja kiolvasni a workflow példány tulajdonságait. Ez történik például akkor, amikor a WorkflowRuntime.WorkflowCompleted eseménykezelőnek megpróbál átadni egy WorkflowCompletedEventArgs paramétert. Ebben az e paraméterben ugyanis van egy OutputParameters tulajdonság, ami egy szótár típusú gyűjteményen keresztül teszi elérhetővé a workflow példány tulajdonságait. Amikor a rendszer megpróbálja felépíteni ezt a dictionary-t, akkor bizony a workflow példányunk már éppen nem él.

Mit tehetünk akkor, ha mindenáron kötni szeretnénk a folyamat egyedi azonosítóját? Készíthetünk egy publikus property-t, ami nem közvetlenül a this.WorkflowInstanceId tulajdonságot olvassa ki, hanem egy olyan változó tartalmát, ahova korábban már átmásoltuk ezt az értéket. Ha biztosak vagyunk abban, hogy ezt a tulajdonságot a workflow példány futása közben valamelyik activity kiolvassa, akkor írhatjuk például ezt:

    private Guid _instanceId;

    public Guid CurrentInstanceId
    {
        get
        {
            if( this._instanceId.Equals( Guid.Empty ) )
            {
                this._instanceId = this.WorkflowInstanceId;
            }
            return this._instanceId;
        }
    }

Általában érdemes odafigyelni a tervezésnél, hogy egy döglött workflow példányról már csak a tracking service segítségével szerezhetünk információkat.

 

Technorati tags: , ,

Elbénázott workflow verzió frissítés – Nézz és láss!

A minap hozzá kellett nyúlnom egy korábbi Workflow Foundationös projekthez, aminek a verziószámát szépen meg is növeltem a módosítás után. A biztonság kedvéért nyomtam a Studioban egy Rebuild Solutiont, mégis az alkalmazás első futtatásakor ezt a hibaüzenetet kaptam:

Could not load file or assembly ‘Sample.Workflows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null’ or one of its dependencies. The located assembly’s manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)

A fene, valami nem jól frissült. Nosza, újrafordítottam a solution minden egyes projektjét. Semmi javulás. Letöröltem majd újra hozzáadtam a szerelvény referenciákat, a hibaüzenet változatlan. Letöröltem a bin és obj mappákat. Ugyanaz a szöveg. Kibányásztam a Windows mappából a Temporary ASP.NET Files mappát és abból is kitakarítottam mindent, de ez sem segített.

Ekkor kezdtem el gyanakodni, hogy rossz helyen keresem a hibát és ekkor ugrott be a hibakeresés első számú alapszabálya: olvasd el a hibaüzenetet, az egészet, elejétől a végéig!

Nézzük meg, melyik sornál keletkezik a hiba! Ez volt a bűnös:

StateMachineWorkflowInstance instance = new StateMachineWorkflowInstance( runtime, workflowInstanceId );

Ezen nincs mit hibáztatni, ennek tényleg kell a workflow típus a szerelvényből. Olvassuk csak végig a hibaüzenetet, ha már rászántuk magunkat. Na de mi van egy .NET-es hibaüzenet végén? A stack trace a maga hosszú és unalmas formájában, jelen esetben ez:

[FileLoadException: Could not load file or assembly ‘Sample.Workflows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null’ or one of its dependencies. The located assembly’s manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)]
   System.Reflection.Assembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection) +0
   System.Reflection.Assembly.InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection) +211
   System.Reflection.Assembly.InternalLoad(String assemblyString, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection) +141
   System.Reflection.Assembly.Load(String assemblyString) +25
   System.UnitySerializationHolder.GetRealObject(StreamingContext context) +355
   System.Runtime.Serialization.ObjectManager.ResolveObjectReference(ObjectHolder holder) +61
   System.Runtime.Serialization.ObjectManager.DoFixups() +2599325
   System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage) +203
   System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage) +190
   System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream) +12
   System.Workflow.ComponentModel.Activity.Load(Stream stream, Activity outerActivity, IFormatter formatter) +219
   System.Workflow.ComponentModel.Activity.Load(Stream stream, Activity outerActivity) +52
   System.Workflow.Runtime.Hosting.WorkflowPersistenceService.RestoreFromDefaultSerializedForm(Byte[] activityBytes, Activity outerActivity) +114
   System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService.LoadWorkflowInstanceState(Guid id) +249
   System.Workflow.Runtime.WorkflowRuntime.InitializeExecutor(Guid instanceId, CreationContext context, WorkflowExecutor executor, WorkflowInstance workflowInstance) +607
   System.Workflow.Runtime.WorkflowRuntime.Load(Guid key, CreationContext context, WorkflowInstance workflowInstance) +268
   System.Workflow.Runtime.WorkflowRuntime.GetWorkflow(Guid instanceId) +148
   System.Workflow.Activities.StateMachineWorkflowInstance..ctor(WorkflowRuntime runtime, Guid instanceId) +126
   WorkflowManager.GetCurrentState(Guid workflowInstanceId) in c:Documents and SettingsdemoDesktopAspNetWFDemoSolutionWebApp_CodeWorkflowManager.cs:187
   Admin_Default.GetState(Object comment) in c:Documents and SettingsdemoDesktopAspNetWFDemoSolutionWebAdminDefault.aspx.cs:46
   ASP.admin_default_aspx.__DataBinding__control18(Object sender, EventArgs e) in c:Documents and SettingsdemoDesktopAspNetWFDemoSolutionWebAdminDefault.aspx:41
   System.Web.UI.Control.OnDataBinding(EventArgs e) +99
   System.Web.UI.Control.DataBind(Boolean raiseOnDataBinding) +206
   System.Web.UI.Control.DataBind() +12
   System.Web.UI.Control.DataBindChildren() +216
   System.Web.UI.Control.DataBind(Boolean raiseOnDataBinding) +216
   System.Web.UI.Control.DataBind() +12
   System.Web.UI.Control.DataBindChildren() +216
   System.Web.UI.Control.DataBind(Boolean raiseOnDataBinding) +216
   System.Web.UI.Control.DataBind() +12
   System.Web.UI.WebControls.GridView.CreateRow(Int32 rowIndex, Int32 dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState, Boolean dataBind, Object dataItem, DataControlField[] fields, TableRowCollection rows, PagedDataSource pagedDataSource) +221
   System.Web.UI.WebControls.GridView.CreateChildControls(IEnumerable dataSource, Boolean dataBinding) +3004
   System.Web.UI.WebControls.CompositeDataBoundControl.PerformDataBinding(IEnumerable data) +59
   System.Web.UI.WebControls.GridView.PerformDataBinding(IEnumerable data) +11
   System.Web.UI.WebControls.DataBoundControl.OnDataSourceViewSelectCallback(IEnumerable data) +111
   System.Web.UI.DataSourceView.Select(DataSourceSelectArguments arguments, DataSourceViewSelectCallback callback) +29
   System.Web.UI.WebControls.DataBoundControl.PerformSelect() +149
   System.Web.UI.WebControls.BaseDataBoundControl.DataBind() +70
   System.Web.UI.WebControls.GridView.DataBind() +4
   System.Web.UI.WebControls.BaseDataBoundControl.EnsureDataBound() +82
   System.Web.UI.WebControls.CompositeDataBoundControl.CreateChildControls() +69
   System.Web.UI.Control.EnsureChildControls() +87
   System.Web.UI.Control.PreRenderRecursiveInternal() +41
   System.Web.UI.Control.PreRenderRecursiveInternal() +161
   System.Web.UI.Control.PreRenderRecursiveInternal() +161
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1360

Ez a hívási lánc önmagában is megérne egy misét és ezt látva talán nem meglepő, hogy a fejlesztők többségéhez hasonlóan én is csak az elejét és a végét néztem meg, abból pedig nem derült ki semmi. 😦

A bőnös pedig ott van a stack trace kellős közepén: WorkflowPersistenceService.RestoreFromDefaultSerializedForm. Ahhoz, hogy egy StateMachineWorkflowInstance példányt kapjunk, a runtime-nak szüksége van a workflow példányra (lásd GetWorkflow fent), amihez természetesen be kell tölteni azt a memóriába (Load), ami jelen esetben azt jelentette, hogy a persistence store-ból vissza kellett állítani a workflow példány állapotát (LoadWorkflowInstanceState). Csakhogy a persistence service sorosításkor eltárolja a workflowt tároló szerelvény teljes nevét, így a verziószámát is, és a visszatöltéskor az alapján próbálja visszaállítani az objektumot sorosított formából.

Tehát ha megváltoztatunk egy workflow definíciót (osztályt), akkor mindig gondoljunk arra, hogy az alapján a definíció alapján már futhatnak workflow példányok, amelyek sorosítva szunnyadnak valamilyen persistence vagy tracking adatbázisban. A friss szerelvényt gond nélkül fogjuk tudni telepíteni és új workflow példányokat is simán fogunk tudni indítani. A hiba csak akkor fog előállni, amikor egy csontváz kiesik a szekrényből: feléled egy várakozó workflow.