Tag Archives: workflow

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.

 

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.

 

Workflow Monitor layouttal

Aki használta már a Windows Workflow Foundationben a tracking service-t, biztosan találkozott már az SDK-ban lévő Workflow Monitor alkalmazással, ami lehetővé teszi, hogy a Visual Studiohoz hasonlóan, grafikusan jelenítsük meg a folyamataink állapotát. Mivel ez az alkalmazás forráskóddal együtt elérhető, remek példa arra, hogy megtanuljuk, hogyan hosztolhatjuk a WorkflowView kontrollt saját alkalmazásainkban. A feladat egyébként nem egyszerű, jobban járunk, ha a kész példából vesszük az osztályokat, többre is szükségünk lesz 😦

Van egy fontos dolog, amire ebben a kódban nem találunk példát, ez pedig a folyamatábrák layoutjának kezelése. Ez szekvenciális workflow esetén nem probléma, hiszen ott úgysem lehet mozgatni az activity-ket, állapotgépek esetén viszont jó lenne, ha a monitorban is úgy jelennének meg a folyamataink, ahogy azokat a Studioban megrajzoltuk. Ha erről nem gondoskodunk, akkor az összes állapot-dobozunk egymás alatt fog elhelyezkedni, keresztül-kasul behálózva az összekötő vonalakkal.

A tetszetős című Everything about re-hosting the Workflow Designer című MSDN cikkben találunk utalást arra, hogyan menthetjük el a layoutot fájlba és tölthetjük vissza a layoutot fájlból. Ezzel csak egy baj van, kell hozzá layout fájl! Nem is probléma, hiszen a VS készít nekünk olyat, ott van .layout kiterjesztéssel, pont arra van szükség. Csakhogy ha ezt a megközelítést választjuk, a layout fájlnak ott kell lennie a futtatandó alkalmazásunk mellett, azaz ezt is telepíteni kell. Amikor pedig a telepítőt készítjük, beleütközünk abba a problémába, hogy a forrásfában kénytelenek leszünk vagy saját build eventeket írni, vagy pedig duplikálni a layout fájlt. Egyik sem túl fincsi.

Az igazi megoldás az lenne, ha fel tudnánk használni ugyanazt a fájlt futási időben, amit a Studio is használ fejlesztési időben. Ha a Properties ablakban megnézzük a .layout fájl Build Actionjét, láthatjuk, hogy Embedded Resource lesz belőle, nincs más dolgunk tehát, mint onnan kiolvasni.

Először megpróbáltam közvetlenül kiolvasni a fájl tartalmát ugyanúgy, mint ahogy más erőforrást kezelni szoktunk. Mivel ez nem vezetett eredményre, természetesen Reflectorhoz nyúltam és az alábbi eredményre jutottam: a Loader.cs fájlban található a WorkflowDesignerLoaderből származó Loader osztály, ami a folyamatok betöltéséért felelős. Ebben írjuk felül az OnEndLoad eseménykezelőt:

  protected override void OnEndLoad( bool successful, ICollection errors )
  {
    base.OnEndLoad( successful, errors );

    IList layoutErrors = null;
    IWorkflowRootDesigner rootDesigner = 
(IWorkflowRootDesigner) ActivityDesigner.GetRootDesigner( base.LoaderHost ); Type type = rootDesigner.Component.GetType(); string manifestResourceName = type.Name + ".layout"; this.LoadDesignerLayoutFromResource( type, manifestResourceName, out layoutErrors ); }

Így már csak arra az egy szerelvényre van szükségünk, amiben a workflow definíció is található és ugyanúgy fog megjelenni a Monitorban, mint a fejlesztőkörnyezetben.

 

Tracking profile importálása szkriptből

Nincs jobb, mint amikor az embernek van egy olyan szkriptje kéznél, ami egyszerűen és átláthatóan teszi a dolgát és igazán megkönnyíti a fejlesztő munkáját. Az egyik gyakori feladat az adatbázis létrehozása, amit két módon lehet megtenni:

  • Adatbázis attachelésével. Ez egyszerű, csak éppen a változtatásokat nehéz követni benne fejlesztés közben. Tipikusan akkor választja az ember ezt a megoldást, amikor azt hiszi, hogy ezzel kevesebb gondja lesz, de később kiderül, hogy mégse.
  • SQL szkriptekkel. Ez kényelmesen szerkeszthető és verzionálható, csak éppen az a kérdés, hogyan fognak egyszerűen lefutni az SQL szkriptek.

Az utóbbi az érdekesebb, hogyan futtatunk SQL szkripteket parancssorból? Korábban az osql.exe vagy az isql.exe volt a jópajtás, az SQL Server 2005-től kezdve ezek helyét átvette az SQLCMD.

Például ha Windows Workflow Foundationt használunk, valószínűleg szükségünk lesz tracking és persistence adatbázisokra, amikhez csak az SQL szkripteket kapjuk a Microsofttól. Nosza gyűrjük be az alábbi néhány sort egy cmd fájlba és máris lehet duplán kattintva adatbázist gyártani (eltördeltem a hosszú sorokat):

    SQLCMD -S .SqlExpress -Q "CREATE DATABASE MyDB"

    SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENSqlPersistenceService_Schema.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENSqlPersistenceService_Logic.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENTracking_Schema.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENTracking_Logic.sql"

Eddig egyszerű volt, de mi van akkor, ha saját tracking profile-t is akarunk használni? Erről annyit érdemes tudni, hogy a profil egy XML fájl, aminek a tartalmát az UpdateTrackingProfile tárolt eljárás segítségével kell egy táblába betöltenünk.

Általánosabban megfogalmazva a feladatot: hogyan tudunk egy olyan tárolt eljárást futtatni, aminek egyik bemeneti paramétere egy fájl teljes tartalma?

A megoldás első lépését a BULK INSERT T-SQL utasítás jelenti. Ennek megadhatunk egy fájl útvonalat, aminek a tartalmát be tudja tölteni egy táblába. Sajnos van néhány nyűgje:

  • Csak tábla típusú változót szeret, tehát nem elég egy DECLARE, csak CREATE TABLE jó neki.
  • A betöltendő fájl útvonalát nem lehet változóban megadni, oda kell írni aposztrófok közé.
  • Ha egy kulturáltan megformázott XML-t akarunk betölteni, akkor minden egyes sor külön rekordba kerül, ami persze nem jó, ha végül egyetlen cellában szeretnénk látni az eredményt.

Íme a megoldás:

    CREATE TABLE #temp( profileXml nvarchar(max) )


BULK INSERT #temp FROM '$(ProfilePath)' WITH ( ROWTERMINATOR = '<<<' -- dummy terminator, hogy az egész fájlt felolvassa )

Csinálunk tehát egy temp táblát, mert mindenképp tábla kell. Az egész fájl beolvasását úgy oldjuk meg, hogy olyan sor elválasztó karaktert adunk meg, ami biztosan nincs a fájlban. A kérdés már csak az, hogy mi a $(ProfilePath)?

Az SQLCMD egyik remek szolgáltatása, hogy lehet változókat megadni, amiket ő behelyettesít. A fenti aposztrófok közé például így varázsolhatunk értéket (a példa kedvéért elhagytam a többi paramétert):

    SQLCMD -v ProfilePath="profile.xml"

Ezzel megint csak gondunk lesz: a BULK INSERT panaszkodik, hogy nem teljes elérési utat adtunk meg. Kérdés: hogyan lehet egy cmd fájlban megtudni az aktuális mappa elérési útvonalát? Meglepő módon a cd parancs és a cd környezeti változó épp ezt adja vissza. Szerencsére az SQLCMD is tud környezeti változókat kezelni, így csak ennyit kell módosítanunk:

    SQLCMD -v ProfilePath="%cd%profile.xml"

Nem maradt más hátra, mint az UpdateTrackingProfile tárolt eljárás meghívása, amihez a már jól ismert EXEC utasítást használjuk. A profil XML-en kívül át kell neki adnunk egy TypeFullName, egy AssemblyName, és egy Version paramétert, melyeknél ugyanúgy járhatunk el, mint a fenti ProfilePath esetén. Ezek típusát és hosszát a tárolt eljárásból lehet kilesni.

Végül nekem a teljes SQL szkript így ez lett:

    -- Bemeneti paraméterek
    DECLARE @TypeFullName nvarchar(128)
    DECLARE @AssemblyName nvarchar(256)
    DECLARE @Version varchar(32)

    SET @TypeFullName = '$(TypeFullName)'
    SET @AssemblyName = '$(AssemblyName)'
    SET @Version = '$(Version)'

    -- Belső változók
    DECLARE @profileXml nvarchar(max)
    CREATE TABLE #temp( profileXml nvarchar(max) )

    -- Tracking profile betöltése fájlból temp táblába
    BULK INSERT #temp 
      FROM '$(ProfilePath)'
      WITH
      (
        ROWTERMINATOR = '<<<'  -- dummy terminator, hogy az egész fájlt felolvassa
      )

    -- Tracking profile XML kiolvasása temp táblából
    SELECT @profileXml = profileXml FROM #temp

    -- Tracking profile adatbázisba mentése
    EXEC dbo.UpdateTrackingProfile @TypeFullName, @AssemblyName, @Version, @profileXml

    -- Temp tábla törlése
    DROP TABLE #temp

És a cmd fájl, ami felépíti a teljes adatbázist és beimportálja a profilt (eltördeltem a hosszú sorokat):

    SQLCMD -S .SqlExpress -Q "CREATE DATABASE SignowDB"

    SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENSqlPersistenceService_Schema.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENSqlPersistenceService_Logic.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENTracking_Schema.sql" SQLCMD -S .SqlExpress -d MyDB -i "C:WINDOWSMicrosoft.NETFrameworkv3.0
Windows Workflow FoundationSQLENTracking_Logic.sql" SQLCMD -S .SqlExpress -d MyDB -i "InsertTrackingProfile.sql" -v Version=1.0.0
-v TypeFullName="MyNamespace.MyWorkflow1"
-v AssemblyName="MyLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=443f6c5b661c8da2"
-v ProfilePath="%cd%profile1.xml" SQLCMD -S .SqlExpress -d MyDB -i "InsertTrackingProfile.sql" -v Version=1.0.0
-v TypeFullName="MyNamespace.MyWorkflow2"
-v AssemblyName="MyLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=443f6c5b661c8da2"
-v ProfilePath="%cd%profile2.xml"

A legjobb az egészben, hogy mindezt nem csak fejlesztés közben lehet használni, hanem ezt a szkriptet be lehet építeni a telepítő alkalmazásba is.

 

Technorati tags: , ,

Workflow tulajdonságainak kiolvasása

Gyakori kérdés, hogy egy adott workflow példány belső állapotát, mezőit, tulajdonságait hogyan lehet kiolvasni? A rövid válasz, hogy egyszerűen sehogy:

  • Egy futó workflow esetén az ember a WorkflowInstance osztály környékén próbálkozik, amin keresztül legfeljebb az InstanceId lehet megszerezni. Futó workflow esetén az igazi megoldás az lehet, ha kommunikálunk a folyamattal, azaz a Queuing- vagy az ExternalDataExchangeServicen-en keresztül üzenetet küldünk neki.
  • Egy már lefutott workflow példány esetén már nincs más lehetőségünk, mint a tracking service használata.

Az utóbbi az érdekesebb és kevésbé dokumentált eset. A cél tehát egy SimpleWorkflow típusú folyamat Result tulajdonságának utólagos kiolvasása.

Ehhez szükségünk lesz a tracking szolgáltatásra, használjuk a beépített SqlTrackingService-t, hozzuk létre az adatbázisát például WorkflowTrackingStore néven és adjuk hozzá a runtime-hoz:

    const string connectionString = 
@"Initial Catalog=WorkflowTrackingStore;Data Source=.SqlExpress;Integrated Security=SSPI;"; SqlTrackingService trackingService = new SqlTrackingService( connectionString ); runtime.AddService( trackingService );

A workflow futtatásakor nem kell semmi extrát tennünk, a tracking naplóz. Csakhogy nem azt naplózza, ami nekünk kell, hanem amiről azt hiszi, hogy nekünk kell! A tracking profile szolgál arra, hogy meghatározzuk, mire van szükségünk. Ezt megtehetjük objektum modell segítségével (használva a System.Workflow.Runtime.Tracking.TrackingProfile) osztályt, vagy deklaratívan egy XML segítségével. XML-t senki sem szeret kézzel írni (minden ellenkező híreszteléssel szemben az XML nem arra való, hogy homo sapiensek olvassák), a profil összeállításához használjuk a WF SDK példák között elérhető Tracking Profile Designert (közvetlenül letölthető a WF közösségi oldalról is).

Néhány tipp a Tracking Profile Designer használatához:

  • Ne felejtsük el módosítani a tracking adatbázisunkra mutató connection stringet a .config állományban.
  • Másoljuk az exe mellé a workflow-nkat tartalmazó lefordított szerelvényt.
  • A profil erősen típusosan hivatkozik a naplózandó workflow típusára, ami egyben a workflow-t tartalmazó szerelvényre is erősen típusos hivatkozást jelent. Ha nem akarjuk minden fordítás után módosítani a profilt, akkor a workflow AssemblyInfo.cs fájljában adjunk fix és ne csillagos verziószámot az AssemblyVersion attribútumban.
  • Ne csak adatbázisba mentsük el a létrehozott profilt, hanem archiváljuk magunknak XML-be is.

A profilban workflow, activity és felhasználói eseményeket tudunk meghatározni. Ahhoz, hogy egy adott tulajdonság értékét naplózza a rendszer, ún. data tracking extractot kell definiálnunk. Erre szolgál a TrackingExtract ősosztály, amiből a WorkflowDataTrackingExtract és az ActivityDataTrackingExtract származik. Biztos én bénáztam, de akárhogy kattintgattam a profile designerben, nekem nem sikerült workflow extractot létrehozni, csak activity extractot. Annyi baj legyen, a szekvenciális workflow végülis egy szép nagy sequence activity.

Nekem ez lett a profil:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<TrackingProfile xmlns="http://schemas.microsoft.com/winfx/2006/workflow/trackingprofile" version="1.0.0">
    <TrackPoints>
        <ActivityTrackPoint>
            <MatchingLocations>
                <ActivityTrackingLocation>
                    <Activity>
                        <Type>MyApp.SimpleWorkflow, MyApp, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null</Type>
                        <MatchDerivedTypes>false</MatchDerivedTypes>
                    </Activity>
                    <ExecutionStatusEvents>
                        <ExecutionStatus>Closed</ExecutionStatus>
                    </ExecutionStatusEvents>
                </ActivityTrackingLocation>
            </MatchingLocations>
            <Extracts>
                <ActivityDataTrackingExtract>
                    <Member>Result</Member>
                </ActivityDataTrackingExtract>
            </Extracts>
        </ActivityTrackPoint>
    </TrackPoints>
</TrackingProfile>

Emberi nyelvre fordítva: a SimpleWorkflow típusú activity (!) Closed eseményekor kérjük a Result tulajdonság naplózását. És semmi több! Ha tehát valaki később csak a lefutott workflow példányokat szeretné felolvasni vagy további tracking információkra van szüksége, az kénytelen lesz kiegészíteni a profilt még azokkal is.

Az összekattintgatott profilt mentsük el az adatbázisba. Akit érdekel, hogy mi történik a háttérben, az nézze meg a TrackingProfile táblát vagy az UpdateProfile tárolt eljárást.

Megvan tehát a workflow és a profil, ha most futtatjuk a folyamatot, akkor az adatbázisba be kell kerülnie a naplózandó adatoknak. Érdemes bekukkantani a TrackingDataItem táblába, ha mindent jól csináltunk, ott lesz.

Nem maradt más hátra, le kell kérdeznünk a tracking adatbázist, mégpedig az objektum modell SqlTrackingQuery osztályának segítségével:

    const string connectionString = 
@"Initial Catalog=WorkflowTrackingStore;Data Source=.SqlExpress;Integrated Security=SSPI;"; SqlTrackingQuery query = new SqlTrackingQuery( connectionString );

Csak azokat a workflow példányokat kérjük, amik SimpleWorkflow típusúak:

    SqlTrackingQueryOptions options = new SqlTrackingQueryOptions();
    options.WorkflowType = typeof( MyApp.SimpleWorkflow ); // Egyben szerelvény és verzió hivatkozás is!

Na itt kell észnél lenni, mert ha itt megszokásból megadjuk, hogy csak a Completed állapotú példányokra vagyunk kiváncsiak, de ennek a ténynek a naplózását nem kértük a profilban, akkor nem lesz eredményünk. Kérjük vissza a feltételnek megfelelő példányokat és ha nem akarunk sokat debuggolni, írassuk ki, hogy lett-e eredmény:

    IList<SqlTrackingWorkflowInstance> instances = query.GetWorkflows( options );
    Console.WriteLine( "Talált workflow: {0} db", instances.Count );

Innentől kezdve nincs más hátra, végig kell menni a példányokon. Minden SqlTrackingWorkflowInstance tartalmaz egy ActivityEvents gyűjteményt az activity szintű eseményeknek. Ha a profilban így kértük, most is ezt kell használni. Az ActivityTrackingRecordok Body tulajdonságában találhatjuk meg a kért napló sorokat:

    foreach( SqlTrackingWorkflowInstance instance in instances )
    {
        Console.WriteLine( "nnID: {0}", instance.WorkflowInstanceId );

        foreach( ActivityTrackingRecord activityRecord in instance.ActivityEvents )
        {
            Console.WriteLine( "t{0,-10:T}{1,-20}{2}", activityRecord.EventDateTime, 
activityRecord.QualifiedName, activityRecord.ExecutionStatus ); foreach( TrackingDataItem dataItem in activityRecord.Body ) { Console.WriteLine( "ttData: {0}t{1}", dataItem.FieldName, dataItem.Data ); } } }

Természetesen még véletlenül sincs benne egyik változó nevében sem, hogy extract. Ami a profilban extract, azt itt TrackingDataItemnek hívják. Ennyi, tessék kísérletezni!

A fenti példát tartalmazó teljes forráskód letölthető a devPORTALról.

 

Technorati tags: , ,