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 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:
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:
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 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:
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.