A crash-course on web application definition through workflow server. Each screen is essentially a processing point (activity) in the workflow. Each activity has a set of possible destinations (links) that connect it to other activities. Additionally, actions within each screen can trigger transitions to other states. That's it (in a very very small nutshell).

Example - I have an application that consists of 3 pages - a login page, a failed password page and a welcome page:

loginflow.jpg

On the definition side, I would have

<activity name="Login">
  <link condition="userHelper.ValidatePassword() == true" destination="Welcome" />
  <link destination="Invalid" />
</activity><activity name="Welcome" />    

<activity name="InvalidPassword">
  <link destination="Login" />
</activity>

From the application, I can invoke actions defined on my activities, without worrying about where those actions take me. This has a several specific advantages:

  • I have a declarative definition of my application, with all the subsequent benefits
  • I can separate a "business action", such as "react to invalid password", from the application action, such as "navigate to InvalidPassword.aspx"
  • All of my possible transitions are defined up-front, and can be validated design-time
  • Additionally, the actual mechanism of transition from activity to activity is controlled by the framework, and not asp.net. This gives me the ability to control what happens when a transition occurs. Specifically, we can introduce interception points - I can say that any time you are present within a specific activity, a condition has to be met. As an example - I can (declaratively) stipulate that prior to being on the Download activity, you have to have visited the Agreement activity, and agreed to my Terms of Use policy

One drawback to this, however, is something that is also the main advantage - application awareness of its components. By defining our application up front, we start running into maintenance/complexity issues when our application gets large enough. Furthermore, what happens if I would like the ability to spread my load accross physical tiers, not in the typical horizontal fashion (the same application replicated on a web farm), but vertically. What I would like to have certain functionalities be performed in one physical location, and others on another? Bare with me here, as I try to muddle my way through the answer - unlike the other posts, this is an idea I'm trying to flesh out, and not a ready solution.

First of - one more piece of information about workflow definitions. Activities can be nested - I can say that there is a login activity, which is actually made up of three activities - login screen, invalid password screen and forgot password screen:

<activity name="Login">
  <activity name="LoginScreen">
    <link condition="userHelper.ValidatePassword() == true" destination="Welcome" />
    <link destination="Invalid" />
  </activity>
  <activity name="Welcome" >
    <link destination="../SomethingElse/stuff" />
  </actvity>
  <activity name="InvalidPassword">
    <link destination="Login" />
  </activity>
</activity>    

<activity name="SomethingElse">
  <activity name="stuff" />
  ...
</activity>

This is for logical grouping purposes only, but comes in very handy. It's a way to organize related functionalities.

With that in mind, let's talk about how we would define our application. If we organize each functional area to be nested within a single activity, then we have a starting point for separation.

partitionedflow.jpg

From the definition standpoint, we can take each piece of the application and stick it in a different place. What's more is that the rest of the application doesn't need to know about the inner workings of that section; all it has to know is that all requests for a particular set of activities need to be routed to wherever they reside:

server a

<activity name="Login">
  <activity name="LoginScreen">
    <link condition="userHelper.ValidatePassword() == true" destination="Welcome" />
    <link destination="Invalid" />
  </activity>
  <activity name="Welcome" >
    <link destination="../SomethingElse/stuff" />
  </actvity>
  <activity name="InvalidPassword">
    <link destination="Login" />
  </activity>
</activity>    

<activity name="SomethingElse" host="server b" />

server b

<activity name="SomethingElse" parent-host="server a">
  <activity name="stuff" />
  ...
</activity>

From a configuration standpoint, this lets us separate individual logical pieces of the application, making them only superficially aware of each other (i.e. they know that a certain logical group exists, and can redirect a request there, but don't know the internal structure thereof). So what happens from a functional standpoint? What happens when we are processing a request and we need to move the processing accross process and machine boundaries? To answer that, we need a bit more background on request processing in workflow apps.

When a request comes in, we determine what activity it is currently in, load the associated UI and Controller elements (think MVC - for every View element, there's a class that's the Controller), and start their processing. During this processing, we can choose to navigate to different areas of the application. However, navigating to a different area does not terminate current processing. Our Controller finishes its lifecycle, returns, and then and only then does the container (framework) evaluate the navigation request.

With that in mind, here's what (I think) we can do. Once processing at a given controller is done, and we're evaluating the next step, we make the determination of whether we're being sent off-machine. If that's the case, we do a server-side request to the other server. We pass along the requested activity, serialize the session and associated state information, and pass that along as well. That way, request processing resumes on the other server. Now, the key here is that the other server doesn't need to know that it's being requested. It's simply processing another workflow request. When it's done, its  (woo! proper usage of its and it's!) result is html. We pass that html back to the requesting server, which passes it down to the browser. The end result is that the server we originally queried gave us back a result that was generated someplace else. As a fun side-effect, the new set of html should have a form with an action that points to the server that actually generated the form, not the server we originally queried. This implicitly transfers subsequent requests to the new server.

Whew. That makes sense... I think. It gives us the ability to split our definitions accross separate servers, defines how we transition between servers, and how we establish affinity to servers. The effectiveness of this, of course, hinges on proper partitioning of the application definition. If every request we make ends up bouncing between servers, our performance will suffer greately.