Improving The Page Flow Application Block: Removing Database Dependencies

Introduction


Especially in development and demonstration scenarios, the dependency on a database can be a big hassle. In this article I'll show how to remove this dependency.


Instance Correlation Provider


A Page Flow Application Block's instance correlation provider is responsible for maintaining information about page flow instances like instance id, type and running status. These providers must implement the Microsoft.Practices.PageFlow.IPageFlowInstanceStore:


namespace Microsoft.Practices.PageFlow
{
/// <summary>
/// Defines the interface for an object that can store <see cref="IPageFlow"/>s.
/// </summary>
public interface IPageFlowInstanceStore
{
/// <summary>
/// Adds an <see cref="IPageFlow"/> to the store.
/// </summary>
/// <param name="pageFlowInstance">The <see cref="IPageFlow"/> to add.</param>
void Add(IPageFlow pageFlowInstance);

/// <summary>
/// Removes an <see cref="IPageFlow"/> from the store.
/// </summary>
/// <param name="pageFlowInstance">The <see cref="IPageFlow"/> to remove.</param>
void Remove(IPageFlow pageFlowInstance);

/// <summary>
/// Removed an <see cref="IPageFlow"/> from the store.
/// </summary>
/// <param name="id">The unique identifier of the <see cref="IPageFlow"/> to remove from the store.</param>
void Remove(Guid id);

/// <summary>
/// Returns the <see cref="Guid">Guid</see> of the last instance.
/// </summary>
/// <returns>The unique identifier of the PageFlow instance.</returns>
Guid GetLastRunningInstance();

/// <summary>
/// Retrieves the unique identifier of an <see cref="IPageFlow">IPageFlow</see> instance
/// of the desired <see cref="Type">Type</see>.
/// </summary>
/// <param name="type">The <see cref="Type"/> of <see cref="IPageFlow"/> to retrieve.</param>
/// <returns>The unique idenitifier of an instance of the desired <see cref="Type"/>.</returns>
Guid GetByType(Type type);

/// <summary>
/// Retrieves the instance <see cref="Type">Type</see> assembly qualified full name correspnding to an instance id
/// </summary>
/// <param name="id">The <see cref="Guid">Guid</see> of the instance</param>
/// <returns>The assembly qualified name of the <see cref="Type">Type</see> of the instance or null if not found</returns>
string GetInstanceType(Guid id);

/// <summary>
/// Marks an <see cref="IPageFlow">IPageFlow</see> instance as the currently running instance.
/// </summary>
/// <param name="iPageFlow">The <see cref="IPageFlow">IPageFlow</see> instance to mark as running.</param>
void SetPageFlowRunning(IPageFlow iPageFlow);

/// <summary>
/// Marks an <see cref="IPageFlow">IPageFlow</see> instance as not currently running.
/// </summary>
/// <param name="iPageFlow">The <see cref="IPageFlow">IPageFlow</see> instance to mark as not currently running.</param>
void SetPageFlowNotRunning(IPageFlow iPageFlow);
}
}


The provided implementation uses an SQL Server database to store the page flow instance data.


To remove this dependency on a database I'll have to build a new page flow instance store.


If the sole purpose of this provider was to be only used in development, some in-memory dictionary would be just fine. But I want to build something that's as much as possible alike the provided provider and of production quality.


For me, the best choice was to use the ASP.NET session state. This way I keep instances isolated from session to session (this is different from the provided provider where page flow instances can be used across ASP.NET sessions) and can, ultimately, be stored in a data base for persistence and load balancing.


Implementing the PageFlowInstanceCorrelationAspNetSessionStateProvider


To replace the database based implementation all I need is to replace the database.


Since the provided implementation only uses one table, this means that it only has one entity - a storage item.


Storage Item


This is the entity that stores the information about page flow instances and its fields have a direct correspondence to the columns of the table of the provided implementation.


[Serializable]
private class StorageItem
{
public Guid InstanceId;
public string PageFlowType;
public string CorrelationToken;
public bool Running;
}

The class is marked with the Serializable attribute to allow its instances to be serialized to and back from a persistence store like a database.


Storage


This entity represents the database table and stored procedures used to manipulate the data.


[Serializable]
private partial class Storage : System.Runtime.Serialization.ISerializable
{
private SortedList<Guid, StorageItem> items = new SortedList<Guid, StorageItem>();

/// <summary>
/// Initializes a new instance of the <see cref="T:PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState.Storage"/> class.
/// </summary>
public Storage()
{
}

#region ISerializable Members

/// <summary>
/// Initializes a new instance of the <see cref="Storage"/> class.
/// </summary>
/// <param name="info">The info.</param>
/// <param name="context">The context.</param>
protected Storage(SerializationInfo info, StreamingContext context)
{
this.items = (SortedList<Guid, StorageItem>)info.GetValue("items", typeof(SortedList<Guid, StorageItem>));
}

/// <summary>
/// Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo"></see> with the data needed to serialize the target object.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"></see> to populate with data.</param>
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext"></see>) for this serialization.</param>
/// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission. </exception>
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("items", this.items);
}

#endregion

internal void InsertInstance(Guid instanceId, string pageFlowType, string correlationToken, bool running)
{
StorageItem item = new StorageItem();
item.InstanceId = instanceId;
item.PageFlowType = pageFlowType;
item.CorrelationToken = correlationToken;
item.Running = running;

this.items.Add(instanceId, item);
}

internal void DeleteInstance(Guid instanceId)
{
this.items.Remove(instanceId);
}

internal Guid GetLastRunningInstanceByCorrelationToken(string correlationToken)
{
foreach (StorageItem item in this.items.Values)
{
if (item.Running && (item.CorrelationToken == correlationToken))
{
return item.InstanceId;
}
}
return Guid.Empty;
}

internal Guid GetInstanceByTypeAndByCorrelationToken(string correlationToken, string pageFlowType)
{
foreach (StorageItem item in this.items.Values)
{
if ((item.CorrelationToken == correlationToken) && (item.PageFlowType == pageFlowType))
{
return item.InstanceId;
}
}
return Guid.Empty;
}

internal string GetTypeByInstanceId(Guid instanceId)
{
StorageItem item;
if (this.items.TryGetValue(instanceId, out item))
{
return item.PageFlowType;
}
return null;
}

internal void SetRunningInstanceForCorrelationToken(Guid instanceId, string correlationToken)
{
foreach (StorageItem item in this.items.Values)
{
// set running instances to not running
if ((item.CorrelationToken == correlationToken) && (item.Running))
{
item.Running = false;
}
// set the requested instance to running
if ((item.InstanceId == instanceId) && (!item.Running))
{
item.Running = true;
}
}
}

internal void ChangeInstanceStatus(Guid instanceId, bool running)
{
StorageItem item;
if (this.items.TryGetValue(instanceId, out item))
{
item.Running = running;
}
}
}


Since this is the entity being directly stored in the session state, the class is marked with the Serializable attribute to allow its instances to be serialized to an back from a persistence store like a database.


Page Flow Instance Correlation Provider


This class looks very much like the provided PageFlowInstanceCorrelationSqlProvider where the database is replaced with an instance of the Storage class and the calls to database stored procedures are replaced by calls to methods of the Storage class.


/// <summary>
///
An implementation of an <see cref="IPageFlowInstanceStore">IPageFlowInstanceStore</see>
///
that uses a the Enterprise Library Data Block <see cref="Database">Database</see> to manage
/// and store <see cref="IPageFlow">IPageFlow</see> instances.
/// </summary>
public partial class PageFlowInstanceCorrelationAspNetSessionStateProvider : IPageFlowInstanceStore
{
private string pageFlowStorageName;
private Storage pageFlowStorage;
private IPageFlowCorrelationTokenProvider tokenProvider;

/// <overloads>
/// Creates an instance of PageFlowInstanceCorrelationSqlProvider.
/// </overloads>
/// <summary>
/// Creates an instance of PageFlowInstanceCorrelationSqlProvider using
/// the provided database name.
/// </summary>
/// <remarks>
/// The PageFlowInstanceCorrelationSqlProvider will use a
/// <see cref="CookiePageFlowCorrelationTokenProvider">CookiePageFlowCorrelationTokenProvider</see>
/// to create and provide tokens for each instance.
/// </remarks>
/// <param name="databaseName">The name of the <see cref="P:HttpContext.Session"/> variable to use as the store.</param>
public PageFlowInstanceCorrelationAspNetSessionStateProvider(string databaseName)
: this(databaseName, new CookiePageFlowCorrelationTokenProvider())
{
}

/// <summary>
/// Creates an instance of PageFlowInstanceCorrelationSqlProvider.
/// </summary>
/// <param name="databaseName">The name of the <see cref="P:HttpContext.Session"/> variable to use as the store.</param>
/// <param name="tokenProvider">The <see cref="IPageFlowCorrelationTokenProvider">IPageFlowCorrelationTokenProvider</see>
/// implementation that will provide tokens.
/// </param>
public PageFlowInstanceCorrelationAspNetSessionStateProvider(string databaseName, IPageFlowCorrelationTokenProvider tokenProvider)
{
Guard.ArgumentNotNullOrEmptyString(databaseName, "databaseName");
Guard.ArgumentNotNull(tokenProvider, "tokenProvider");
this.pageFlowStorageName = databaseName;
this.tokenProvider = tokenProvider;
}

/// <summary>
/// Adds an <see cref="IPageFlow">IPageFlow</see> instance to the store.
/// </summary>
/// <param name="pageFlowInstance">The instance to add.</param>
public void Add(IPageFlow pageFlowInstance)
{
Guard.ArgumentNotNull(pageFlowInstance, "pageFlowInstance");
string token = this.tokenProvider.GetCorrelationToken();
this.PageFlowStorage.InsertInstance(pageFlowInstance.Id, pageFlowInstance.Definition.PageFlowType.AssemblyQualifiedName, token, false);
}

/// <overloads>
/// Removes a PageFlow instance from the store.
/// </overloads>
/// <summary>
/// Removes an <see cref="IPageFlow">IPageFlow</see> instance from the store.
/// </summary>
/// <param name="pageFlowInstance">The instance to remove.</param>
public void Remove(IPageFlow pageFlowInstance)
{
Guard.ArgumentNotNull(pageFlowInstance, "pageFlowInstance");
this.Remove(pageFlowInstance.Id);
}

/// <summary>
/// Removes the <see cref="IPageFlow">IPageFlow</see> instance with the appropriate Guid from the store.
/// </summary>
/// <param name="id">The unique identifier of the instance to remove.</param>
public void Remove(Guid id)
{
this.PageFlowStorage.DeleteInstance(id);
}

/// <summary>
/// Returns the <see cref="Guid">Guid</see> of the last instance.
/// </summary>
/// <returns>The unique identifier of the PageFlow instance.</returns>
public Guid GetLastRunningInstance()
{
string token = this.tokenProvider.GetCorrelationToken();
return this.PageFlowStorage.GetLastRunningInstanceByCorrelationToken(token);
}

/// <summary>
/// Retrieves the unique identifier of an <see cref="IPageFlow">IPageFlow</see> instance
/// of the desired <see cref="Type">Type</see>.
/// </summary>
/// <param name="type">The <see cref="Type">Type</see> of PageFlow to retrieve.</param>
/// <returns>The unique idenitifier of an instance of the desired type.</returns>
public Guid GetByType(Type type)
{
Guard.ArgumentNotNull(type, "type");

string token = this.tokenProvider.GetCorrelationToken();
return this.PageFlowStorage.GetInstanceByTypeAndByCorrelationToken(token, type.AssemblyQualifiedName);
}

/// <summary>
/// Retrieves the instance <see cref="Type">Type</see> assembly qualified full name correspnding to an instance id
/// </summary>
/// <param name="id">The <see cref="Guid">Guid</see> of the instance</param>
/// <returns>The assembly qualified name of the <see cref="Type">Type</see> of the instance or null if not found</returns>
public string GetInstanceType(Guid id)
{
PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState.Utils.Guard.ArgumentNotEmptyGuid(id, "id");

return this.PageFlowStorage.GetTypeByInstanceId(id);
}

/// <summary>
/// Marks an <see cref="IPageFlow">IPageFlow</see> instance as the currently running instance.
/// </summary>
/// <param name="pageFlowInstance">The <see cref="IPageFlow">IPageFlow</see> instance to mark as running.</param>
public void SetPageFlowRunning(IPageFlow pageFlowInstance)
{
Guard.ArgumentNotNull(pageFlowInstance, "pageFlowInstance");
string token = this.tokenProvider.GetCorrelationToken();
this.PageFlowStorage.SetRunningInstanceForCorrelationToken(pageFlowInstance.Id, token);
}

/// <summary>
/// Marks an <see cref="IPageFlow">IPageFlow</see> instance as not currently running.
/// </summary>
/// <param name="pageFlowInstance">The <see cref="IPageFlow">IPageFlow</see> instance to mark as not currently running.</param>
public void SetPageFlowNotRunning(IPageFlow pageFlowInstance)
{
Guard.ArgumentNotNull(pageFlowInstance, "pageFlowInstance");
this.PageFlowStorage.ChangeInstanceStatus(pageFlowInstance.Id, false);
}

private Storage PageFlowStorage
{
get
{
Storage pageFlowStorage= HttpContext.Current.Session[this.pageFlowStorageName] as Storage;
if (pageFlowStorage == null)
{
HttpContext.Current.Session[this.pageFlowStorageName] = pageFlowStorage = new Storage();
}
return pageFlowStorage;
}
}
}


Workflow Foundation


The implementation of the page flow engine provided with the Page Flow Application Block is based on Windows Workflow Foundation and assumes that a persistence service is present and calls Unload method of the workflow instance.


So far, the only persistence service supplied with the .NET framework is the SQL persistence service, which is based on SQL Server.


I could have created my own in memory persistence service but, instead, I chose to check if a persistence service is present before calling the Unload method. This was accomplished by replacing:


_instance.Unload();

with:


System.Collections.ObjectModel.ReadOnlyCollection<WorkflowPersistenceService> persistenceServices = _instance.WorkflowRuntime.GetAllServices<WorkflowPersistenceService>();
if ((persistenceServices != null) && (persistenceServices.Count != 0))
{
_instance.Unload();
}

in the Microsoft.Practices.PageFlow.WorkflowFoundation.WorkflowFoundationPageFlow.Suspend and PageFlow.WorkflowFoundation.WorkflowFoundationPageFlowFactory.GetPageFlow methods.


Page Flow Store QuickStart


(This sample supplied with the Web Client Software Factory will be used to test and demonstrate the improvements made to the Page Flow Application Block.)


Having built a database-free page flow application block, all I need now is to configure the application to use the new implementations by editing the web.config file and replacing:


<?xml version="1.0"?>
<
configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<
connectionStrings>
<
add name="PageFlowPersistanceStore" connectionString="Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WCSF_Quickstart;Data Source=.\SQLExpress" providerName="System.Data.SqlClient"/>
</
connectionStrings>
<
pageFlow>
<
pageFlowProvider providerType="Microsoft.Practices.PageFlow.WorkflowFoundation.WorkflowFoundationPageFlowProvider, Microsoft.Practices.PageFlow.WorkflowFoundation"/>
<
hostingWorkflowRuntime Name="Hosting">
<
Services>
<
add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionString="Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WCSF_Quickstart;Data Source=.\SQLEXPRESS;" LoadIntervalSeconds="5" UnloadOnIdle="true"/>
<
add type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" useActiveTimers="true"/>
<
add type="System.Workflow.Activities.ExternalDataExchangeService, System.Workflow.Activities, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</
Services>
</
hostingWorkflowRuntime>
<
pageFlowInstanceStoreProvider providerType="Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary.PageFlowInstanceCorrelationSqlProvider, Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary" connectionString="PageFlowPersistanceStore"/>
<
pageFlowInstanceCorrelationTokenProvider providerType="Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary.CookiePageFlowCorrelationTokenProvider, Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary" />
</
pageFlow>
</configuration>

with:


<?xml version="1.0"?>
<
configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<
connectionStrings>
</
connectionStrings>
<
pageFlow>
<
pageFlowProvider providerType="Microsoft.Practices.PageFlow.WorkflowFoundation.WorkflowFoundationPageFlowProvider, Microsoft.Practices.PageFlow.WorkflowFoundation"/>
<
hostingWorkflowRuntime Name="Hosting">
<
Services>
<
add type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" useActiveTimers="true"/>
<
add type="System.Workflow.Activities.ExternalDataExchangeService, System.Workflow.Activities, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</
Services>
</
hostingWorkflowRuntime>
<
pageFlowInstanceStoreProvider providerType="PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState.PageFlowInstanceCorrelationAspNetSessionStateProvider, PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState" connectionString="PageFlowPersistanceStore"/>
<
pageFlowInstanceCorrelationTokenProvider providerType="Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary.CookiePageFlowCorrelationTokenProvider, PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState" />
</
pageFlow>
</
configuration>

But the sample application still has a dependency to a membership provider that uses SQL Server.


To remove this dependency, I'll use the Login control with the Forms Authentication's user definitions and I'll need to edit the login page and web.config file:


<%@ Page MasterPageFile="~/Shared/QuickStarts.master" Title="Page Flow Store QuickStart - Login" Language="C#" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login" %>

<asp:Content ID="Content1" ContentPlaceHolderID="mainContent" runat="Server">
<h2>Login</h2>
<div>
<p>Enter "user" for the user name and "p@ssw0rd" for the password.</p>
<asp:Login ID="Login1" runat="server" OnAuthenticate="Login1_Authenticate">
</asp:Login>
</div>
</
asp:Content>


public partial class Login : Page
{
protected void Login1_Authenticate(object sender, System.Web.UI.WebControls.AuthenticateEventArgs e)
{
e.Authenticated = FormsAuthentication.Authenticate(Login1.UserName, Login1.Password);
}
}

<?xml version="1.0"?>
<
configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<
connectionStrings>
<!--
removed - <add name="MembershipStore" connectionString="Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WCSF_Quickstart;Data Source=.\SQLExpress" providerName="System.Data.SqlClient"/>-->
</
connectionStrings>
<
system.web>

<authentication mode="Forms">
<!--
added start -->
<
forms>
<
credentials passwordFormat="Clear">
<
user name="user" password="p@ssw0rd"/>
</
credentials>
</
forms>
<!--
added end -->
</
authentication>

<!-- removed - <membership defaultProvider="SqlProvider">
<providers>
<clear />
<add
name="SqlProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="MembershipStore"
applicationName="WCSF_Quickstart"
passwordFormat="Hashed" />
</providers>
</membership>
-->
</
configuration>


Conclusion


And I'm all set to go and develop (or even go to production) without the need for a database.


Resources


12 Responses to Improving The Page Flow Application Block: Removing Database Dependencies

  • Joern says:

    Thanks for the code!!!

    I believe I found a bug:

    private Storage PageFlowStorage
       {
           get
           {
               if (this.pageFlowStorage == null)
               {
                   this.pageFlowStorage = HttpContext.Current.Session[this.pageFlowStorageName] as Storage;
                   if (this.pageFlowStorage == null)
                   {
                       HttpContext.Current.Session[this.pageFlowStorageName] = this.pageFlowStorage = new Storage();
                   }
               }
               return this.pageFlowStorage;
           }
       }

    With this implementation the pageFlowStorage is cached by the PageFlowInstanceCorrelationAspNetSessionStateProvider class. I have not tested it but I believe that there is only one instance of the class per ASP.NET application. Therefore, with this implementation the storage really is tied to the class instance and not the Session object.

    I would implement:

    private Storage PageFlowStorage
       {
           get
           {
               Storage s = HttpContext.Current.Session[this.pageFlowStorageName] as Storage;
               if (s == null)
               {
                       HttpContext.Current.Session[this.pageFlowStorageName] = s = new Storage();
               }
               return s;
           }
       }

    Your implementation works because the provider probably goes out of scope, when the Application unloads. But I guess that is not what you intended. (By the way, caching it on the provider level probably won’t work neither if the Session is stored in a database.)

  • Thanks Joern,

    You’re absolutely right about the code. I don’t know what I was thinking of when I wrote that.

    (By the way, I think it now works with any session state storage, but I haven’t tested it.)

  • Joern says:

    Because I did not want to patch the Microsoft.Practices.PageFlow.WorkflowFoundation.WorkflowFoundationPageFlow.Suspend and PageFlow.WorkflowFoundation.WorkflowFoundationPageFlowFactory.GetPageFlow methods and I felt it would be a good idea to have both providers,

    I implemented a memory persistence service. (Based on your correlation provider.)

    It replaces the SQL persistence service and serializes the workflow state to the ASP.NET session. (In a form that should allow it to be persisted to any database.)

    Just replace the SQL provider in the web.config file with this provider.

    Also, if used in a production environment, it saves memory by persisting the workflow to a compressed format if the workflow is idle – which basically means after each page request. (In my tests the persisted workflow required about 7k in compressed format and 60k in uncompressed format.)

    BTW, GetDefaultSerializedForm() does the compression.

    Here is the code:

    using System;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    using System.Web;
    using System.Workflow.ComponentModel;
    using System.Workflow.Runtime;
    using System.Workflow.Runtime.Hosting;
    namespace Joern.PageFlow.SimpleStorage.SessionState
    {
       /// <summary>
       /// Implements the custom WorkflowPersistenceService, which persists workflow instances in the ASP.NET session.
       /// </summary>
       public class PageFlowStorageSessionStateProvider : WorkflowPersistenceService
       {
           private const string SessionStateStorageName = “PageFlowWorkflowStore”;
           public PageFlowStorageSessionStateProvider()
           {
           }
           #region Serialization
           /// <summary>
           /// Performs the serialization of one activity. Invoked during dehydration.
           /// </summary>
           /// <param name=”id”></param>
           /// <param name=”rootActivity”></param>
           private void SerializeActivity(Guid id, Activity rootActivity)
           {
               byte[] data = WorkflowPersistenceService.GetDefaultSerializedForm(rootActivity);
               SessionStorage.WriteInstanceData(id, data);
           }
           /// <summary>
           /// Performs deserialization of persisted activity. Invoked during rehydration.
           /// </summary>
           /// <param name=”id”></param>
           /// <param name=”outerActivity”></param>
           /// <returns></returns>
           private Activity DeserializeActivity(Guid id, Activity outerActivity)
           {
               byte[] data = SessionStorage.LoadInstanceData(id);
               return WorkflowPersistenceService.RestoreFromDefaultSerializedForm(data, outerActivity);
           }
           /// <summary>
           /// Removes the serialization of an activity. Invoked when the workflow is completed or terminated.
           /// </summary>
           /// <param name=”id”></param>
           private void DeleteActivity(Guid id)
           {
               SessionStorage.DeleteInstance(id);
           }
           #endregion
           #region Provider Members
           #region WorkflowProvider Interface Implementation
           protected override Activity LoadCompletedContextActivity(Guid scopeId, Activity outerActivity)
           {
               //Trace.WriteLine(“Rehydration”);
               object obj = DeserializeActivity(scopeId, outerActivity);
               return (Activity)obj;
           }
           protected override Activity LoadWorkflowInstanceState(Guid instanceId)
           {
               //Trace.WriteLine(“Rehydration”);
               object obj = DeserializeActivity(instanceId, null);
               return (Activity)obj;
           }
           protected override void SaveCompletedContextActivity(Activity activity)
           {
               Guid contextGuid = (Guid)activity.GetValue(Activity.ActivityContextGuidProperty);
               SerializeActivity(contextGuid, activity);
           }
           protected override void SaveWorkflowInstanceState(Activity rootActivity, bool unlock)
           {
               Guid contextGuid = (Guid)rootActivity.GetValue(Activity.ActivityContextGuidProperty);
               WorkflowStatus workflowStatus = WorkflowPersistenceService.GetWorkflowStatus(rootActivity);
               if ((workflowStatus != WorkflowStatus.Completed) && (workflowStatus != WorkflowStatus.Terminated))
               {
                   SerializeActivity(contextGuid, rootActivity);
               }
               else
               {
                   DeleteActivity(contextGuid);
               }
           }
           protected override bool UnloadOnIdle(Activity activity)
           {
               return true;
           }
           protected override void UnlockWorkflowInstanceState(Activity rootActivity)
           {
           }
           #endregion
           #endregion
           private Storage SessionStorage
           {
               get
               {
                   Storage pageFlowStorage = HttpContext.Current.Session[SessionStateStorageName] as Storage;
                   if (pageFlowStorage == null)
                   {
                       HttpContext.Current.Session[SessionStateStorageName] = pageFlowStorage = new Storage();
                   }
                   return pageFlowStorage;
               }
           }
           [Serializable]
           private class Storage : ISerializable
           {
               private SortedList<Guid, StorageItem> items = new SortedList<Guid, StorageItem>();
               /// <summary>
               /// Initializes a new instance of the Storage class.
               /// </summary>
               public Storage()
               {
               }
               #region ISerializable Members
               /// <summary>
               /// Initializes a new instance of the <see cref=”Storage”/> class.
               /// </summary>
               /// <param name=”info”>The info.</param>
               /// <param name=”context”>The context.</param>
               protected Storage(SerializationInfo info, StreamingContext context)
               {
                   this.items = (SortedList<Guid, StorageItem>)info.GetValue(“items”, typeof(SortedList<Guid, StorageItem>));
               }
               /// <summary>
               /// Populates a <see cref=”T:System.Runtime.Serialization.SerializationInfo”></see> with the data needed to serialize the target object.
               /// </summary>
               /// <param name=”info”>The <see cref=”T:System.Runtime.Serialization.SerializationInfo”></see> to populate with data.</param>
               /// <param name=”context”>The destination (see <see cref=”T:System.Runtime.Serialization.StreamingContext”></see>) for this serialization.</param>
               /// <exception cref=”T:System.Security.SecurityException”>The caller does not have the required permission. </exception>
               void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
               {
                   info.AddValue(“items”, this.items);
               }
               #endregion
               internal void WriteInstanceData(Guid instanceId, byte[] data)
               {
                   StorageItem item;
                   if (!this.items.TryGetValue(instanceId, out item))
                   {
                       item = new StorageItem();
                       this.items.Add(instanceId, item);
                       item.InstanceId = instanceId;
                   }
                   item.Data = data;
               }
               internal byte [] LoadInstanceData(Guid instanceId)
               {
                   StorageItem item;
                   if (this.items.TryGetValue(instanceId, out item))
                       return item.Data;
                   return null;
               }
               internal void DeleteInstance(Guid instanceId)
               {
                   if (this.items.ContainsKey(instanceId))
                       this.items.Remove(instanceId);
               }
               [Serializable]
               private class StorageItem
               {
                   public Guid InstanceId;
                   public byte [] Data;
               }
           }
       }

    }

    Feel free to share it with the community.

    Joern

  • Thanks a lot Joem.

    This was my first choice of how to do it, but, due to the lack of time and, mostly, Workflow Foundation knowledge, I took the quick way out.

    I’ll add it to the article and code download as soon as possilbe (I’m heading for TechEd Developers Europe).

  • Mike says:

    Hi Paulo, Joem,

    Good work! Why didn’t they include it out of the box?!

    I tried Joem’s example.

    However changing the sql provider by itself (1 line in the web.config) is not enough.

    Could you send me a working example by E-mail?

    mi.van.engelenDELETEME@DELETEMEinterpolis.nl

    Regards,

    Mike

  • Hi Mike,

    Joem did a great job.

    To make it work in the sample I just replaced:

    <add type=”System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35″ connectionString=”Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WCSF_Quickstart;Data Source=.\SQLEXPRESS;” LoadIntervalSeconds=”5″ UnloadOnIdle=”true”/>

    with:

    <add type=”PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState.WorkflowAspNetSessionStatePersistenceService, PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState”/>

    I’ll post the modified sample as soon as I can.

  • TJ says:

    Thanks for sharing!

    I noticed that the data compression as Joern suggests turned out to be a bit problematic in my case.

    Seems like the compression/decompression occurs each time I access the data. When reading/writing same data multiple times within a single request (like with session you can do), the total time consumed in retrieving/saving the data builds up very high.

    I tried to tackle this problem by commenting out the parts where the compression/decompression occurs and storing an Activity objects instead  (this forced me to change session to InProc mode as PageFlow object is not Serializable).

    But still it seems to take an awful long time to get the property Data a few times:

    public MyObject[] Data
    {
      get
      {
        if (!CurrentPageFlow.UserData.Contains(“MyObject”))
          return null;
        return CurrentPageFlow.UserData[“MyObject”] as MyObject[];
      }
      set { CurrentPageFlow.UserData[“MyObject”] = value; }
    }

    Should I instead store all my objects inside a container and retrieve/store that object  only once per request?

    Also, session enables to do things like this:

    MyObject data = Session[“somekey”];
    data.Property = otherdata; // new Property contents will be saved eventually
    // With pageflow the data doesn’t get stored I set it explicitly
    MyObject data = MyPageFlow.UserData[“somekey”];
    data.Property = otherdata;
    MyPageFlow.UserData[“somekey”] = data; // This is required

    What would be the best approach to make the data be stored at the end of the request execution like session does it?

  • Every time you access the UserData, the underlying workflow of the page flow is loaded, ran and unloaded. Decompression is part of the loading process and compression is part of the unloading process.

    The PageFlow object is not serializable because it doesn’t need to. On each request, the underlying workflow is loaded and wrapped by a new PageFlow instance.

    As you figured out, with the present implementation of the WorkflowFoundationPageFlow, you should touch the UserData property as less as possible.

    I totally agree with you that the underlying workflow should only be loaded and unloaded once per HTTP request. I hope to continue this series of articles about the Page Flow Application Block soon and I’ll try to implement that.

  • TJ says:

    But is there a way to disable compression totally with the current implementation?

    I tried by commenting out these lines in the PageFlowStorageSessionStateProvider (also a few other changes were needed)

    byte[] data = WorkflowPersistenceService.GetDefaultSerializedForm(rootActivity);

    return WorkflowPersistenceService.RestoreFromDefaultSerializedForm(data, outerActivity);

    By doing that I got error messages complaining about PageFlow not beeing serializable and had to switch to InProc mode. Can I also somehow disable compression of the underlying workflow?

    I implemented retrieving all UserData in OnInit and storing in PreRender of a common page base class. Seems to work ok, but I guess there could be a more elegant solution..

  • When Joern says that “GetDefaultSerializedForm() does the compression”, it’s not because he choosed to. It’s becaus that’s how wrokflow serialization works (look it up with Reflector).

    I think your real problem is too much access to UserData in the same request.

  • TJ says:

    But even if I read and write my data to UserData only once per request I can still take away almost 100 milliseconds by commenting out the compression.

    I’m looking forward to the pageflow bundle that doesn’t require a workflow (http://www.codeplex.com/websf/Thread/View.aspx?ThreadId=16781). You don’t happen to know anything about its schedule?

  • Are you sure it’s the compression that is taking the 100 milliseconds? I’m asking because I haven’t measured it yet.

    The WorkflowPersistenceService.GetDefaultSerializedForm does a lot more than just compression the workflow.

    You can activate the workflow traces (runtime, tracking and host) but setting (in your configuration file) the value of the following SourceSwitches:

    • System.Workflow.Runtime
    • System.Workflow.Runtime.Tracking
    • System.Workflow.Runtime.Hosting

    for the TraceSources with the same name.

    Additionally, you can enable the following BooleanSwitches:

    • System.Workflow LogToFile – To output traces to the WorkflowTrace.log file.
    • System.Workflow LogToTraceListeners – To output traces to the registered trace listeners.