www.rolandk.de
- Aktuelle Themen zu .Net -
Achtung: Hier handelt es sich um meine alte Seite.
Die aktuelle ist unter folgendem Link erreichbar: www.rolandk.de/wp/
Home Artikel .Net Allgemein Workflow Persistenz und eigener Datenspeicher




















































Workflow Persistenz und eigener Datenspeicher
Donnerstag, den 26. Januar 2012 um 15:25 Uhr

 

Allgemeines

Persistierbare Workflows können eine feine Sache sein. Der Workflow läuft und sobald er nichts mehr zu tun hat, kann er sich in eine Sql-Datenbank speichern, um von dort später wieder geladen zu werden. Einer der Hacken an der Sache: Microsoft unterstützt für diesen Vorgang nur SQL-Server 2005 und 2008. Die dafür verwendeten Tabellen werden von Microsoft vorgegeben und können über ein Sql-Script (liegt im Framework-Verzeichnis) in die eigene Datenbank eingespielt werden. Für mich ist eine Lösung über den SqlServer aber aus verschiedenen Gründen nicht sinnvoll, daher habe ich nach einer anderen Lösung gesucht. Herausgekommen ist eine Klasse, die einen oder mehrere Workflows in eine Zip-Datei speichern kann und von dort wieder laden kann. Jeder Workflow wird dabei als Xml-Datei in der Zip-Datei gespeichert.

 

Die Klasse InstanceStore

Für die Persistenz in eine Sql-Datenbank ist die Klasse SqlWorkflowInstanceStore zuständig. Die Workflow Foundation greift auf die Funktionen dieser Klasse allerdings nur über die Basisklasse InstanceStore zu, wodurch ein eigener InstanceStore durch Überschreiben dieser Klasse möglich wird. Auf den ersten Blick sieht diese Klasse relativ kompliziert aus, wirklich interessant sind aber zunächst einmal nur die Methoden BeginTryCommand und EndTryCommand. Über diese Methoden schickt die WorkflowFoundation Befehle in Form von Command Objekten an den InstanceStore, die dann asynchron ausgeführt werden sollen. Wichtige Befehle sind zum Beispiel SaveWorkflowCommand und LoadWorkflowCommand.

 

Die Klasse ZipWorkflowInstanceStore

Wie beschrieben, soll diese Klasse von InstanceStore ableiten um so die Speicher- und Ladebefehle der WorkflowFoundation umzuleiten. Um auf Zip-Dateien zuzugreifen, verwende ich System.IO.Packaging, wie in diesem Artikel beschrieben. Der Konstruktor der Klasse nimmt einfach nur den Pfad zur Zip-Datei. Falls bereits vorhanden, wird daraus eine eindeutige Guid gelesen, falls nicht vorhanden, wird das Zip-Archiv und eine eindeutige Guid dafür erzeugt. Diese Guid wird in einem späteren Schritt benötigt.

  1. public class ZipWorkflowInstanceStore : InstanceStore
  1. /// <summary>
  2. /// XMLs the workflow instance store.
  3. /// </summary>
  4. /// <param name="zipFilePath">The zip file path.</param>
  5. public ZipWorkflowInstanceStore(string zipFilePath)
  6. {
  7. m_zipFilePath = zipFilePath;
  8.  
  9. if (File.Exists(m_zipFilePath))
  10. {
  11. //Load existing instance store
  12. try
  13. {
  14. using (ZipPackage zipPackage = Package.Open(
  15. m_zipFilePath,
  16. FileMode.Open) as ZipPackage)
  17. {
  18. ZipPackagePart zipPackagePart = zipPackage.GetPart(
  19. new Uri(PART_GUID, UriKind.Relative)) as ZipPackagePart;
  20. using (StreamReader inputStream = new StreamReader(
  21. zipPackagePart.GetStream(FileMode.Open)))
  22. {
  23. m_instanceStoreGuid = new Guid(inputStream.ReadToEnd());
  24. }
  25. }
  26. }
  27. catch (Exception ex)
  28. {
  29. throw new ArgumentException(
  30. "File " + zipFilePath + " is no valid ZipWorkflowInstanceStore!",
  31. "zipFilePath", ex);
  32. }
  33. }
  34. else
  35. {
  36. //Create a new instance store
  37. m_instanceStoreGuid = Guid.NewGuid();
  38. using (ZipPackage zipPackage = Package.Open(m_zipFilePath, FileMode.Create) as ZipPackage)
  39. {
  40. ZipPackagePart zipPackagePart = zipPackage.CreatePart(
  41. new Uri(PART_GUID, UriKind.Relative),
  42. CONTENT_TYPE_TXT, CompressionOption.Fast)
  43. as ZipPackagePart;
  44. using (StreamWriter outputStream =
  45. new StreamWriter(zipPackagePart.GetStream(FileMode.Create)))
  46. {
  47. outputStream.Write(m_instanceStoreGuid.ToString());
  48. }
  49. }
  50. }
  51. }

Wie bereits erwähnt, empfängt die Klasse alle Befehle über die BeginTryCommand Methode. In meiner Implementierung behandle ich 3 Befehle von der WorkflowFoundation. Der Befehl CreateWorkflowOwnerCommand bindet die WorkflowInstanz an diesen Datenspeicher, und zwar über die im Konstruktor erzeugte Guid. Die anderen beiden laden und speichern jeweils eine WorkflowInstanz.

  1. /// <summary>
  2. /// Starts executing the given command.
  3. /// This is the main method of this InstanceStore object.
  4. /// </summary>
  5. /// <param name="context">The context of execution.</param>
  6. /// <param name="command">The command to be executed.</param>
  7. /// <param name="timeout">The timeout value.</param>
  8. /// <param name="callback">The callback method (Async Pattern).</param>
  9. /// <param name="state">The async state object (Async Pattern).</param>
  10. /// <returns></returns>
  11. protected override IAsyncResult BeginTryCommand(
  12. InstancePersistenceContext context,
  13. InstancePersistenceCommand command,
  14. TimeSpan timeout, AsyncCallback callback, object state)
  15. {
  16. Func<bool> asyncFunc = new Func<bool>(() =>
  17. {
  18. //This command locks the workflow
  19. CreateWorkflowOwnerCommand createOwnerCommand =
  20. command as CreateWorkflowOwnerCommand;
  21. if (createOwnerCommand != null)
  22. {
  23. context.BindInstanceOwner(m_instanceStoreGuid, Guid.NewGuid());
  24. }
  25.  
  26. //This command saves the workflow into the instance store
  27. SaveWorkflowCommand saveWorkflowCommand = command as SaveWorkflowCommand;
  28. if (saveWorkflowCommand != null)
  29. {
  30. SaveData(saveWorkflowCommand.InstanceData);
  31. }
  32.  
  33. //This command loads a workflow
  34. LoadWorkflowCommand loadWorkflowCommand = command as LoadWorkflowCommand;
  35. if (loadWorkflowCommand != null)
  36. {
  37. Guid targetGuid = ReadProperty<Guid>(context.InstanceHandle, "Id");
  38. IDictionary<XName, InstanceValue> loadedData = LoadData(targetGuid);
  39. context.LoadedInstance(InstanceState.Initialized, loadedData, null, null, null);
  40. }
  41.  
  42. return true;
  43. });
  44. return asyncFunc.BeginInvoke(
  45. callback,
  46. state);
  47. }

Den Umweg über den Func<bool> mache ich übrigens, um die Funktion asynchron auszuführen und automatisch das Verhalten dieses Async-Patterns abzubilden. Das EndTryCommand dazu sieht einfach wie folgt aus.

  1. /// <summary>
  2. /// Ends execution of the command.
  3. /// </summary>
  4. /// <param name="result">The result.</param>
  5. protected override bool EndTryCommand(IAsyncResult result)
  6. {
  7. System.Runtime.Remoting.Messaging.AsyncResult asyncResult =
  8. result as System.Runtime.Remoting.Messaging.AsyncResult;
  9. Func<bool> func = asyncResult.AsyncDelegate as Func<bool>;
  10. return func.EndInvoke(result);
  11. }

Die Daten, die gespeichert werden müssen, bekommt man über ein IDictionary<XName, InstanceValue> Objekt. Genau diese Dictionary muss beim Ladevorgang wieder hergestellt werden können, alles Weitere liegt am Entwickler. In diesem Beispiel habe ich die InstanceValue Objekte über einen NetDataContractSerializer serialisiert und zusammen mit dem jeweiligen Namen in eine Xml-Datei abgespeichert. Die Implementierung meiner SaveData Methode sieht folgendermaßen aus.

  1. /// <summary>
  2. /// Saves all given instance data into the zip file.
  3. /// </summary>
  4. /// <param name="instanceData">The instance data to save.</param>
  5. private void SaveData(IDictionary<XName, InstanceValue> instanceData)
  6. {
  7. using (ZipPackage zipPackage = Package.Open(m_zipFilePath, FileMode.Open) as ZipPackage)
  8. {
  9. //Create new document that will store all given data
  10. XmlDocument docToSave = new XmlDocument();
  11.  
  12. //Create initial element
  13. docToSave.LoadXml("<InstanceValues />");
  14.  
  15. //Serialize all data
  16. foreach (KeyValuePair<XName, InstanceValue> actPair in instanceData)
  17. {
  18. XmlElement newInstance = docToSave.CreateElement("InstanceValue");
  19. XmlAttribute typeAttrib = docToSave.CreateAttribute("type");
  20. typeAttrib.Value = actPair.Key.LocalName;
  21. newInstance.Attributes.Append(typeAttrib);
  22.  
  23. XmlElement newKey = SerializeObject(actPair.Key, "key", docToSave);
  24. newInstance.AppendChild(newKey);
  25.  
  26. XmlElement newValue = SerializeObject(actPair.Value.Value, "value", docToSave);
  27. newInstance.AppendChild(newValue);
  28.  
  29. docToSave.DocumentElement.AppendChild(newInstance);
  30. }
  31.  
  32. //Get id of the workflow instance
  33. XmlNamespaceManager namespaceManager = new XmlNamespaceManager(docToSave.NameTable);
  34. namespaceManager.AddNamespace(
  35. "activity",
  36. "http://schemas.datacontract.org/2010/02/System.Activities");
  37. namespaceManager.AddNamespace(
  38. "linq",
  39. "http://schemas.datacontract.org/2004/07/System.Xml.Linq");
  40. XmlNode idNode = docToSave.SelectSingleNode(
  41. "/InstanceValues/InstanceValue[@type='Workflow']/value/" +
  42. "activity:Executor/activity:WorkflowInstanceId",
  43. namespaceManager);
  44. Guid workflowID = new Guid(idNode.InnerText);
  45.  
  46. //Check for state
  47. XmlNode stateNode = idNode.ParentNode.SelectSingleNode("activity:state", namespaceManager);
  48. bool isWorkflowClosed = false;
  49. if (stateNode != null)
  50. {
  51. isWorkflowClosed = stateNode.InnerText == WF_STATE_CLOSED;
  52. }
  53.  
  54. //Open or create package part for writing
  55. string packagePartPath = "/instances/" + workflowID.ToString() + ".xml";
  56. Uri packagePartUri = new Uri(packagePartPath, UriKind.Relative);
  57. ZipPackagePart packagePart = null;
  58. if (zipPackage.PartExists(packagePartUri))
  59. {
  60. packagePart = zipPackage.GetPart(packagePartUri) as ZipPackagePart;
  61. }
  62. else
  63. {
  64. packagePart = zipPackage.CreatePart(
  65. new Uri(packagePartPath, UriKind.Relative),
  66. "text/xml", CompressionOption.Fast) as ZipPackagePart;
  67. }
  68.  
  69. //Write the generated sql
  70. using(Stream outStream = packagePart.GetStream(FileMode.Create, FileAccess.Write))
  71. {
  72. docToSave.Save(outStream);
  73. }
  74. }
  75. }

Sieht etwas kompliziert aus, aber aus einem anderen Grund. Damit ich für jede WorkflowInstanz eine eigene Datei anlegen kann und diese eindeutig zuordnen kann, benötige ich die InstanceID. Diese bekommt man allerdings nicht über die Eigenschaften der übergebenen Objekte, da sie sich in einem privaten Feld der Workflow Objekte befindet. Aus diesem Grund habe ich mich dazu entschieden, die Instance-ID nach der Serialisierung per XPath auszulesen. Am Ende wird noch die Xml-Datei in die Zip-Datei eingefügt und damit ist das Speichern abgeschlossen. Die Hilfsmethode zum Serialisieren der Objekte sieht folgendermaßen aus:

  1. /// <summary>
  2. /// Serializes the given object into the given xml element.
  3. /// </summary>
  4. /// <param name="objectToSerialize">The object to serialize.</param>
  5. /// <param name="elementName">The name of the generated element.</param>
  6. /// <param name="doc">The owner.</param>
  7. private XmlElement SerializeObject(
  8. object objectToSerialize,
  9. string elementName,
  10. XmlDocument doc)
  11. {
  12. NetDataContractSerializer serializer = new NetDataContractSerializer();
  13. XmlElement newElement = null;
  14.  
  15. using (MemoryStream memoryStream = new MemoryStream())
  16. {
  17. serializer.Serialize(memoryStream, objectToSerialize);
  18. memoryStream.Position = 0;
  19.  
  20. using (StreamReader rdr = new StreamReader(memoryStream))
  21. {
  22. newElement = doc.CreateElement(elementName);
  23. newElement.InnerXml = rdr.ReadToEnd();
  24. }
  25. }
  26.  
  27. return newElement;
  28. }

Damit sind alle nötigen Methoden gezeigt, die eine Workflow Insanz speichern können. Das Laden der Instanzen sieht etwas einfacher aus. Wie oben schon beschrieben geht es dabei darum, eine bestimmte Instanz zu laden und daraus das zuvor gespeicherte Dictionary Objekt wieder zusammen zu auben. Nachfolgende Methode erfüllt diese Aufgabe.

  1. /// <summary>
  2. /// Loads all data theat belongs to the given instance id.
  3. /// </summary>
  4. /// <param name="instanceGuid">The instance id to load.</param>
  5. private IDictionary<XName, InstanceValue> LoadData(Guid instanceGuid)
  6. {
  7. Dictionary<XName, InstanceValue> result = new Dictionary<XName, InstanceValue>();
  8.  
  9. using (ZipPackage zipPackage = Package.Open(m_zipFilePath, FileMode.Open) as ZipPackage)
  10. {
  11. //Check if there is any instance with the given guid
  12. string packagePartPath = "/instances/" + instanceGuid.ToString() + ".xml";
  13. Uri packagePartUri = new Uri(packagePartPath, UriKind.Relative);
  14. if (!zipPackage.PartExists(packagePartUri))
  15. {
  16. throw new ApplicationException("Unable to find workflow instance " + instanceGuid + "!");
  17. }
  18.  
  19. //Read the file
  20. ZipPackagePart packagePart = zipPackage.GetPart(packagePartUri) as ZipPackagePart;
  21. using (Stream inputStream = packagePart.GetStream(FileMode.Open))
  22. using (XmlReader xmlReader = XmlReader.Create(inputStream))
  23. {
  24. //Deserialize xml file
  25. NetDataContractSerializer serializer = new NetDataContractSerializer();
  26.  
  27. XmlDocument xmlDocument = new XmlDocument();
  28. xmlDocument.Load(xmlReader);
  29.  
  30. XmlNodeList instances = xmlDocument.GetElementsByTagName("InstanceValue");
  31. foreach (XmlElement instanceElement in instances)
  32. {
  33. XmlElement keyElement = (XmlElement)instanceElement.SelectSingleNode("descendant::key");
  34. XName key = (XName)DeserializeObject(serializer, keyElement);
  35.  
  36. XmlElement valueElement = (XmlElement)instanceElement.SelectSingleNode("descendant::value");
  37. object value = DeserializeObject(serializer, valueElement);
  38. InstanceValue instVal = new InstanceValue(value);
  39.  
  40. result[key] = instVal;
  41. }
  42. }
  43. }
  44.  
  45. return result;
  46. }

Bei dieser Methode passiert nichts mehr wirklich Neues. Es werden schlicht alle Daten, die in dem Xml-File gefunden werden, wieder deserialisiert und zusammengesetzt.

 

Fazit

Dieser Artikel hat kurz gezeigt, wie ein eigener Datenspeicher für Workflow Instanzen verwendet werden kann. Ob es sich wie hier um eine Zip-Datei handeln soll, ist jedem Entwickler selbst überlassen. Der Aufwand dafür, eine solche Klasse zu schreiben, sollte überschaubar sein. Etwas schwierig kann es aber sein, die für die eigene Implementierung wichtigen Befehle herauszufinden, die man implementieren muss. In diesem Beispiel behandle ich nur 3, es gibt aber mehrere davon. Auf Msdn etwa kann man eine Auflistung der Befehle abrufen.

lt;XName, InstanceValue

 

Kommentar hinzufügen

Ihr Name:
Kommentar: