Showing posts with label WF WCF. Show all posts
Showing posts with label WF WCF. Show all posts

Monday, September 17, 2012

SharePoint 2010 Email Notification With Workflow

Here's a common scenario in SharePoint environment: you have a SharePoint custom list to store and trace some tasks or jobs, you want the system automatically send out email notification based on some conditions. Let's say there's a SharePoint List with following two columns:

1. Tickler Date: a DateTime field that triggers the workflow to send out the email notification
2. Tickler Contacts: a People field (multiple section) where the notification email will be sent to

The requirement is simple:

1. Email notification sent to "Tickler Contacts" whenever the "Tickler Date" is reached, if both fields are defined.
2. Notification should be aware of the change on those two fields, and updated "Tickler Date" and "Tickler Contacts" should be applied.

There are a few options to accomplish such task. We can set a Windows Scheduled Task to run a console app which scans the whole list daily and send the notification, or use SharePoint Timer Jobs to do similar things, or implement SharePoint Workflow. SharePoint Workflow is very powerful and capable of handling many SharePoint tasks easily. With SharePoint Designer, you can even visually work on workflow's conditions, steps and actions, and apply the Workflow directly to the server without any code deployment (arguably good and bad on this). Workflow approach will be discussed in this post.

SharePoint Designer Workflow has a built-in "Send an email" action. We use that as the only one action step in our first Workflow for notification. Because the recipients come from a multi-user People field, we need to set Person field returned as delimited email addresses:


By default the BCC is not available on the email form. However, when you can highlight the send email action and click the "Advanced Properties" button on the top menu you will see the BCC field. You could even fine tune the formatting of the mail content since the original body HTML can be seen and updated there:


The next step is to figure out how to start the Workflow. A list-associated Workflow can be set to start manually, or automatically start when a list item is created or updated. For the simple Workflow we defined above is not going to start automatically because we haven't defined the condition yet.

If the trigger condition is based on the date time field, then we can also utilize retention policy to start a Workflow. Go to List's settings page and click "Information management policy settings" you will see following settings (the top "List Based Retention Schedule" won't be available if "Library and Folder Based Retention" Site Collection Feature is not activated):


We are interested in the item properties, so click the Item and go to Item policy editing page, check "Enable Retention", and then add a retention stage:


The condition here is that when "Tickler Date" is passed by 0 day, or "Tickler Date" is today, then workflow will start and send the email notification. All look good except that requirement #2 can not be satisfied. The "Tickler Date" is fixed, and Retention policy won't recalculate the condition when the "Tickler Date" is changed. There's no workaround on that as far as I know. So Retention policy is not an option in our case.

The solution is let Workflow deals with the conditions by itself, and make it start automatically when a List item is created or updated:


The Workflow quite explains itself. First it checks if "Tickler Contacts" and "Tickler Date" are both valid. It would simply "quit" (WF completes) if any of those two empty or "Tickler Date" has passed. Otherwise it continues to further logic. A Workflow variable is set to the "Tickler Date" so later it has a reference to compare if that has been changed. Then two parallel branches are split to run simultaneously. The first branch is waiting until the "Tickler Date" reaches so email notification will be sent out, and the workflow completes its work; the second branch is pending and looking for any change on the "Tickler Date" field, when that occurs the workflow stops and a new workflow instance will be created by SharePoint using the updated "Tickler Date", because the workflow had set to auto-start when list item is updated.

You may ponder what happens if the fields other than "Tickler Date" have been changed, would a new Workflow instance will be created? The answer is no. SharePoint only allows one instance of Workflow for each version to run. That's why we cancel the old Workflow instance when the "Tickler Date" is changed, so a new Workflow instance can be created and start.

You may also wonder why not just simply using the general "Modified" field instead of "Tickler Date": cancel the old Workflow instance whenever the List item is changed, then there’s room for a new WF instance with updated data. You will see that won't work if you try it out. The reason is that SharePoint does modify the List item silently for some background process such as updating List item's Workflow status. That causes time change on List item's "Modified" field which stops old Workflow, but SharePoint won't start a Workflow for such internal processing.

A final note: don't use system account to test your Workflow. It won't work! Creating or updating a list item using system account won't trigger associated Workflow even although the Workflow is configured to auto-start. Under the hood List Workflow was implemented using List Item Event Receivers, and system account is just ignored there.
Reference: Create a Detailed Custom Task Notification with a SharePoint Designer Workflow: http://sharepoint-videos.com/sp10creating-a-workflow-on-a-list-using-sharepoint-designer-2010

Thursday, October 27, 2011

Invoke SharePoint Workflow Programmatically

There's some restricted SharePoint sites and content in your SharePoint environment, and users can only interact with secure SharePoint resources through custom webpart(s). A typical example is performance review system. For simplicity let's say each department has a site or subsite to store the review data. You create a SPList for each year's review like PerformanceReview-2011, PerformanceReview-2012, etc. Only the management team members of a department can have direct access to the department performance review site. Every employee can input the data and respond manager's question/review through a custom webpart. Usually RunWithElevatedPrivileges is used inside the WebPart to get through the permission issue for SPList content update as code below:
    SPSecurity.RunWithElevatedPrivileges(delegate()
    {
        using (SPSite site = new SPSite(siteID))
        {
            using (SPWeb web = site.OpenWeb(webID))
            {
                SPList list = web.Lists.TryGetList(listName);
                //Update List Item ...
            }
        }
    });
A workflow associated with the List is supposed to automatically kick off doing some extra stuff such as assigning task to corresponding managers. The problem is that the execution context of above code is system account, and updating List Item with System account won't trigger the workflow. So you have to start the workflow manually inside the WebPart:
    // Start a workflow manually since system account won't trigger workflow automatically
    private static void StartWorkflow(SPListItem listItem, string workflowName)
    {
        try
        {
            var listWorkflowAssociations = listItem.ParentList.WorkflowAssociations;
            foreach (SPWorkflowAssociation wfAssoication in listWorkflowAssociations)
            {
                if (wfAssoication.Name == workflowName)
                {
                    listItem.Web.Site.WorkflowManager.
                      StartWorkflow(listItem, wfAssoication, wfAssoication.AssociationData, true);
                    break;
                }
            }
        }
        catch (Exception ex)
        {
            // Exception handling
        }
    }
Bear in mind that the Associator, Initiator and Current User inside Workflow context all are system account with such implementation. You have to use People field defined in List Item to reference the original user.

Friday, January 28, 2011

Implementing Simple Custom Rules By Code

In my previous two posts Microsoft Workflow Rules Engine was used to execute the business rules. That maybe a bit overkill if there're only a small set of rules to run and the use cases of them are relatively simple. In that case we could implement the rules and control their execution directly. The simple console app below demos the concept of custom rules implementation:

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

namespace ConsoleApplication1
{
    class Program
    {
        #region demo domain objects
        public class Product
        {
            public string Name { get; set; }
            public decimal Price { get; set; }
        }

        public class ShoppingCart
        {
            public List<Product> SelectedProducts { get; set; }
            public List<string> CouponCodes { get; set; }
            public decimal TotalAmount { get { return SelectedProducts.Sum(prod => prod.Price); } }
            internal decimal Discount { get; set; }

            public ShoppingCart()
            {
                SelectedProducts = new List<Product>();
                CouponCodes = new List<string>();
                Discount = 0;
            }
        }
        #endregion demo domain objects

        #region promotion objects 
        class CouponPromotion  
        {
            public string Name { get; set; }
            public string PromotionCode { get; set; }
            public decimal Discount { get; set; }
            public DateTime ExpiredOn { get; set; }
        }

        class TotalAmountPromotion // discount if total amount is greater than a number
        {
            public string Name { get; set; }
            public decimal TotalAmount { get; set; }
            public decimal Discount { get; set; }
            public DateTime ExpiredOn { get; set; }
        }
        #endregion promotion object

        #region custom rules interface and implementation
        interface ICustomRule
        {
            void Execute(ShoppingCart cart);
        }

        class CouponPromotionRule : ICustomRule
        {
            private CouponPromotion mCouponPromotion;
            public CouponPromotionRule(CouponPromotion promotion)
            {
                this.mCouponPromotion = promotion;
            }

            public void Execute(ShoppingCart cart)
            {
                if (mCouponPromotion.ExpiredOn >= DateTime.Today && cart.CouponCodes.Contains(mCouponPromotion.PromotionCode))
                {
                    Console.WriteLine(mCouponPromotion.Name + " applied");
                    cart.Discount += mCouponPromotion.Discount;
                }
            }
        }

        class TotalAmountGreaterRule : ICustomRule
        {
            private TotalAmountPromotion mTotalAmountPromotion;
            public TotalAmountGreaterRule(TotalAmountPromotion promotion)
            {
                this.mTotalAmountPromotion = promotion;
            }

            public void Execute(ShoppingCart cart)
            {
                if (mTotalAmountPromotion.ExpiredOn >= DateTime.Today && cart.TotalAmount >= mTotalAmountPromotion.TotalAmount)
                {
                    Console.WriteLine(mTotalAmountPromotion.Name + " applied");
                    cart.Discount += mTotalAmountPromotion.Discount;
                }
            }
        }
        #endregion custom rules interface and implementation

        static void RuleTest(ShoppingCart cart)
        {
            // Mock CouponPromotionRule and TotalAmountRules
            CouponPromotionRule rule1 = new CouponPromotionRule(new CouponPromotion()
            {
                Name = "Coupon Promotion Rule 1",
                PromotionCode = "123456789",
                Discount = 10,
                ExpiredOn = new DateTime(2100, 1, 1)
            });

            CouponPromotionRule rule2 = new CouponPromotionRule(new CouponPromotion()
            {
                Name = "Coupon Promotion Rule 2",
                PromotionCode = "111111111",
                Discount = 20,
                ExpiredOn = new DateTime(2000, 1, 1)
            });
            TotalAmountGreaterRule rule3 = new TotalAmountGreaterRule(new TotalAmountPromotion()
            {
                Name = "Total Amount Rule 1",
                TotalAmount = 1000,
                Discount = 100,
                ExpiredOn = new DateTime(2100, 1, 1)
            });

            ICustomRule[] rules = new ICustomRule[] { rule1, rule2, rule3 };
            foreach (var rule in rules)
            {
                rule.Execute(cart);
            }
        }

        static void Main(string[] args)
        {
            ShoppingCart shoppingCart = new ShoppingCart();
            Enumerable.Range(1, 5).ToList().ForEach(i => shoppingCart.SelectedProducts.Add(
                   new Product() { Name = "Product" + i.ToString(), Price = 100 * i }));
            shoppingCart.CouponCodes.Add("123456789");
            shoppingCart.CouponCodes.Add("111111111");
            Console.Write("\nShopping Cart before rules execution -- ");
            Console.WriteLine("Total : ${0}\tDiscount : ${1}", shoppingCart.TotalAmount, shoppingCart.Discount);

            RuleTest(shoppingCart);

            Console.Write("Shopping Cart after rules execution -- ");
            Console.WriteLine("Total : ${0}\tDiscount : ${1}", shoppingCart.TotalAmount, shoppingCart.Discount);

            Console.Read();
        }      
    }
}

The result of the test app:

Thursday, January 27, 2011

Create Windows Workflow 4.0 Rules By Custom String

In my previous post, I presented a way to create Windows Workflow rules by CodeDom expression. Sometimes it's hard to build a meaningful UI for authoring business rules. In such case we may need to use custom string to construct WF rules. The key part is how to parse the rule condition/action string. We could do similar things in WF RuleSetDialog. Can we re-use the parser used by RuleSetDialog? The answer is yes. Beau Crawford gave a solution for WF 3.5. Basically WF internally has a rule condition and action parser under System.Workflow.Activities.Rules.Parser inside System.Workflow.Activities assembly, but the parser is not exposed to public. In order to re-use the internal parser we need to use reflection.

Following code example demos how to archive this by slight modification of the code used in previous post:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.Reflection;
using System.Workflow.Activities.Rules;
using System.Workflow.ComponentModel.Compiler;

class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}

class ShoppingCart
{
public List<Product> SelectedProducts { get; set; }
public List<string> CouponCodes { get; set; }
public decimal TotalAmount { get { return SelectedProducts.Sum(prod => prod.Price); } }
internal decimal Discount { get; set; }

public ShoppingCart()
{
SelectedProducts = new List<Product>();
CouponCodes = new List<string>();
Discount = 0;
}
}

class CouponPromotion
{
public string Name { get; set; }
public string PromotionCode { get; set; }
public decimal Discount { get; set; }
public DateTime ExpiredOn { get; set; }
}

/// <summary>
/// WFRuleParser is helper class to parse Windows Workflow condition and action.
/// Invoke ParseCondition and ParseAction two internal methods
/// defined in System.Workflow.Activities.Rules.Parser by reflection
/// </summary>
class WFRuleParser
{
public static RuleExpressionCondition ParseCondition(Type targetObjectType, string expression)
{
RuleValidation ruleValidation = new RuleValidation(targetObjectType, null);
return ExecuteMethod("ParseCondition",
new object[] { ruleValidation }, new object[] { expression }) as RuleExpressionCondition;
}

public static List<RuleAction> ParseAction(Type targetObjectType, string expression)
{
RuleValidation ruleValidation = new RuleValidation(targetObjectType, null);
return ExecuteMethod("ParseStatementList",
new object[] { ruleValidation }, new object[] { expression }) as List<RuleAction>;
}

// Invoke private/internal method using reflection
private static object ExecuteMethod(string methodName, object[] ctorParameters, object[] methodParameters)
{
string ParserTypeName ="System.Workflow.Activities.Rules.Parser, System.Workflow.Activities,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
;
try
{
Type type = Type.GetType(ParserTypeName);
ConstructorInfo constructor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance,
null, new Type[] { typeof(RuleValidation) }, null);
object instance = constructor.Invoke(ctorParameters);
MethodInfo method = instance.GetType().GetMethod(methodName,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
return method.Invoke(instance, methodParameters);
}
catch (Exception ex)
{
throw ex;
}
}
}

class WFRuleBuilder
{
// Create a Rule for a CouponPromotion
public static Rule GetCouponPromotionRule(CouponPromotion couponPromo)
{
Type targetObjectType = typeof(ShoppingCart); // Shopping Cart is the input object for Rules Engine

// condition => if coupone code is matched and promotion is not expired
var conditionString = string.Format(
"this.CouponCodes.Contains(\"{0}\") && DateTime.Today < DateTime.Parse(\"{1}\")",
couponPromo.PromotionCode, couponPromo.ExpiredOn.ToString("yyyy-MM-dd"));
var condition = WFRuleParser.ParseCondition(targetObjectType, conditionString);

// action => add discount
var actionString = string.Format("this.Discount = this.Discount + {0}", couponPromo.Discount);
var action = WFRuleParser.ParseAction(targetObjectType, actionString);

return new Rule(couponPromo.Name, condition, action, null);
}
}

class Program
{
static void Main(string[] args)
{
// Mock 5 products in shopping cart:
// [{"Product1":{Price:$100, Category:"Category1"}}, {"Product2":{Price: $200, Category:"Category1"}}, ...]
ShoppingCart shoppingCart = new ShoppingCart();
Enumerable.Range(1, 5).ToList().ForEach(i => shoppingCart.SelectedProducts.Add(
new Product() { Name = "Product" + i.ToString(), Price = 100 * i }));
shoppingCart.CouponCodes.Add("123456789");
shoppingCart.CouponCodes.Add("111111111");
Console.Write("\nShopping Cart before rules execution -- ");
Console.WriteLine("Total : ${0}\tDiscount : ${1}", shoppingCart.TotalAmount, shoppingCart.Discount);

// Mock 2 CouponPromotions
CouponPromotion promo1 = new CouponPromotion()
{
Name = "Promotion1", PromotionCode = "123456789", Discount = 100, ExpiredOn = new DateTime(2000, 1, 1)
};
CouponPromotion promo2 = new CouponPromotion()
{
Name = "Promotion2", PromotionCode = "111111111", Discount = 100, ExpiredOn = new DateTime(2100, 1, 1)
};


// Build a RuleSet with above two rules
RuleSet ruleSet = new RuleSet();
ruleSet.Rules.Add(WFRuleBuilder.GetCouponPromotionRule(promo1));
ruleSet.Rules.Add(WFRuleBuilder.GetCouponPromotionRule(promo2));

// Print Rule information
Console.WriteLine("\nRules defined in the RuleSet:");
ruleSet.Rules.ToList().ForEach(rule =>
{
Console.WriteLine("\"{0}\" RuleCondition: \n{1}", rule.Name, rule.Condition);
Console.WriteLine("\"{0}\" RuleAction: \n{1}", rule.Name, rule.ThenActions[0]);
});

// Execute Rules
RuleValidation validation = new RuleValidation(typeof(ShoppingCart), null);
if (ruleSet.Validate(validation))
{
RuleEngine engine = new RuleEngine(ruleSet, validation);
engine.Execute(shoppingCart);
}
else // Validation failed, print errors
{
StringBuilder errors = new StringBuilder();
foreach (ValidationError validationError in validation.Errors)
{
errors.Append(validationError.ErrorText);
}
Console.WriteLine("Validation errors:{0}", errors);
}

Console.Write("\nShopping Cart after rules execution -- ");
Console.WriteLine("Total : ${0}\tDiscount : ${1}", shoppingCart.TotalAmount, shoppingCart.Discount);

Console.Read();
}
}
A CouponPromotion has a coupon code and an expiration date property. At run-time, rules engine will check all coupon codes stored in a ShoppoingCart, then apply the discount if they are valid. The logic is:
if ShoppingCart.Contains(couponPromotion.CouponCode) && couponPromotion is not expired
then add CurrentCouponPromotion.Discount to ShoppingCart
Remember the target object the rule engine runs against on is a ShoppingCart instance. The rule action would be update ShoppingCart's Discount property. The exact strings to be parsed for two promotions defined in above example code are:
Promotion1 condition string:
this.CouponCodes.Contains("123456789") && System.DateTime.Today < System.DateTime.Parse("2000-01-01")
Promotion1 action string:
this.Discount = this.Discount + 100
Promotion2 condition string:
this.CouponCodes.Contains("111111111") && System.DateTime.Today < System.DateTime.Parse("2100-01-01")
Promotion2 action string:
this.Discount = this.Discount + 100
Obviously Promotion1 has expired and only Promotion1 is valid and applied. The console app result:

Tuesday, January 25, 2011

Business Rules Implementation Using Windows Workflow 4.0

Recently I have been working on a project for a back-end service providing product information for a telecom company. The pricing of a product or products is relying on a number of business rules. e.g. a cell-phone's price and its monthly charge can vary based on the data/voice plan as well as those value-added features. A discount or multiple discounts could be applied if certain criteria are matched. In addition, complicated validation rules are applied for those products.

Windows Workflow Rules Engine, a free out-of-box feature from Windows Workflow Foundation, plays a center role in our business rules implementation. We construct our own business entities and relationship. On top of those business objects we build our customized rule authoring UI. Those rule-related entities are converted to CodeDom expression at run-time, and then a set of Workflow RuleSets are created based on those CodeDom expression. For each service call, products will be passed to the Workflow Rule Engine as an input, and rules execution would update their metadata or properties if a rule or some rules are satisfied.

Following code sample demos how all this works:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.Workflow.Activities.Rules;
using System.Workflow.ComponentModel.Compiler;

#region Business Object Models
class Product
{
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}

class ShoppingCart
{
public List<Product> SelectedProducts { get; set; }
public string CouponCode { get; set; }
public decimal TotalAmount { get { return SelectedProducts.Sum(prod => prod.Price); } }
internal decimal Discount { get; set; }

public ShoppingCart()
{
SelectedProducts = new List<Product>();
Discount = 0;
}

// Check total amount for a given category
public bool TotalCategoryAmountGreaterThan(decimal price, string category)
{
decimal totalPriceForCategory = 0;
foreach (var prod in SelectedProducts)
{
if (prod.Category == category)
totalPriceForCategory += prod.Price;
}

return totalPriceForCategory >= price;
}
}
#endregion

#region Rule-related Object Models
/// <summary>
/// Apply a discount if having the promotion code
/// </summary>
class CouponPromotion
{
public string Name { get; set; }
public string PromotionCode { get; set; }
public decimal Discount { get; set; }
}

/// <summary>
/// Apply a discount when total amount for a given category greater than a value
/// </summary>
class BundlePromotion
{
public string Name { get; set; }
public string ProductCategory { get; set; }
public decimal TotalAmountThreshold { get; set; }
public decimal Discount { get; set; }
}
#endregion

#region Helper class to create rules based on Rule-related objects
class WFRuleBuilder
{
// Create a Rule for a CouponPromotion
public static Rule GetCouponPromotionRule(CouponPromotion couponPromo)
{
CodePrimitiveExpression couponCode = new CodePrimitiveExpression(couponPromo.PromotionCode);
CodePrimitiveExpression couponDiscount = new CodePrimitiveExpression(couponPromo.Discount);

CodePropertyReferenceExpression couponPropertyRef =
new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), "CouponCode");
CodePropertyReferenceExpression discountPropertyRef =
new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), "Discount");

// Create a binary equality expression to compare the property value
CodeBinaryOperatorExpression couponConditionExpression =
new CodeBinaryOperatorExpression(couponCode, CodeBinaryOperatorType.ValueEquality, couponPropertyRef);

// Create a binary addition expression to increase the discount
CodeBinaryOperatorExpression discountExpression =
new CodeBinaryOperatorExpression(discountPropertyRef, CodeBinaryOperatorType.Add, couponDiscount);
CodeAssignStatement discountActionExpression =
new CodeAssignStatement(discountPropertyRef, discountExpression);

Rule couponRule = new Rule(couponPromo.Name);
couponRule.Condition = new RuleExpressionCondition(couponConditionExpression);
couponRule.ThenActions.Add(new RuleStatementAction(discountActionExpression));

return couponRule;
}

// Create a Rule for a BundlePromotion
public static Rule BuildBundlePromotionRule(BundlePromotion bundlePromo)
{
CodePrimitiveExpression priceThreshold = new CodePrimitiveExpression(bundlePromo.TotalAmountThreshold);
CodePrimitiveExpression productCategory = new CodePrimitiveExpression(bundlePromo.ProductCategory);
CodePrimitiveExpression discount = new CodePrimitiveExpression(bundlePromo.Discount);
CodePrimitiveExpression[] parameters = new CodePrimitiveExpression[] { priceThreshold, productCategory };

CodePropertyReferenceExpression discountPropertyRef =
new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), "Discount");

// Create a method invocation expression to check the totalValue at runtime
CodeMethodInvokeExpression methodEvaluationExpression =
new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "TotalCategoryAmountGreaterThan", parameters);

// Create a binary addition expression to increase the discount
CodeBinaryOperatorExpression discountExpression =
new CodeBinaryOperatorExpression(discountPropertyRef, CodeBinaryOperatorType.Add, discount);
CodeAssignStatement discountActionExpression = new CodeAssignStatement(discountPropertyRef, discountExpression);

Rule priceThresholdRule = new Rule(bundlePromo.Name);
priceThresholdRule.Condition = new RuleExpressionCondition(methodEvaluationExpression);
priceThresholdRule.ThenActions.Add(new RuleStatementAction(discountActionExpression));

return priceThresholdRule;
}
}
#endregion

class Program
{
static void Main(string[] args)
{
// Mock 5 products in shopping cart:
// [{"Product1":{Price:$100, Category:"Category1"}}, {"Product2":{Price: $200, Category:"Category1"}}, ...]
ShoppingCart mockShoppingCart = new ShoppingCart();
Enumerable.Range(1, 5).ToList().ForEach(i => mockShoppingCart.SelectedProducts.Add(
new Product() { Name = "Product" + i.ToString(), Price = 100 * i, Category = "Category1" }));
mockShoppingCart.CouponCode = "123456789";
Console.Write("\nShopping Cart before rules execution -- ");
Console.WriteLine("Total : ${0}\tDiscount : ${1}", mockShoppingCart.TotalAmount, mockShoppingCart.Discount);

// Mock some rules
RuleSet rules = MockRules();

// Execute Rules
RuleValidation validation = new RuleValidation(typeof(ShoppingCart), null);
if (rules.Validate(validation))
{
RuleEngine engine = new RuleEngine(rules, validation);
engine.Execute(mockShoppingCart);
}
else // Validation failed, print errors
{
StringBuilder errors = new StringBuilder();
foreach (ValidationError validationError in validation.Errors)
{
errors.Append(validationError.ErrorText);
}
Console.WriteLine("Validation errors:{0}", errors);
}

Console.Write("\nShopping Cart after rules execution -- ");
Console.WriteLine("Total : ${0}\tDiscount : ${1}", mockShoppingCart.TotalAmount, mockShoppingCart.Discount);

Console.Read();
}

// Mock one CouponPromotion rule and PackageTotalPricePromotion rule for testing
private static RuleSet MockRules()
{
// Mock coupon rule: $10 discount if the user has a coupon code of "123456789"
CouponPromotion couponPromo = new CouponPromotion()
{
Name = "2011 new year coupon",
PromotionCode = "123456789",
Discount = 10
};

// Mock total price rule: $100 discount if purchased more than $1000 for ProductCategory of "Category1"
BundlePromotion bundlePromo1 = new BundlePromotion()
{
Name = "Bundle Promotion for Category1",
ProductCategory = "Category1",
TotalAmountThreshold = 1000,
Discount = 100
};

// Mock total price rule: $200 discount if purchased more than $1000 for ProductCategory of "Category2"
BundlePromotion bundlePromo2 = new BundlePromotion()
{
Name = "Bundle Promotion for Category2",
ProductCategory = "Category2",
TotalAmountThreshold = 1000,
Discount = 200
};

// Create a RuleSet to contain above two rules
RuleSet promotionRules = new RuleSet("Promotion Rules");
promotionRules.Rules.Add(WFRuleBuilder.GetCouponPromotionRule(couponPromo));
promotionRules.Rules.Add(WFRuleBuilder.BuildBundlePromotionRule(bundlePromo1));
promotionRules.Rules.Add(WFRuleBuilder.BuildBundlePromotionRule(bundlePromo2));

// Print Rule information
Console.WriteLine("\nRules defined in the RuleSet:");
promotionRules.Rules.ToList().ForEach(rule =>
{
Console.WriteLine("\"{0}\" RuleCondition: \n{1}", rule.Name, rule.Condition);
Console.WriteLine("\"{0}\" RuleAction: \n{1}", rule.Name, rule.ThenActions[0]);
});

return promotionRules;
}

}
The console app shows that a $10 coupon discount and a $100 bundle discount has been applied:



We have a simple Product class with Name, Category and Price properties. A ShoppingCart sent to a rule engine contains a list of products and an optional coupon code. Rule engine will update ShoppingCart's discount property if applicable.

Two promotion types have been abstracted in the demo code. CouponPromotion simply applies a discount if the promotion code is matched. BundlePromotion is setup the way that a discount is applied if the total amount for certain category is greater than a number. For such rules we are easily build a nice authoring UI without using the out-of-box RuleSetDialog. RuleSetDialog is powerful but not that user friendly.

GetCouponPromotionRule and BuildBundlePromotionRule are two helper methods to build the Windows Workflow Rule objects based on the defined CouponPromotion and BundlePromotion. CodeDom expression trees are constructed to setup the WF condition and actions. CodeDom expression is an other topic and I am not going to expand it in detail here. Basically you need to build an expression for each statement but join them as an expression tree.

For CouponPromotion the logical condition is comparing the ShoppingCart's CouponCode property with the PromotionCode property defined in CouponPromotion by using CodeBinaryOperatorType.ValueEquality. The CodeBinaryOperatorExpression returns true if the values are the same. The action for a satisfied CouponPromotion condition is a property assignment using CodeAssignStatement to increase the ShoppingCart's Discount value. For BundlePromotion, the condition expression is a little different. The TotalCategoryAmountGreaterThan method defined in ShoppingCart class is used to determine the Workflow Rule's condition. This is achieved by CodeMethodInvokeExpression.

In our test code we create a shoppoing cart with five mock products and three promotions:
ShoppingCart1:
Product1: Name: Product1, Price: $100, Category: "Category1"
Product2: Name: Product2, Price: $200, Category: "Category1"
Product3: Name: Product3, Price: $300, Category: "Category1"
Product4: Name: Product4, Price: $400, Category: "Category1"
Product5: Name: Product5, Price: $500, Category: "Category1"
CouponCode: "123456789"
Promotion1: CouponPromotion $10 discount for coupon code "123456789"
Promotion2: BundlePromotion $100 discount if total amount for "Category1" is more than $1000
Promotion3: BundlePromotion $200 discount if total amount for "Category2" is more than $1000

We can see that both Promotion1 and Promotion2's conditions are matched, but Promotion3's condition is not. So the result shows that total $110 discount has been applied.

How about the performance? Let's do a simple test by increasing the ShoppoingCart's product number to 200, faking 200 CouponPromotion rules and 200 BundlePromotion rules:
    static void Main(string[] args)
{
ShoppingCart mockShoppingCart = new ShoppingCart();
Enumerable.Range(1, 200).ToList().ForEach(i => mockShoppingCart.SelectedProducts.Add(
new Product() { Name = "Product" + i.ToString(), Price = 100 * (i%10),
Category = "Category" + (i%10).ToString() }));
mockShoppingCart.CouponCode = "100";
Console.Write("\nShopping Cart before rules execution -- ");
Console.WriteLine("Total : ${0}\tDiscount : ${1}", mockShoppingCart.TotalAmount, mockShoppingCart.Discount);

RuleSet promotionRules = new RuleSet("Promotion Rules");
Enumerable.Range(1, 200).ToList().ForEach(i =>
{
string sequence = i.ToString(), category = "Category" + (i % 10).ToString();
CouponPromotion couponPromo = new CouponPromotion()
{ Name = "Coupon" + sequence, PromotionCode = sequence, Discount = 10 };
BundlePromotion bundlePromo = new BundlePromotion()
{ Name = "Bundle" + sequence, ProductCategory = category, TotalAmountThreshold = 1000, Discount = 100 };
promotionRules.Rules.Add(WFRuleBuilder.GetCouponPromotionRule(couponPromo));
promotionRules.Rules.Add(WFRuleBuilder.BuildBundlePromotionRule(bundlePromo));
});
Console.WriteLine("\nRules number: {0}", promotionRules.Rules.Count);

Stopwatch watch = new Stopwatch();
RuleValidation validation = new RuleValidation(typeof(ShoppingCart), null);
if (promotionRules.Validate(validation))
{
RuleEngine engine = new RuleEngine(promotionRules, validation);
watch.Start();
engine.Execute(mockShoppingCart);
watch.Stop();
}
else // Validation failed, print errors
{
StringBuilder errors = new StringBuilder();
foreach (ValidationError validationError in validation.Errors)
{
errors.Append(validationError.ErrorText);
}
Console.WriteLine("Validation errors:{0}", errors);
}

Console.WriteLine("Execution time in milliseconds: {0}", watch.ElapsedMilliseconds);

Console.Write("\nShopping Cart after rules execution -- ");
Console.WriteLine("Total : ${0}\tDiscount : ${1}", mockShoppingCart.TotalAmount, mockShoppingCart.Discount);
Console.Read();
}
The test result in an i5-CPU 8G memory Windows 2008 machine:



It shows the execution time for 400 rules is around 20-millisecond. In reality the rules would be more complex than our simple test case. But in general we can see that the speed of Windows Workflow Rules Engine is okay.

Friday, October 29, 2010

WCF 4.0 CommunicatinException

Found an exception sometimes in WCF 4.0 communication with wsHttpBinding binding in a test environment:

CommunictionException: "An error occurred while receiving the HTTP response to ....svc. This could be due to the service endpoint binding not using the HTTP protocol. This could also be due to an HTTP request context being aborted by the server (possibly due to the service shutting down). See server logs for more details."

InnerExcpetion: "The underlying connection was closed: An unexpected error occurred on a receive."

InnerInnerException: "An existing connection was forcibly closed by the remote host"

NativeErrorCode: 10054

Turn on WCF logging in the server and found following error message using Microsoft Service Trace Viewer:

"There was an error while trying to serialize parameter http://tempuri.org/:Get...Result. The InnerException message was 'Maximum number of items that can be serialized or deserialized in an object graph is '65536'. Change the object graph or increase the MaxItemsInObjectGraph quota. '. Please see InnerException for more details."

Update server's web.config as suggested:
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior>
<!-- ... -->
<dataContractSerializer maxItemsInObjectGraph="2147483647" />
</behavior>
</serviceBehaviors>
</behaviors>
<!-- ... -->
</system.serviceModel>
Now get another exception in the client:
NetDispatcherFaultException: The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:Get...Result. The InnerException message was 'Maximum number of items that can be serialized or deserialized in an object graph is '65536'. Change the object graph or increase the MaxItemsInObjectGraph quota. '. Please see InnerException for more details.
Inner Exception: "Maximum number of items that can be serialized or deserialized in an object graph is '65536'. Change the object graph or increase the MaxItemsInObjectGraph quota. "

Update client's app.config:
<system.serviceModel>
<client>
<endpoint behaviorConfiguration="ServiceBehavior" _A_B_C_Setting... />
</client>
<behaviors>
<endpointBehaviors>
<behavior name="ServiceBehavior">
<dataContractSerializer maxItemsInObjectGraph="2147483647"/>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
The exception is gone after configuration update.

Saturday, December 19, 2009

WorkflowInvoker in WF 4.0

Just noticed this new and handy class in .NET 4.0 Beta 2. Following is its description on MSDN document:

"Windows Workflow Foundation (WF) provides several methods of hosting workflows. WorkflowInvoker provides a simple way for invoking a workflow as if it were a method call and can be used only for workflows that do not use persistence."

Simple enough, if we just want to invoke a workflow synchronously with current thread (WorkflowInvoker also provides asynchronous versions of the invoke method with InvokeAsync and BeginInvoke), WorkflowInvoker is your best friend, and you don't need to set up the environment for workflow runtime:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Activities;
using System.Activities.Statements;

class Program
{
static void Main(string[] args)
{
Activity activity = new WriteLine() { Text = "Workflow running at " + DateTime.Now.ToString() };
WorkflowInvoker.Invoke(activity);
Console.ReadLine();
}
}
For long running workflows or persistence scenarios, .NET 4.0 also added a new WorkflowApplication class, which provides a richer model for executing workflows that includes notification of lifecycle events, execution control, bookmark resumption, and persistence. Details refer to http://msdn.microsoft.com/en-us/library/system.activities.workflowapplication.aspx.