CHAPTER 6

image

Versioning and Updating Workflows

Up until the release of WF4.5, updating and versioning workflows had its challenges because of the lack of support for managing changes within existing workflows. Even though WF provided a better programming paradigm than using imperative code for modeling ever-changing business processes, support for updating and versioning workflows was badly needed. One of the key contributors that drive the need for managing existing workflows that have been implemented in production is business process maturity. As processes evolve within businesses, software that was developed to model original business processes must be updated to provide new functionality. This can be a hard task for software that models processes that are long-running and are actually in the middle of executing a long-running task when changes need to be made to the software.

This chapter will cover WF4.5’s new features for updating and versioning workflows and will walk through examples for when to use one over the other for managing workflows. Although Chapter 8 is dedicated to covering persistence, some aspects of persisting workflows will be mentioned in this chapter in terms of how they apply to managing versions of a workflow.

Persistence Maturity

Workflow persistence was introduced in Chapter 2 as the mechanism used for storing long-running workflows as they are executed and become idle. Persisting an idle workflow frees up memory resources as the workflow waits. WF has always supported persistence, but as it matured so did its model for how workflows are persisted.

In WF3.x, persisting a workflow included persisting both the instance of the workflow and the workflow definition within a persistence store (see Figure 6-1).

9781430243830_Fig06-01.jpg

Figure 6-1.  Persistence in WF3.x

Persistence in WF4 was changed so that only instance data was stored within a persistence store but not the workflow definition (see Figure 6-2).

9781430243830_Fig06-02.jpg

Figure 6-2.  Storing only instance data within the instance store

This dramatically reduced the amount of data needed to persist workflows compared to WF3.x. It also provided an increase in performance for persisting and rehydrating workflows. The drawback of persisting only instance data in WF4 became noticeable when a workflow was versioned. In WF4, there was no way of knowing which workflow should be used to reload an existing workflow instance. This could cause WF to throw exceptions when a workflow instance was loaded into the wrong workflow definition.

With WF4.5, workflows can be versioned without having to deal with the uncertainty as to which workflow definition a workflow instance should be associated with as it is loaded. A new concept within WF4.5 called WorkflowIdentity handles the correlation between a persisted instance and a workflow definition. Table 6-1 illustrates the properties for a WorkflowIdentity.

Table 6-1. System.Activities.WorkflowIdentity Properties

Property Description
Name Descriptive name for the WorkflowIdentity
Version Establishes the version for the WorkflowIdentity
Package Optional property providing clarity for a workflow definition. A package could be represented as a unique service URI or assembly name.

The WorkflowIdentity is persisted as a part of the persistence store so the persistence model has been slightly modified to implement this correlation within WF4.5. This means that version information can be queried via the persistence store. When tracking is configured for a workflow, WorkflowIdentity data can also be tracked. WorkflowIdentity allows the following new features for workflow execution in WF4.5:

  • Side-by-side
  • Version mismatch
  • Dynamic updates

Side-by-Side Workflow Execution

As business processes evolve and are required to change, there are circumstances when work that has been initiated must complete its execution within the original logic that started it. These are usually long-running processes that were executed before one or more changes to a business process were identified, but the requirement states that any new execution of the business process that the software models must incorporate any new business logic changes. Any executing business logic that was executed before the change is said to be “grandfathered in” and does not follow the updated logic. In the world of WF, changing the workflow model for workflow instances that have already been set in motion or executed can cause problems. For instance, if an approval process has already been started or executed within a workflow, exceptions or unanticipated logical results will occur if the approval process is changed and needs to incorporate new business logic. Consider the approval workflow in Figure 6-3 for candidates applying for a teacher position for the State.

9781430243830_Fig06-03.jpg

Figure 6-3.  Simple application process for reviewing candidates for a teacher position

Running the workflow in Figure 6-3 causes a new candidate application to be submitted for approval. The WCF Test Client is used to host the workflow and expose it as a service. Figure 6-4 shows how a candidate can submit an application for the teacher position. In this case, the application process is kept simple and the only information that is needed to submit an application is the candidate’s name.

9781430243830_Fig06-04.jpg

Figure 6-4.  Hosting the workflow service within the WCF Test Client

After the candidate is submitted, the workflow generates an application number and returns a message indicating that the application has been successfully submitted.

However, after reviewing the current process for approving teacher candidates, it has been determined that only candidates that have more than 4 years of prior experience can have their applications approved (see Figure 6-5).

9781430243830_Fig06-05.jpg

Figure 6-5.  Logic now checks that the candidate has more than 4 years of experience

Figure 6-4 shows that the application ID that was generated from the workflow the last time a candidate submitted an application was 35. Figure 6-6 shows that running the workflow again for ApplicationId 35, but after the workflow is modified, causes the application to be rejected. Even though the teacher application is manually approved by setting the Approval flag to True, the new workflow logic insists that the candidate must have more than 4 years of experience. The workflow is now using the new logic for checking the number of years of experience, but back when the candidate’s application was created, years of experience was not a factor; since the teacher application was “grandfathered in” for simply approving or rejecting an application, years of experience should not be used to determine if the application gets approved or rejected.

9781430243830_Fig06-06.jpg

Figure 6-6.  Modified workflow fails during execution of a persisted instance

The behavior illustrated in Figure 6-6 shows that updating a workflow’s definition after workflow instances have been executed using a previous workflow definition can have undesirable results. WF4.5 takes care of these types of scenarios by allowing workflows to run side by side. This means that workflow instances can still be run using a previous workflow definition rather than having to be run against an updated workflow definition. Let’s walk through the project to get a better understanding of how this is set up.

Working with workflows that are hosted as WCF services is covered in detail in Chapter 12; however, I will explain some of the basics for building workflow services in this chapter as well. The easiest way to host workflows as WCF services is to create a new WCF Workflow Service Application project. The activities included within the default workflow need to be removed. Figure 6-7 indicates that a new ReceiveAndSendReply messaging activity has been added to the workflow and will allow the workflow to be called so candidate applications can be submitted. The OperationName for the messaging activity is set to SubmitApplication. The only parameter that is passed with the SubmitApplication service method is a custom object of type TeachingApplication, illustrated in Listing 6-1. Although it only has two data members, it will be useful for demonstrating side-by-side workflow execution.

Listing 6-1.  TeachingApplication Class Passed as a Parameter into the Workflow

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Web;

namespace Apress.Chapter7
{
    [DataContract]
    public class TeachingApplication
    {
        [DataMember]
        public string FirstName { get; set; }
        [DataMember]
        public string LastName { get; set; }
    }
}

Next, an Assign activity is used to generate a new application ID by assigning it to a randomly generated number using the C# expression of new Random().Next(1, 100).ToString(). This value is then set to a WF variable called holdApplicationId, which will be used to correlate a particular workflow instance when referring to a candidate’s application ID. Although correlation is also covered in Chapter 9 as it associates to uniquely identifying persisting workflows, I want to quickly mention how it is being used. The InitalizeCorrelation activity will take the value stored in the holdApplicationId variable and use it to correlate workflow instances. An application ID will then be used to call a workflow instance so it can be executed again after is has gone idle and persisted within the SQL Server persistence store. Figure 6-7 illustrates how the SendReply activity is used to send a message back from the workflow indicating that an application ID has been generated and that the application has been received.

9781430243830_Fig06-07.jpg

Figure 6-7.  Implementing the application submission for a teacher position

Next is the approval process of the workflow. Figure 6-8 illustrates that another ReceiveAndSendReply messaging activity is being used to indicate that a decision is being made to either approve or reject a candidate’s application. The Receive activity has its OperationName property set to ApproveTeacher and it accepts two parameters, ApplicationId and Approval. ApplicationId provides correlation, which has been configured for associating a workflow instance that has been persisted. The Approval parameter is Boolean type to indicate whether the teacher has been approved or rejected. Finally, a SendResponse activity is used to pass the message that the candidate application has been approved or rejected. The If activity shows the original logic that does not account for a candidate’s years of experience.

9781430243830_Fig06-08.jpg

Figure 6-8.  Logic for approving or rejecting a candidate’s application

Listing 6-2 shows the contents of web.config that have been updated to allow persistence to be configured using SQL Server as the workflows go idle.

Listing 6-2.  Configuring Persistence within the Project’s Web.config File

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" strict="false" explicit="true" targetFramework="4.5"/>
    <pages controlRenderingCompatibilityVersion="4.0"/>
  </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <sqlWorkflowInstanceStore connectionString="Server=HYPERVWINSEVEN2;Database=WFPersist;Trusted_Connection=yes"
hostLockRenewalPeriod="00:00:30" runnableInstancesDetectionPeriod="00:02:00"
instanceCompletionAction="DeleteAll" instanceLockedExceptionAction="AggressiveRetry"
instanceEncodingOption="GZip"/>

          <workflowIdle timeToPersist="00:00:05" timeToUnload="00:00:30"/>
          <!-- To avoid disclosing metadata information, set the values below to false before deployment -->
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
</configuration>

Adding Definition Identities

At this point a workflow can be versioned so there is no confusion about which persisted workflow instance should be applied to a particular workflow definition. Figure 6-9 illustrates two different versions of a workflow can be run side by side for managing long-running workflows.

9781430243830_Fig06-09.jpg

Figure 6-9.  Running workflow versions side by side

Setting up the versions of a workflow that are supported by a workflow host can be accomplished either through code or configured using the WF designer.

Versioning Through Code

Listing 6-3 shows the code used to set the latest version of the workflow service that should be hosted using the WorkflowServiceHost. By calling CurrentWorkflowService(), the version of WorkflowService is set to 2.0.0.0 using the DefinitionIdentity property, which is of type WorkflowIdentity described earlier in in Table 6-1. The WorkflowServiceHost has a new SupportedVersions property, which is of type ICollection<WorkflowService>. After the WorkflowServiceHost is initialized, each additional version of the workflow it supports is also added through the WorkflowServiceHost SupportedVersions property.

Listing 6-3.  Supporting Multiple Workflow Versions Through Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Activities;
using System.Web;
using System.Activities;
using System.Collections.ObjectModel;

namespace Apress.Chapter7
{
    public static class HostWorkflowService
    {
        public static void StartServiceHost()
        {
            using (WorkflowServiceHost wfServiceHost
                        = new WorkflowServiceHost(CurrentWorkflowService(),
                            new Uri("http://localhost:8080/EquipmentRentalService")))
            {
                var supportedServices = SupportedWorkflowServices();
                foreach(var wfService in supportedServices)
                {//add each supported version
                    wfServiceHost.SupportedVersions.Add(wfService);
                }

                wfServiceHost.Open();
            }
        }

        public static WorkflowService CurrentWorkflowService()
        {
            var v2Workflow = new WorkflowService
            {
                Body = new TeachingApplicationService(),
                DefinitionIdentity = new WorkflowIdentity
                {
                    Name = "SimpleApplication",
                    Version = new Version(2, 0, 0, 0) //set the current version of the workflow
                }
            };

            return v2Workflow;
        }

        public static Collection<WorkflowService> SupportedWorkflowServices()
        {
            var services = new Collection<WorkflowService>();

            var v1Workflow = new WorkflowService
            {
                Body = new TeachingApplicationService(),
                 DefinitionIdentity = new System.Activities.WorkflowIdentity
                {
                    Name = "SimpleApplication",
                    Version = new Version(1, 0, 0, 0) //set the initial version of the workflow
                }
            };

            services.Add(v1Workflow);

            var v15Workflow = new WorkflowService
            {
                Body = new TeachingApplicationService(),
                DefinitionIdentity = new System.Activities.WorkflowIdentity
                {
                    Name = "SimpleApplication",
                    Version = new Version(1, 5, 0, 0) //set the updated minor version of the workflow
                }
            };

            services.Add(v15Workflow);
            
            return services;
        }
    }
}

Versioning Workflow Applications

Workflows that are not intended to be delivered as WCF services can be versioned in a similar way. Listing 6-3 illustrates the WorkflowServiceHost versioning hosted workflow services, but the WorkflowApplication can also be used for versioning workflows hosted within applications, as shown in Listing 6-4.

Listing 6-4.  Setting the Versions for a Workflow Hosted Through WorkflowApplication

WorkflowIdentity v1WorkflowIdentity = new WorkflowIdentity
{
    Name = "SimpleApplication",
    Version = new Version(1, 0, 0, 0) //set the current version of the workflow
};

WorkflowApplication wfApp = new WorkflowApplication(new TeachingApplication(),v1WorkflowIdentity);

// Setup the WorkflowApplication
WorkflowApplicationFactory(wfApp);

// Execute the workflow.
wfApp.Run();

After the workflow goes idle and is persisted, at a later time the workflow can be reloaded from its persisted store. As the workflow is reloaded, the same WorkflowIdentity properties used when the workflow was persisted must be used for loading the workflow. If the WorkflowIdentity is different, then a VersionMismatchException is thrown. Listing 6-5 illustrates the contents for the message based on the WorkflowIdentity set in Figure 6-4.

Listing 6-5.  Error Message Thrown When a Version Is Loaded with the Wrong Version

The WorkflowIdentity ('SimpleApplication; Version=1.0.0.0') of the loaded instance does not match the
WorkflowIdentity ('SimpleApplication; Version=2.0.0.0') of the provided workflow definition. The
instance can be loaded using a different definition, or updated using Dynamic Update.

A new object called WorkflowApplicationInstance is returned while retrieving a persisted workflow instance using WorkflowApplication.GetInstance. It has a DefinitionIdentity of type WorkflowIdentity that can be used to check that that the workflow definition version is being used.

Configuring Versioning within Visual Studio

Figure 6-10 illustrates how Visual Studio can be used to configure workflow versions through the WF designer. The new property of DefinitionIdentity is being set within Visual Studio to a WorkflowIdentity. The workflow version has been set to 1.0.0.0 and it has been given the name SimpleApplication. After setting these properties of the WorkflowIdentity, the workflow can be run.

9781430243830_Fig06-10.jpg

Figure 6-10.  Persisting a workflow instance within SQL Server

To check that the workflow has been properly persisted, SQL Server Management Studio can be used to connect to the database used for persisting workflow instances. The System.Activities.DurableInstancing.Instances view can be run to view persisted workflow instances. Figure 6-11 indicates that the workflow instance has been persisted and that the version of the workflow definition has been stored.

9781430243830_Fig06-11.jpg

Figure 6-11.  Persisting the version for a particular workflow definition

The business now mandates that the workflow must be changed. The first thing to do is copy the workflow and move it within the App_Code folder of the project. In this case, another folder called SimpleApproval is created and used to hold older versions of the same type of workflow (older versions, in other words). Although Figure 6-12 only shows the workflow file v1SimpleApproval.xamx, other versions of the same type of workflow can be added as well.

9781430243830_Fig06-12.jpg

Figure 6-12.  Copying the older version of the workflow within the project

The original workflow, SimpleApproval, can have its version updated to 2.0.0.0 by changing the workflow’s DefinitionIdentity property for the root of the workflow. The workflow can now be updated to implement the logic within Figure 6-5, which mandates that a candidate must have more than 4 years of experience. The code in Listing 6-1 must accommodate a new YearsOfExperience property:

[DataMember]
public int YearsOfExperience {get; set;}

This time, as the updated workflow is run, the new property, YearsOfExperience, can be set to indicate the years of experience for the candidate. In this case, Linda Owen only has 3 years of experience, as shown in Figure 6-13.

9781430243830_Fig06-13.jpg

Figure 6-13.  Passing into the workflow years of experience

Now two workflow instances have been persisted, but the second persisted instance indicates that it uses version 2.0.0.0 of the SimpleApplication workflow (see Figure 6-14).

9781430243830_Fig06-14.jpg

Figure 6-14.  Different versions of the same workflow have been persisted

When the workflow is run again, the logic will check that the candidate has more than 4 years of experience even if the candidate is approved, as illustrated in Figure 6-15. The workflow instance will then be removed from the persistence store by the WF runtime because the workflow will have completed.

image Tip   Figure 6-14 shows that the persistence store in WF4.5 now has an IdentityName and columns that correlate to the version of the workflow definition that was used to execute a workflow instance. Chapter 8 includes a lab that shows how the database view in Figure 6-14 should be queried through code for making decisions based on the versions of names of workflow instances that have been persisted.

9781430243830_Fig06-15.jpg

Figure 6-15.  Candidate is rejected because of insufficient years of experience

Now the other workflow instance can be run using the previous version of the workflow, which does not factor in years of experience for approval. Figure 6-16 illustrates that the candidate application has been approved without taking into account the years of experience.

9781430243830_Fig06-16.jpg

Figure 6-16.  Running a different version of a workflow at the same time

image Caution  Previous versions of a workflow must be copied within folders that have the same name as the original workflow and placed within the App_Code folder for the project. Figure 6-11 illustrates how this should be done.

Updating Running Workflow Instances

Running workflows side by side is great when a long-running workflow instance has already been executed and needs to finish executing an original version of the workflow definition, even after the workflow definition has been updated and new workflow instances have been executed. However, sometimes workflow instances executed with an earlier workflow definition need to be updated to directly reflect updates made to a new version of the workflow. This scenario is different than running different workflow instances with different versions of a workflow. In this case, the workflow definition needs to run with an updated workflow definition of the version of the workflow definition that originally executed it.

A common example of updating existing workflow instances to run under an updated workflow definition arises when an original workflow contains bugs or a business process mandates that a process must be changed even after the workflow instance has been executed. Let’s take a look at what happens if a workflow is updated after a workflow instance has been initiated through a previous version of a workflow. Consider the workflow orchestrated through code in Listing 6-6.

Listing 6-6.  Simple Workflow Defined Through Code

var wf = new Sequence
            {
                Activities =
               {
                  new WriteLine()
                  {
                    Text = "Started a new workflow..."
                  },
                 new WriteLine()
                  {
                    Text = "Time to persist the workflow..."
                  },
                 new Delay()
                  {
                    Duration = new TimeSpan(0, 0, 5)
                  },
                  new WriteLine()
                  {
                    Text = "Workflow is about to complete..."
                  }
               }
            };

Once a workflow instance is initiated through this workflow and becomes persisted, as it becomes idle, the workflow definition cannot be updated. The following line of code

wf.Activities.Add(new WriteLine() { Text = "Ok workflow can finish!" });

adds a new WriteLine activity at the end of the workflow in Listing 6-5. If the workflow is rehydrated from the persisted store so it can complete, the WF runtime will throw the error message illustrated in Figure 6-17. The error indicates that the updated workflow cannot be used to run an existing workflow instance, therefore the workflow instance must be dynamically updated to incorporate the new WriteLine activity that was added to the workflow.

9781430243830_Fig06-17.jpg

Figure 6-17.  Running a workflow instance with an updated workflow

The next couple of sections will explain the steps required for dynamically updating workflow instances. Therefore, when workflow instances have become idle and are persisted, they can be executed without throwing exceptions.

Step 1: Preparing the Update Map

The first step that is required before dynamically updating a workflow is to map the changes from the original workflow to a workflow with an updated implementation. Consider the update map to be the delta or difference between the two workflows. Before a workflow can be updated in WF4.5, a delta must be prepared. WF4.5 has a new namespace called System.Activities.DynamicUpdate, and it includes the class DynamicUpdateServices, which provides functionality for dynamically updating a workflow definition. Before the delta can be created, the original workflow must be prepared for the update or change. The method DynamicsUpdateServices.PrepareForUpdate must be called on the workflow every time before it can be updated. The PrepareForUpdate method accepts a parameter of either Activity or ActivityBuilder type, and it will duplicate the original workflow from which it was created. The duplicated workflow is then automatically attached to the original workflow (see Listing 6-7).

Listing 6-7.  Preparing the Workflow to be Updated

var wf = new Sequence
            {
                Activities =
               {
                  new WriteLine()
                  {
                    Text = "Started a new workflow..."
                  }
               }
            };

DynamicUpdateServices.PrepareForUpdate(wf);

Step 2: Apply the Update Map

After a workflow has been prepared to be updated, it can be modified to reflect new business logic. Figure 6-18 illustrates that the Version 1 workflow has initiated a new workflow instance that has been persisted. As a new version of the workflow called Version 2 is updated, the changes made to Version 2 are mapped so the persisted workflow instance is aware of the changes that have been made between the two versions of the workflow.

9781430243830_Fig06-18.jpg

Figure 6-18.  Mapping dynamic updates to a workflow

To map the changes made to a workflow, DynamicUpdateServices.CreateUpdateMap must be called. Imagine that the workflow in Figure 6-6 needs to be modified. After the workflow has been prepared, as illustrated in Figure 6-6, the workflow can be updated by adding a new WriteLine activity using the following code:

wf.Activities.Add(new WriteLine() { Text = "Ok workflow can finish!" });

Once the workflow definition is updated, a map of the changes made between the two different workflows can be created using the following code:

DynamicUpdateMap wfMap = DynamicUpdateServices.CreateUpdateMap(wf);

Step 3: Updating the Workflow Instance

Now that the workflow has been prepared for changes, changes have been made to the workflow, and a DynamicUpdateMaphas been created to map the changes between the two workflows, the last step is to update the workflow instance that has been loaded within the persistence store. The WorkflowApplicationInstance is another new object provided with WF4.5 that provides a DefinitionIdentity property of type WorkflowIdentity. This property assists in the versioning of workflow instances and the code in Listing 6-8 indicates how it is used to get a particular workflow instance. The CurrentInstance parameter of type Guid indicates the workflow to retrieve from the persistence store that is specified by the parameter CurrentPersistenceStore. In WF4.5, the WorkflowApplication host has also been updated and now allows a DynamicUpdateMap object to be passed in when calling its Load method. After loading the WorkflowApplicationInstance, the persisted workflow instance is updated using the DynamicUpdateMap so it can be executed against the updated workflow definition.

Listing 6-8.  Updating the Persisted Workflow Instance

WorkflowApplicationInstance wfApplicationInstance = WorkflowApplication.GetInstance(CurrentInstance, CurrentPersistenceStore);
wfApplication.Load(wfApplicationInstance, wfMap);
wfApplication.Run();

Saving a DynamicUpdateMap to File

Before the code is called in Listing 6-8, the DynamicUpdateMap is created by calling DynamicUpdateServices.CreateUpdateMap. The new DynamicUpdateMap is then immediately used to update a persisted WorkflowApplicationInstance so it can use the updated workflow definition the next time the workflow instance is reloaded from the persistence store. But what happens if one or more persisted instance cannot be updated immediately? In this scenario, a workflow is updated, but there might be a certain procedures in place for making changes in a production environment that contains persisted workflow instances. In a situation like this, the DynamicUpdateMap must be saved to the file system so it can be used later for updating persisted workflow instances after the procedures are followed for modifying the production environment (see Listing 6-9).

Listing 6-9.  Saving the DynamicUpdateMap to Disk

public void SaveUpdateMap(DynamicUpdateMap map, string fileName)
        {
            var path = System.IO.Path.ChangeExtension(fileName, "map");
            DataContractSerializer serialize = new DataContractSerializer(typeof(DynamicUpdateMap));
            using (FileStream fs = File.Open(path, FileMode.Create))
            {
                serialize.WriteObject(fs, map);
            }
        }

The code in Listing 6-7 can be updated so that the update map can be retrieved from disk at a later time to be used for updating persisted workflow instances (see Listing 6-10).

Listing 6-10.  Retreiving the Update Map from Disk

//Retrieve the update map from disk
DataContractSerializer serializer = new DataContractSerializer(typeof(DynamicUpdateMap));
            using (FileStream fs = File.Open(@"C:MyWorkflowUpdateMap.xml", FileMode.Open))
            {
                updateMap = serializer.ReadObject(fs) as DynamicUpdateMap;
            }
//use the new update map to
wfApplication.Load(wfApplicationInstance, updateMap);

Decoupling Workflow Implementation from Workflow Updates

This section will demonstrate how a workflow application can be decoupled from a different application that prepares a workflow to be updated. A separate application can be used to create an update map that is used to update a workflow dynamically at a later point in time. In this scenario, a workflow that models a movie rental process will be built. Later I will show how the rental process can be updated dynamically so that customers who have movies still rented are able to experience added functionality within the rental process.

Renting movies has changed quite a bit in the last few years, so the process will be based on those movie rental machines that have become quite popular. I recently had my first experience renting a movie from a machine rather than walking into a store, and I thought to myself that this scenario would be a fun exercise to model with WF. Here are the steps I took to rent a movie.

  1. Searching for one or more movies.
  2. Confirming when I was done searching for movies.
  3. Inserting my credit card to pay for selected rentals.
  4. Confirming my rental order.

Once I had finished watching the rented movies, I brought them back to the rental machine and entered my credit card so the rental machine knew that I had returned the movies; my credit card was charged the amount of the rentals.

Now that the steps for renting a movie have been identified, the next thing to do is to model the process. Since this process is mainly human driven, a state machine control flow will be used.

9781430243830_Fig06-19.jpg

Figure 6-19.  State machine control flow that models a movie rental process

Figure 6-19 illustrates the state machine that will be used to model the movie rental process, and the flow models each of the steps mentioned for renting a movie. When the workflow is run, the first state that the workflow will execute is the MovieSearch state.

Setting Up the Workflow

While the current state of the workflow is MovieSearch, there are two transitions that can be made by the customer:

  • DoneSearching
  • SelectAMovie

Each of these transitions add a System.Activities.Statements.Transition to the MovieSearch System.Activities.Statement.State, and each transition uses the custom activity of WaitForResponse to set up a bookmark for each transition (see Listing 6-11).

Listing 6-11.  WaitForResponse Custom Activity

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;

namespace Apress.Chapter7.Activities
{
    public sealed class WaitForResponse<TResult> : NativeActivity<TResult>
    {
        public WaitForResponse()
            : base()
        {

        }

        public string ResponseName { get; set; }

        protected override bool CanInduceIdle
        { //override when the custom activity is allowed to make he workflow go idle
            get
            {
                return true;
            }
        }

        protected override void Execute(NativeActivityContext context)
        {
            context.CreateBookmark(this.ResponseName, new BookmarkCallback(this.ReceivedResponse));
        }

        void ReceivedResponse(NativeActivityContext context, Bookmark bookmark, object obj)
        {
            this.Result.Set(context, (TResult)obj);
        }
    }
}

Figure 6-20 illustrates that the WaitForResponse activity has been added within the Trigger section of the SelectAMovie transition and the object type Movie will be passed through the bookmark from the workflow host to make the transition occur. This event will indicate that the customer has made their first movie selection. Listing 6-12 indicates the properties associated with the Movie object:

  • MovieName
  • Rating
  • Price

9781430243830_Fig06-20.jpg

Figure 6-20.  Setting up the SelectAMovie transition

Listing 6-12.  Movie Class That Identifies Properties for a Movie

using System;
using System.Collections.Generic;
using System.Linq;

using System.Text;
using System.Threading.Tasks;

namespace MovieRental.DataModel
{
    [Serializable]
    public class Movie
    {
       public string MovieName { get; set; }
       public string Rating { get; set; }
       public Decimal Price { get; set; }
    }
}

Since the SelectAMovie transition points to the same State MovieSearch, the event SelectAMovie can be fired multiple times indicating that a customer can rent as many movies as they would like for a rental order.

The bookmark’s ResponseName property indicates the name of the bookmark that needs to be called from the workflow host and the Result property that is used to set an existing property or argument within the workflow. The workflow has a property called holdSelectedMovie of type Movie, and this property accepts the value that is passed into the workflow so it can be used to process logic. The Condition section of the transition checks that the workflow property holdSelectedMovie is not null based on the Movie object that was passed in from the workflow host (see Figure 6-20).

Finally, an AddToCollection activity is added to create a collection of movies that the customer intends to rent. Figure 6-21 indicates the properties that are set within the activity.

9781430243830_Fig06-21.jpg

Figure 6-21.  Setting up the SelectAMovie transition

There is a TypeArgument property that is set to type Movie. The Item property is set to holdSelectedMovie for the movie that is passed in through the bookmark, and the Collection property is used to hold a collection of movies. This property is set to holdNewRental.Movies, and holdNewRental is another workflow property of type CustomerRental that is indicated in Figure 6-20. Listing 6-13 shows the CustomerRental class.

Listing 6-13.  CustomerRental Class

using System;
using System.Collections.Generic;
using System.Linq;

using System.Text;
using System.Threading.Tasks;

namespace MovieRental.DataModel
{
    [Serializable]
    public class CustomerRental
    {
        public CustomerRental()
        {
            Movies = new List<Movie>();
        }
        
        public Guid RentalId { get; set; }
        public List<Movie> Movies { get; set; }
        public CreditCard PaymentCard { get; set; }
       }
}

The other transition, DoneSearching, also uses a WaitForResponse activity; however, it is not intended to do much other than indicate when a customer is done searching and selecting movies to rent. The WaitForResponse accepts a Boolean type that will be passed in from the workflow host to the workflow (see Figure 6-22).

9781430243830_Fig06-22.jpg

Figure 6-22.  Setting up the DoneSearching transition

Once the customer is done selecting movies to be rented, the next state that is activated is CompletedMovieSearch. Once CompletedMovieSearch is set as the current state for the workflow, a customer can then insert their credit card to process and confirm the order. The InsertCard transition is initiated from the workflow host once the customer is ready to process the order. The WaitForResponse activity for this transition accepts a CreditCard object and sets it to holdNewRental.PaymentCard. There is also a custom activity that inherits from CodeActivity<T>, which simulates processing a credit card. If this was real, a third-party reference could be made and the code that uses the third party code could be added here. Instead, Listing 6-14 shows the custom activity code for RunCreditCard that simulates a credit card being successfully run by returning a transaction number and assigning the number to the CreditCard object that was passed in. The RunCreditCard activity accepts a CreditCard object and then returns the object with a hard-coded transaction number.

Listing 6-14.  RunCreditCard Custom Activity

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Activities;
using MovieRental.DataModel;

namespace Apress.Chapter7.Activities
{
    [Serializable]
         public class RunCreditCard : CodeActivity<CreditCard>
    {
         [RequiredArgument]
         public InArgument<CreditCard> inCreditCard { get; set; }
         protected override CreditCard Execute(CodeActivityContext context)
         {
             var ccWithTransNo = inCreditCard.Get(context);
             ccWithTransNo.TransactionNumber = 1542514612;
             return ccWithTransNo;
         }
    }
}

The CreditCard object used is indicated in Listing 6-15, which contains the property TransactionNumber. It also implements IEquatable<T> so a CreditCard object can be compared to another CreditCard object based on certain its properties. The Equals function purposely omits checking the TransactionNumber because later when a customer comes back to return the movies, the same credit card that was entered to process the order will be used to check that it was the same credit card charged for the rental.

Listing 6-15.  CreditCard Class Used to Pass Credit Card Data to the Workflow

using System;
using System.Collections.Generic;
using System.Linq;

using System.Text;
using System.Threading.Tasks;

namespace MovieRental.DataModel
{
    [Serializable]
    public class CreditCard:IEquatable<CreditCard>
    {
        
        public string CCNumber { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int ExpireMonth { get; set; }
        public int ExpireYear { get; set; }
        public int TransactionNumber  { get; set; }
        public bool Equals(CreditCard other)
        {
            if (this.CCNumber == other.CCNumber
                && this.ExpireMonth == other.ExpireMonth
                && this.ExpireYear == other.ExpireYear
                && this.FirstName == other.FirstName
                && this.LastName == other.LastName)
                return true;
            else
                return false;
        }
    }

After the customer’s credit card is charged, a Persist activity is used to persist the workflow instance within the persistence store in SQL Server; this way memory is released for customer movie rental until the customer returns the movies, allowing other customer rental orders to be processed. The Condition section for the transition checks that a transaction number has been added and that it has been charged (see Figure 6-23).

9781430243830_Fig06-23.jpg

Figure 6-23.  Setting up the DoneSearching transition

Once the customer decides to return the movies, they will enter the same credit card they used to place the movie rental. The transition ReturnMovie is used to indicate that a customer has entered their credit card by using another WaitForResponse activity. The activity accepts the CreditCard object and sets it to another workflow property called holdReturnMovieCard. Although the ResponseName is also set to ReturnMovie, which is the same name as the transition, it does not have to be. The condition for the transition uses the CreditCard.Equals function to check that the CreditCard object that was passed in is the same CreditCard object that was used to process the rental, but of course the Equals function does not care about the TransactionNumber property since the CreditCard object that is passed in to return the movie will not have a transaction number set (Figure 6-24).

9781430243830_Fig06-24.jpg

Figure 6-24.  Returning rented movies and passing in the credit card to the transition

The last state of the workflow uses a FinalState state. This indicates the last state of the workflow and also sets the OutArgument OutMovieRental associated to the workflow. Here the holdNewRental CustomerRental type is set to OutMovieRental argument, which is also a CustomerRental type. The OutMovieRental argument will be returned back to the workflow host to indicate that the workflow has completed and specific properties were set for the argument inside the workflow (see Figure 6-25).

9781430243830_Fig06-25.jpg

Figure 6-25.  Returning movies rented and passing in the credit card to the transition

image Note   To understand how to set up a persistence store within SQL Server, please review Chapter 8.

Creating a Workflow Host

Now that the workflow has been built to create movie rentals, a workflow host needs to be built to make sure the workflow works as intended. This workflow relies on using the WF runtime to manage workflow execution like persistence and versioning. Since the workflow will execute solely within the rental machine’s software, the WorkflowApplicationHost will be used to manage the workflow as a hosted application. Figure 6-26 shows the steps that were defined within the workflow, and now code needs to be written to interact with the workflow and test its execution before the solution can be loaded onto the rental machines hardware.

9781430243830_Fig06-26.jpg

Figure 6-26.  Simple workflow host used to test the workflow

When the application to simulate how a customer will rent one or more movies is started using the workflow illustrated in Figure 6-26, a new rental is initiated when the “New Rental” button is selected. The next step is to add a movie that a customer would like to rent. After the movie is entered then the “Select Movie” button is pressed to store the movie selection. Remember that the workflow in Figure 6-19 is built so that multiple movies are allowed to be rented at one time; however, after the customer has decided that they are done adding movies to rent, the “Selection Complete” button is pressed.

At this point payment is required, so the customer needs to select the “Insert Card” button, which will take the credit card information and process the card. Finally, the workflow is unloaded from the WF runtime when the customer selects the  “Finish” button. At this point the application can stop its execution to simulate a later time when the customer will bring back the movies. This workflow is considered to be long-running because a customer may take hours or even days to return the one or more of the rented movies.

To simulate a customer returning one or more movies, the Guid that was generated through the persistence store is used to track the original rental. The Guid is then added to the RentalTransactionId textbox and the button called “Insert Card to Return Movie” is then pressed to complete the workflow. When the workflow has completed, a pop-up window indicates that the movie has been returned. Figure 6-27 illustrates how workflow instances can be monitored using the Server Explorer within VS2012 to see the Instances database view that is created within the SQL Server persistence store.

9781430243830_Fig06-27.jpg

Figure 6-27.  Using the Instances view to monitor workflow instances

image Tip   The persistence store used in this example does not require a full-blown instance of SQL Server to be installed since it is being used for testing. Of course, a licensed production version of SQL Server would normally be required for a production scenario. Figure 6-27 illustrates that SQL2012’s LocalDB is being used. This version of SQL Server is extremely lightweight and easy to use.

The next few pages will walk through the code used to host the movie rental workflow. The custom workflow host application in Figure 6-26 is a simple WPF application that is its own project within the solution. The application uses the namespaces illustrated in Listing 6-16. The using statements after System.Activities indicate some of the references that are required to be made to the project.

Listing 6-16.  Initiating the Workflow Host Application

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using System.Activities;
using System.Runtime.DurableInstancing;
using System.Activities.DurableInstancing;
using System.Threading;
using MovieRental.DataModel;
namespace Apress.Chapter7.Host
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private WorkflowApplication _wfApp;
        private SqlWorkflowInstanceStore _instanceStore;

        public MainWindow()
        {
            InitializeComponent();
            CreatePersistenceStore();
        }

The SqlWorkflowInstanceStore is initiated first as a private variable to be used throughout the code; as the class loads, its constructor creates and configures the persistence store defined as the private variable. To initialize the SqlWorkflowInstanceStore, its ConnectionString property needs to be set. Optionally its InstanceCompletionAction property can be set, and for the application it is set to InstanceCompletionAction.DeleteNothing. This setting indicates that after a workflow instance persists and later completes, it will not be removed from the persistence store (see Listing 6-17).

Listing 6-17.  Configuring the Persistence Store

private void CreatePersistenceStore()
        {
            try
            {
                _instanceStore = new SqlWorkflowInstanceStore();
                _instanceStore.ConnectionString =
                    @"Data Source=(LocalDB)v11.0;Initial Catalog=WFPersist;Integrated Security=True";
                _instanceStore.InstanceCompletionAction = InstanceCompletionAction.DeleteNothing;
            }
            catch (Exception)
            {
                throw;
            }
        }

After the persistence store is configured, the WorkflowApplication is then instantiated. Listing 6-18 illustrates that after instantiating the MovieRentalProcess workflow as an Activity object, it is passed into a new WorkflowApplication as the workflow that will be managed by the WF runtime. In WF4.5, a WorkflowApplication object now accepts a WorkflowIdentity object. Here the Name property is set to a meaningful name and a new Version object indicating the current version of the workflow.

Since the WorkflowApplication allows workflow instances to be run asynchronously, this application will only run workflow instances using the same thread that the WPF application uses. This is enforced by setting the WorkflowApplication object’s SynchronizationContext property to SynchronizationContext.Current. The WorkflowApplication object’s InstanceStore property is also set to use the persistence store that was set up in Listing 6-18.

Listing 6-18.  Configuring the WorkflowApplication Variable

private void InitiateWorkflowRuntime()
        {
            try
            {
                Activity rentalWorkflow = new MovieRentalProcess();
                _wfApp = new WorkflowApplication(rentalWorkflow,
                    new WorkflowIdentity
                    {
                        Name = "v1MovieRentalProcess",
                        Version = new System.Version(1, 0, 0, 0)
                    });
                _wfApp.SynchronizationContext = SynchronizationContext.Current;
                _wfApp.OnUnhandledException = OnUnhandledException;
                _wfApp.Completed = OnWorkflowCompleted;
                _wfApp.Idle = OnWorkflowIdle;
                _wfApp.InstanceStore = _instanceStore;
                _wfApp.Unloaded = OnWorkflowUnloaded;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

Next, a couple of WF runtime events are also wired up using the WorkflowApplication events. For instance, OnUnhandledExeption is an important delegate to wire up because it is important to know when a workflow instance has thrown an exception. I cannot stress the amount I have wasted while building workflow applications in the past without wiring up this delegate and not knowing when a workflow instance was failing. The OnWorkflowCompleted event fires when a workflow instance finishes, of course. An OnWorkflowIdle event fires while the workflow waits, and OnWorkflowUnloaded fires once the workflow is unloaded from the WF runtime. In some cases, like in Listing 6-19, it is not important to add code; however, it is nice to have the event fire when these different events occur.

Listing 6-19.  Declaring How the WF Runtime Events Will Be Handled

private void OnWorkflowIdle(WorkflowApplicationEventArgs args)
{
}

private void OnWorkflowUnloaded(WorkflowApplicationEventArgs args)
{
}

private void OnWorkflowCompleted(WorkflowApplicationCompletedEventArgs wc)
{
    var createdRental =
        wc.Outputs["OutMovieRental"] as CustomerRental;
    MessageBox.Show(
        string.Format("New rental for {0} {1} has been returned!",createdRental.PaymentCard.FirstName,createdRental.PaymentCard.LastName));
}

private UnhandledExceptionAction OnUnhandledException(WorkflowApplicationUnhandledExceptionEventArgs uh)
        {
            return UnhandledExceptionAction.Terminate;
        }

Now the application starts getting into the button events that will drive the workflow. Listing 6-20 indicates that when the cmdStartRentalProcess button is pressed, the WorkflowApplication is initialized by calling InitiateWorkflowRuntime() and running the code in Listing 6-18.

Listing 6-20.  Starting the Workflow

private void cmdStartRentalProcess_Click(object sender, RoutedEventArgs e)
        {
            InitiateWorkflowRuntime();
           _wfApp.Run();
        }

After a new workflow instance is started, the customer can start adding movie titles that they want to rent. As each new movie title is added, the movie’s price and rating is hard-coded, as illustrated with the code in Listing 6-21. Once a Movie object is created, the workflow host indicates to the workflow that it is ready to pass it some information. The SelectMovie bookmark is called using the WorkflowApplication method of ResumeBookmark and passing to the workflow the Movie object that was created. Once the customer is done adding movie titles to rent, the FinishSearching bookmark is called. The bookmark accepts a Boolean, which is not really significant; however, if additional logic needed to be built off of the bookmark or another bookmark with the same name needed to be added to define another transition, it could be applied based on the Boolean value that is passed. Here the code simply passes in true, indicating that the customer has finished entering movies.

Listing 6-21.  Adding Movies To Be Rented Using the SelectMovie Bookmark

private void cmdSelectMovie_Click(object sender, RoutedEventArgs e)
        {
            var movie = new Movie
            {
                MovieName = txtMovieName.Text,
                Rating = "PG",
                Price = Convert.ToDecimal(4.50)
            };
            _wfApp.ResumeBookmark("SelectMovie", movie);
        }
private void cmdSelectionComplete_Click(object sender, RoutedEventArgs e)
        {
            _wfApp.ResumeBookmark("FinishedSearching", true);
        }

After one or more movies have been selected to be rented, the workflow host needs to initiate when a payment card is entered in order to process a movie rental. Figure 6-21 shows the cmdInsertCard button click event that builds a new CreditCard object and passes it to the workflow by calling the ScanPaymentCard bookmark. Listing 6-22 indicates that the values have been hard-coded.

Listing 6-22.  Passing a CreditCard Object to be Processed for the Rental Order

private void cmdInsertCard_Click(object sender, RoutedEventArgs e)
        {
            var creditCard = new CreditCard()
            {
                CCNumber = "1235626427465",
                FirstName = "Bayer",
                LastName = "White",
                ExpireMonth = 10,
                ExpireYear = 14
            };

            _wfApp.ResumeBookmark("ScanPaymentCard", creditCard);
        }

The last button required to unload the workflow instance from memory is the cmdUnload button. The button is labeled as Finished; however, its click event, which is defined in Listing 6-23, illustrates that the WorkflowApplication object’s Unload method is called. Since the workflow instance is persisted, it no longer needs to run in memory while waiting for the customer to return rented movies.

Listing 6-23.  Unloading the Workflow Instance from Memory

private void cmdUnload_Click(object sender, RoutedEventArgs e)
{
       _wfApp.Unload();
}

After running the application illustrated in Figure 6-26 to create a new movie rental, the Instances database view should contain a new record. Figure 6-28 illustrates some of the fields that indicate that the workflow instance has become idle and is waiting on the ReturnMovie bookmark to be resumed. The last couple of fields on the record also indicate the values for the fields IdentityName, IdentityPackage, Build, Major, Minor, and Revision that were given to the workflow when it was created using the code in Listing 6-18.

9781430243830_Fig06-28.jpg

Figure 6-28.  Record within the Instances database view indicating persistence

image Caution  It is important to make sure that sensitive or Personal Identification Information (PII) are not added as property values of a WorkflowIdentity object. These properties are persisted to the persistence store in plain text, so be sure to only add property values that are strictly relevant to the workflow version.

Preparing a Workflow for Update

At this point the workflow is being used to process movie rentals, but as the business’s goals change, the workflow needs to be updated in order to reflect updates to the business process. In some cases, it might not be practical to update workflows within the same solution that contains the implementation of the workflow application. In this section, a new WPF application is built to demonstrate how an application can be decoupled into a separate solution for updating a workflow, rather than updating a workflow from the same application that hosts the workflow.

The scenario changes just a bit for the process of renting movies. Now, when a customer returns one or more movies, the workflow needs to provide the customer with a list of new movies that have just become available to rent. Hopefully this will entice the customer to rent one of the new movies. This not only includes new movie rentals that occur after the change is rolled into production, but the change should also apply to existing customers who have rented movies but have not yet returned them. Figure 6-29 shows the custom tool that will be used for updating the movie rental workflow.

9781430243830_Fig06-29.jpg

Figure 6-29.  Custom tool used to update the movie rental workflow

This application will follow the three steps introduced earlier for dynamically updating a workflow. These steps are

  • Prepare the workflow for update.
  • Create an identity map for workflow changes.
  • Update existing workflow instances to use this version of the workflow.

The last step of updating an existing workflow instance will use a workflow instance that was created and persisted similar to the one in Figure 6-28. To get started, the application in Figure 6-29 will search for the workflow represented as a XAML file to be updated. After a workflow is located that needs to be updated, it will be prepared for update. In this case, a new workflow XAML file will be created, so after a workflow file path is identified by clicking the “Set Workflow to Update” button, the “Save Original Snapshot” button will be clicked next to create the snapshot. The new workflow XAML file path will then be added to the next textbox (see Figure 6-30).

9781430243830_Fig06-30.jpg

Figure 6-30.  Preparing a workflow for update

The first file path indicates the workflow used to compile the Apress.Chapter7.Workflow project. The second file path, which points to the file location for the wfReadytoUpdate.xaml, represents the snapshot file that was created when the “Save Original Snapshot” button is clicked. Listing 6-24 shows the code used to create a workflow snapshot. The magic that creates the workflow snapshot happens in cmdWorkflowUpdate_Click where a workflow is loaded, set as a snapshot, and then saved back to file.

Listing 6-24.  Creating and Saving a Workflow Snapshot from an Original Workflow

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using System.Activities;
using Microsoft.Win32;
using System.Activities.DynamicUpdate;
using System.IO;
using System.Xaml;
using System.Activities.XamlIntegration;
using System.Runtime.Serialization;
using System.Xml;
using System.Activities.DurableInstancing;
using System.Threading;
using System.Activities.Statements;

namespace Apress.Chapter7.DynamicUpdate
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        ActivityBuilder OriginalWF = null;
        ActivityBuilder DeltaWF = null;
        DynamicUpdateMap updateMap = null;
        

        public MainWindow()
        {
            InitializeComponent();
        }

        private void cmdSearchWorkflow_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog fileDialog = new OpenFileDialog();
            fileDialog.Filter = @"Workflow files (*.xaml, *.xamlx)|*.xaml;*.xamlx";
            fileDialog.Multiselect = false;
            bool? close = fileDialog.ShowDialog(this);
            if (close.HasValue && close.Value)
            {
                txtWorkflowFile.Text = fileDialog.FileName;
            }
        }

        private void cmdWorkflowUpdate_Click(object sender, RoutedEventArgs e)
        {
            OriginalWF = LoadActivityBuilder(txtWorkflowFile.Text); //Load original workflow to update
            DynamicUpdateServices.PrepareForUpdate(OriginalWF); //Prepare the workflow for update
            SaveActivityBuilder(OriginalWF, txtWorkflowFile.Text); //Save the prepared workflow
        }

        public void SaveActivityBuilder(ActivityBuilder builder, string path)
        {
            var actualpath = System.IO.Path.GetDirectoryName(path) + "\wfReadytoUpdate.xaml";
            txtWorkflowSnapshot.Text = actualpath;
            using (var writer = File.CreateText(actualpath))
            {
                var xmlWriter = new XamlXmlWriter(writer, new XamlSchemaContext());
                using (var xamlWriter = ActivityXamlServices.CreateBuilderWriter(xmlWriter))
                {
                    XamlServices.Save(xamlWriter, builder);
                }
            }
        }

Next, view the other solution that contains the project used to build the original workflow, MovieRentalProcess.xaml . This workflow can now be excluded from the project and the new wfReadytoUpdate.xaml file that was created can be included in the project. In order to view the new wfReadytoUpdate.xaml file, click on the “Show All Files” button at the top of the Solution Explorer (see Figure 6-31).

9781430243830_Fig06-31.jpg

Figure 6-31.  Showing all files to include and exclude workflow files

Now that the snapshot workflow is included within the project, it can be updated so that customers are aware of the latest movies that have been released. To simulate getting the new movies, a new custom activity is created so it can be added to the workflow. The code in Listing 6-25 illustrates the code used to define the new activity of MoviesJustReleased. This accepts a mandatory InArgument and will return the latest movies released, simulated by three hard-coded movies that are available. The activity first checks the rating type for the movie passed in and returns the latest movies that have the same rating. The return object is a collection of movies of type List<Movie>. Once the MoviesJustReleased activity is compiled successfully, it can then be added to the workflow snapshot that was created earlier.

Listing 6-25.  Unloading the Workflow Instance from Memory

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Activities;
using MovieRental.DataModel;

namespace Apress.Chapter7.Activities
{
    public class MoviesJustReleased : CodeActivity<List<Movie>>
    {
        [RequiredArgument]
        public InArgument<Movie> inBasedOnMovie { get; set; }
        protected override List<Movie> Execute(CodeActivityContext context)
        {
            List<Movie> retMovies = null;
            var basedOnMovie = inBasedOnMovie.Get(context);
            if (basedOnMovie.Rating == "PG")
            {//could do some logic here to return only movies based on a movie's rating
                retMovies = new List<Movie>
                {
                    new Movie
                    {
                        Rating="PG",
                        MovieName="The Walking Dead (Seasons 1 and 2)",
                        Price=Convert.ToDecimal(4.50)
                    },
                    new Movie
                    {
                        Rating="PG",
                        MovieName="Promethius",
                        Price=Convert.ToDecimal(5.50)
                    },
                    new Movie
                    {
                        Rating="PG",
                        MovieName="Hotel Transylvania",
                        Price=Convert.ToDecimal(4.50)
                    }
                };
            }
            return retMovies;
        }
    }
}

image Caution  While updating a workflow snapshot, it is important not to remove a bookmark that a persisted workflow is expecting to be available. Instead, focus on the execution of a workflow after an existing bookmark is resumed.

Updating the Workflow Snapshot

After the workflow snapshot is created, the workflow may look a little rough through the WF designer. Figure 6-32 illustrates the workflow snapshot when compared to the workflow in Figure 6-19. While interrogating the XML used to build the workflow snapshot generated within the wfReadyToUpdate.xaml file, I don’t believe the original locations for the states and transitions is being accounted for as they were laid out in Figure 6-19.

9781430243830_Fig06-32.jpg

Figure 6-32.  Updating the workflow snapshot

By clicking on the CompletedMovieRental FinalState of the workflow, the MoviesJustReleased activity can be added. The inBasedOnMovie argument needs to be set to the movie that the customer is returning using the holdNewRental.Movies(0). This value represents the first movie that was rented and set earlier when the movie rental was created. The Result argument will be set to holdNewRental.Movies (see Figure 6-33).

image Caution  The original workflow used in Figure 6-19 is a C# workflow, meaning it uses C# expressions. You may have noticed that Figure 6-33 uses VB expressions instead when making updates to the generated snapshot. Another interesting observation is that the existing C# expressions used show up as “Value was set in XAML.” Hopefully these characteristics will be taken care of in the next update of WF4.5.

9781430243830_Fig06-33.jpg

Figure 6-33.  Updating the workflow snapshot

Now that the workflow has been updated, it is important to recompile the solution and update the references that the workflow-updating application will use. In this case, the references that are indicated as being stale in Figure 6-34 will be updated.

9781430243830_Fig06-34.jpg

Figure 6-34.  Updating references for the workflow application to be used in the workflow-updating application

Now that the changes to the workflow have been updated, a DynamicUpdateMap can be created so it can later be used to update a persisted workflow instance. Figure 6-35 illustrates that that selecting the “Save Update Map” button creates the wfReadytoUpdate.map file.

9781430243830_Fig06-35.jpg

Figure 6-35.  Creating the update map and using it to update a workflow instance

The code used to generate the DynamicUpdateMap is shown in Listing 6-26. Specifically, the code in cmdSaveUpdateMap_Click is used to load the updated workflow snapshot from disk, create a DynamicUpdateMap file by calling DynamicUpdateServices.CreateUpdateMap, and then save the map to disk.

Listing 6-26.  Creating the Update Map

private void cmdSaveUpdateMap_Click(object sender, RoutedEventArgs e)
        {

            DeltaWF = LoadActivityBuilder(txtWorkflowSnapshot.Text);
            updateMap = DynamicUpdateServices.CreateUpdateMap(DeltaWF);

            SaveUpdateMap(updateMap, txtWorkflowSnapshot.Text);
        }
                

        public ActivityBuilder LoadActivityBuilder(string fileName)
        {
            ActivityBuilder builder;
            using (var xamlReader = new XamlXmlReader(fileName))
            {
                var builderReader = ActivityXamlServices.CreateBuilderReader(xamlReader);
                builder = (ActivityBuilder)XamlServices.Load(builderReader);
                xamlReader.Close();
            }
            return builder;
        }

        public void SaveUpdateMap(DynamicUpdateMap map, string fileName)
        {
            var path = System.IO.Path.ChangeExtension(fileName, "map");
            DataContractSerializer serialize = new DataContractSerializer(typeof(DynamicUpdateMap));
            using (FileStream fs = File.Open(path, FileMode.Create))
            {
                serialize.WriteObject(fs, map);
            }

            txtUpdateMapFile.Text = path;
        }

After the map file is created, it can be used to update any workflow instances that have been persisted using the original workflow for renting movies. The code in Listing 6-26 is used to update a workflow instance associated by a Guid that is added in the textbox, as illustrated in Figure 6-35. Listing 6-27 indicates that the persistence store must be configured first using a connection string.

Next, a WorkflowApplicationInstance is created using the SqlWorkflowInstanceStore object that was configured and the Guid identifying which workflow instance needs to be updated. The DynamicUpdateMap is then read from file and created into memory. A WorkflowApplication object is then instantiated using a new MovieRentalProcess, representing the workflow definition that will be used. It is important to set this since the MovieRentalProcess workflow is reflected based on the referenced DLLs from the workflow project and when the workflow project was compiled, it was using an updated workflow snapshot during compilation.

The WorkflowApplication also takes a WorkflowIdentity object. You may have noticed that the workflow application host was creating workflow instances using v1MovieRentalProcess as the name of the workflow and version 1.0.0.0. This information was saved to the persistence store each time a workflow was persisted. Listing 6-27 shows that the workflow instance should now be updated to the workflow named v2MovieRentalProcess, and its version is now 2.0.0.0. This will now be reflected in the persistence store after the workflow is unloaded from memory. Once the workflow is loaded using the WF runtime and updated within the persisted store, it is simply unloaded again so it can later be loaded when a customer returns their movies.

Listing 6-27.  Updating a Persisted Workflow Instance to the New Version

private void cmdUpdateInstance_Click(object sender, RoutedEventArgs e)
        {
            
           SqlWorkflowInstanceStore instanceStore = new SqlWorkflowInstanceStore();
            
            instanceStore.ConnectionString =
                @"Data Source=(LocalDB)v11.0;Initial Catalog=WFPersist;Integrated Security=True";

            WorkflowApplicationInstance wfInstance =
                WorkflowApplication.GetInstance(new Guid(txtUpdateInstance.Text), instanceStore);

            DataContractSerializer s = new DataContractSerializer(typeof(DynamicUpdateMap));
            using (FileStream fs = File.Open(txtUpdateMapFile.Text, FileMode.Open))
            {
                updateMap = s.ReadObject(fs) as DynamicUpdateMap;
            }
            
            var wfApp =
                new WorkflowApplication(new MovieRentalProcess(),
                     new WorkflowIdentity
                     {
                         Name = "v2MovieRentalProcess",
                         Version = new Version(2, 0, 0, 0)
                     });

            IList<ActivityBlockingUpdate> act;
            if(wfInstance.CanApplyUpdate(updateMap, out act))
            {
                wfApp.Load(wfInstance, updateMap);
                wfApp.Unload();
            }
        }

Knowing What Can be Updated

Even though a workflow has been updated, it does not mean that persisted workflow instances that were run using a previous workflow definition can be updated. When a workflow is persisted, it might have gone idle while waiting on some external event, like a bookmark. When this happens, as far as the workflow knows, a bookmark existed the last time it was persisted; therefore the same bookmark should not be removed or changed. The WorkflowApplicationInstance also has a CanApplyUpdate(DynamicUpdateMap updateMap, out IList<ActivityBlockingUpdate> activitiesBlockingUpdate() function that returns a Boolean, indicating if a persisted workflow instance can be updated (see Listing 6-27). The first parameter, updateMap, is of course the update map used to update a persisted workflow instance, and IList<ActivityBlockingUpdate> activitiesBlockingUpdate passes back a collection of ActivityBlockUpdate objects that indicate why a persisted workflow instance cannot be updated. The Reason property on the ActivityBlockUpdate returns a message explaining why an update for an instance cannot occur.

image Tip   Do not make changes to the arguments while updating a workflow definition. This will cause the CanApplyUpdate function to return false. This includes adding or removing arguments.

Now that the workflow instance is updated, when a customer returns they should get the latest release of movies based on the rating for the first movie they return. The focus is now back on the workflow application that is hosting the movie rental workflow. All that needs to happen to simulate a customer returning a movie is to enter the Guid, represented as the persisted workflow ID, and press the “Insert Card to Return Movie” button (see Figure 6-36).

9781430243830_Fig06-36.jpg

Figure 6-36.  Returning movies that were rented

Running the Correct Workflow Version

Earlier I mentioned that the workflow host was creating workflow instances under a different workflow name and version, as illustrated in Listing 6-18. If you simulated a customer returning movie and received the error in Figure 6-37, that means that the WF runtime is smart enough to check and make sure that the right version of the workflow is being run.

9781430243830_Fig06-37.jpg

Figure 6-37.  Resuming an incorrect version of a workflow

What’s happening here is that the workflow instance version has been updated as illustrated by looking at the values pertaining to versioning within the persistence store. To resume workflow instances that have been updated, the WF runtime in WF4.5 now has a safety mechanism that checks to see if you want to run an existing workflow instance against an updated workflow version or run it using the same workflow that was used to initiate the workflow. In the scenario for the customer returning a movie, Figure 6-17 needs to be updated so that version 2.0.0.0 is used for completing the workflow.

Once the update is made, Figure 6-38 illustrates that the workflow is now calling out the MoviesJustReleased activity as the workflow is debugged during execution. The workflow instance still has the record within the persistence store because we indicated to the WF runtime to keep completed workflow instance records in the persistence store.

9781430243830_Fig06-38.jpg

Figure 6-38.  Executing new functionality by calling the MoviesJustReleased activity

Updating Activities

Activities can also be used for updating workflows, but only activities that inherit from NativeActivity can be dynamically updated. Activities that inherit from NativeActivity have direct interaction to the WF runtime, therefore the WF runtime needs to be aware of any updates made to an activity, and any new child activities must be manually scheduled. After a custom activity inherits from NativeActivity, there are two methods that need to be overridden from NativeActivity. The first method is OnCreateDynamicUpdateMap, which is an event that gets raised when a map gets created for a dynamic update. The other is UpdateInstance, which updates the instance of the activity with any new activities, assuring that they are added as child activities and scheduled for execution (see Listing 6-28).

Listing 6-28.  Overriding Methods for an Activity That Inherits from NativeActivity

protected override void OnCreateDynamicUpdateMap(NativeActivityUpdateMapMetadata metadata, Activity originalActivity)
            {
                metadata.AllowUpdateInsideThisActivity();
            }
protected override void UpdateInstance(NativeActivityUpdateContext updateContext)
            {
                    if (updateContext.IsNewlyAdded(childActivity))
                    {
                        updateContext.ScheduleActivity(childActivity);
                    }

}  

Summary

This chapter focused on the most desired feature that developers requested in WF4.5, which is built-in functionality for managing workflow versions. The WF Team has done an outstanding job of providing a robust versioning model for workflows. In addition to versioning, the chapter also covered how different versions of workflows can be run side by side and at the same time. The scenario that calls out to side-by-side workflow execution is when a current workflow instance has already been executed and a mandatory update to the workflow definition needs to be made without affecting an existing executed workflow instance.

The second type of versioning demonstrated was dynamic updating workflows; this works by ensuring a running workflow instance executed using an earlier version of a workflow is updated to finish its execution using a later version of a workflow. By updating the workflow instance, it inherits whatever changed functionality that was made to the next version of the workflow.

Chapter 7 will switch gears a bit and focus on explaining how different types of custom activities can be created.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.105.159