With all this talk about the Provider Model, it's importance to Whidbey, and all the nifty things you can do with it from the point of view of enabling a pluggable architecture, and the discussions surrounding the extensibility that it can provide an application, I got to thinking. I noticed that, from all points I've seen regarding the Provider Model, it's sole purpose has been to allow developers the ability of specifying to their applications' innerworkings “use this object in this spot to do this one little thing.”
While that's all well and good, I began to think to myself, “what about using the Provider Model to not only extend an application at one place, but to change or control the overall pathway through the application?” In a sense, I'm talking about changing the process by which an application functions - changing the workflow via the Provider Model.
This article will posit such a paradigm and show you, in a step-by-step fashion, just how something like this could be achieved. We'll use the most common example possible - the consumption and use of a generic Person object captured in a web form environment - to exemplify Provider Model as a vehicle for workflow modification. So to begin with, let's state the problem.
There's this web site I have to create that will capture information about visitors. We'll need to capture their first and last names, and email addresses. Then, we'll want to do something with that information, but we want to allow the architecture room to grow while at the same time not requiring much modification to the underlying architecture. In a sense, the whole logical flow should be driven by, and changed by, the provision of new plug-ins that will perform small pieces of functionality.
For starters, we'll need a Person class that the web application will use for entity persistence. The code for the Person class is relatively simple. Note in particular that we've specified this class is a serializable class (we'll use that later on in our first Provider class).
///
<summary>
/// The class we'll operate on.
/// </summary>
[Serializable()]
public class Person
{
public string Firstname;
public string Lastname;
public string Email;
}
Not too much to look at. Then again, we don't need too much to look at, as this is just an entity object that we'll be operating on. The operatee isn't what's important here but rather the operation that actually happens. To define the basic structure of classes which will actually perform operations on Person objects, we'll create a simple interface called IPersonHandler.
///
<summary>
/// Interface all providers will inherit from.
/// </summary>
public interface IPersonHandler
{
Person HandlePerson(Person personInstance);
}
This interface defines the most basic structure of any provider class used by the system. So long as the classes which implement this interface implement the HandlePerson method, the class is golden and should plug right into the architecture with no problem. Notice in particular how the HandlePerson method takes an instance of a Person class and how it also returns an instance of a Person class. We're doing this so that, as we move through the workflow process, each step of the way picks up the previous step's changes to the Person object. In essence, we use the object and pass it along; if one step changes the object, the next step can react to those changes!
As is true with most Provider Model implementations, we'll need to allow developers a very easy way of snapping plugins in and out of the application's architecture. To solve this problem, we'll create our own custom configuration section reader. This reader will basically look through the web.config file, where the type names will be placed in sequential order. To begin with the discussion of this process, take a look at the code for the PersonConfigurationReader class, which implements the IConfigurationSectionHandler interface.
public
class PersonConfigurationReader : IConfigurationSectionHandler
{
public object Create(object parent,
object context,
XmlNode section)
{
XmlNodeList lstActions = section.SelectNodes("//typeName");
string[] types = new string[lstActions.Count];
for(int i=0; i<lstActions.Count; i++)
{
types[i] = lstActions[i].InnerText;
}
return new PersonSettings(types);
}
}
If you take a look at what's going on here, you'll see that we're basically concerned with the section parameter of this method. Looking carefully at this implementation, you'll notice that we expect this node to contain child nodes named “typeName.” In essence, we're going to take the idea of the Provider Model - allow a developer to specify their own Type to provide their own functionality - and extend it by allowing a developer the ability of daisy-chaining multiple Providers together.
The last line of code in our Create method implementation returns an instance of a new Type called PersonSettings. This settings class' code can be seen below. Notice especially how we're passing in a string array; we'll eventually return that array via the TypesInProcess property, which will specify the order in which we'll process a Person class instance.
///
<summary>
/// The settings class, which contains the type name to be loaded.
/// </summary>
public class PersonSettings
{
const string section = "tatochip.com/Person";
internal PersonSettings(string[] types)
{
TypesInProcess = types;
}
public readonly string[] TypesInProcess;
public static PersonSettings GetSettings()
{
PersonSettings b = (PersonSettings)cs.GetConfig(section);
return b;
}
}
Now that we've created the configuration reader and settings classes, let's get back to the implementation of the IPersonHandler interface. We'll take a look at the custom configuration section in a moment; before we specify some types to create and use during the workflow process, we'll first need to create those types' class structures. For starters, take a look at the simplest of all of these Provider classes, the FileBasedPersonHandler class, which basically just serializes a Person class (hence the provision of the Serializable attribute in the Person class' code earlier) to a file on the hard disk.
///
<summary>
/// Serializes the class and saves it to disk.
/// </summary>
public class FileBasedPersonHandler : IPersonHandler
{
public Person HandlePerson(Person personInstance)
{
string filepath = @"C:\" + personInstance.Firstname + "_" + personInstance.Lastname + ".xml";
FileStream fs = null;
fs = new FileStream(filepath,FileMode.Create);
SoapFormatter soap = new SoapFormatter();
soap.Serialize(fs,personInstance);
fs.Close();
return personInstance;
}
}
Not too fancy, but it does a good job of explaining one possible scenario; we'll save each person object to disk for persistence.
So what if that's not good enough? What if we also want to save each Person to a database somewhere? Not to worry, we just create a new Provider class and give it whatever implementation we desire. In the DatabasePersonHandler Provider class, we just execute a stored procedure in a database and save the Person to SQL Server.
///
<summary>
/// Saves the person to the database.
/// </summary>
public class DatabasePersonHandler : IPersonHandler
{
public Person HandlePerson(Person personInstance)
{
SqlConnection cn =
new SqlConnection("Data Source=HRDB; User Id=DBuser; Password=pass4dbUser; Initial Catalog=PersonDB");
SqlCommand cmd = new SqlCommand("uspInsertPerson",cn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@firstName",personInstance.Firstname);
cmd.Parameters.Add("@lastname",personInstance.Lastname);
cmd.Parameters.Add("@email",personInstance.Email);
if(cn.State != ConnectionState.Open) cn.Open();
cmd.ExecuteNonQuery();
if(cn.State != ConnectionState.Closed) cn.Close();
return personInstance;
}
}
Now that we've succesfully provided the application the ability of saving each Person our web site receives to file and/or to a database, we'll add one final piece of functionality - marketing! The EmailPersonHandler Provider class will basically shoot an email to the visitor informing them that their account was created, and that we appreciate their business.
///
<summary>
/// Notifies the person that they're accomodated.
/// </summary>
public class EmailPersonHandler : IPersonHandler
{
public Person HandlePerson(Person personInstance)
{
MailMessage msg = new MailMessage();
msg.Subject = "Person Instance Handled";
msg.Body = personInstance.Firstname + " " + personInstance.Lastname + " was handled succesfully. Thanks for submitting yourself!";
msg.To = personInstance.Email;
msg.From = “noreply@site.com“;
SmtpMail.SmtpServer = "smtp.myserver.com";
SmtpMail.Send(msg);
return personInstance;
}
}
So, we've almost done it. We've created a custom configuration reader, a settings class which informs us about all the types we've got in our process model, and we've created a few Provider classes to start up our own process. The next step is to basically kick the process into gear. We'll do this by creating an Engine class.
The PersonEngine class below does the work for us; it reads in the custom configuration settings, then creates an instance of each type specified in the custom configuration settings, and executes that type's functionality on the Person instance provided to it. Each step of the process persists the Person instance to the next step. Though none of these Provider classes actually make any changes to the Person instance passed in, you most likely get the point (and a few ideas of your own, hopefully!). Take a look at this class, most importantly the Run method, which kicks the whole process off.
///
<summary>
/// Runs the collection of providers on a person instance.
/// </summary>
public class PersonEngine
{
/// we won't allow construction, as the static Run method will be enough.
internal PersonEngine(){}
/// <summary>
/// Executes the collection of providers' functions.
/// </summary>
public static void Run(Person personInstance)
{
// get the name of the type of provider we're supposed to use
string typeName = String.Empty;
PersonSettings settings = PersonSettings.GetSettings();
// set up a local variable to reference the person class
// so we can continue to act on it repetitiously
Person pTmp = personInstance;
for(int i=0; i<settings.TypesInProcess.Length; i++)
{
typeName = settings.TypesInProcess[i];
// create an instance of that type
Type t = Type.GetType(typeName);
// now create an instance of that type
object provider = null;
provider = Activator.CreateInstance(t);
// make sure it's an implementor of the proivider interface
IPersonHandler handler = provider as IPersonHandler;
if(handler != null)
{
// perform it's functionality
pTmp = handler.HandlePerson(pTmp);
}
}
}
}
Now that we've got the vehicle and Provider classes set up, let's take a look at the custom configuration section. First off, we'll need to inform the configuration reading process that “we've got a custom section in here,” and that, in order to know what to do with that section, you'll need to make use of our new custom configuration reader class (the PersonConfigurationReader class from earlier). The first snippet of the web.config code can be seen below.
<?
xml version="1.0" encoding="utf-8" ?>
<
configuration>
<configSections>
<sectionGroup name="tatochip.com">
<section
name="Person"
type="ProviderWorkflow.PersonConfigurationReader, TestSite"
/>
</sectionGroup>
</configSections>
Now that we've informed the runtime that we've got this custom section enclosed later on in a node called “tatochip.com,” and you'll need to use the ProviderWorkflow.PersonConfigurationReader class to know what to do with it. Finally, take a look at that code below, which specifies a complete list of providers to drive the workflow of the application.
<
tatochip.com>
<Person>
<actionList>
<typeName>ProviderWorkflow.FileBasedPersonHandler</typeName>
<typeName>ProviderWorkflow.EmailPersonHandler</typeName>
<typeName>ProviderWorkflow.DatabasePersonHandler</typeName>
</actionList>
</Person>
</tatochip.com>
</configuration>
Now, we create a very simple ASPX page that captures the data from a web visitor using a few texboxes, and kick off the whole process when the user clicks a submit button.
private
void btnSavePerson_Click(object sender, System.EventArgs e)
{
// first build the person according to what was provided from the user
Person p = new Person();
p.Firstname = txtFirst.Text;
p.Lastname = txtLast.Text;
p.Email = txtEmail.Text;
// now process it
PersonEngine.Run(p);
// display confirmation
lblConfirm.Text = "The process has completed.";
}
When the form is submitted, each of the Provider classes specified in the web.config's custom configuration section will be instantiated and the interface implementation residing in each executed. The result? Dynamic functionality that can be altered without the requirement of code modification or recompilation. In essence, we've provided any developer who inherits this application the easiest possible mechanism for extension or expansion. They simply need to implement our IPersonHandler interface, and specify at what stage in the workflow process their own implementation will occur.
Voila! Workflow via the Provider Model!