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:
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
Published
Monday, October 15, 2007 12:06 AM
by
Paulo Morgado
Filed under: .NET, Architecture, ASP.NET, Community, SoftDev, Microsoft, MSDN, MVP, Web, WCSF, PnP, SoftwareFactories, PageFlow