Tuesday, March 04, 2008

Ever since I first got into workflow foundation, I've taken a fancy to statemachines. Once you wrap your head around them, they are a natural fit for most business processes.
The main problem everybody seems to be having with workflow though, is the versioning story. There is none!
That might be a bit harsh, you can certainly version your workflows, but to tell you the truth, you will be in a world of hurt.

The sample solution can be downloaded at the end of the post. It contains two workflows and a console application that you can play with.

Why is this updating so tough?
The workflow template is serialized to the persistence store. Any change in the workflow (adding or removing an activity) will make it impossible to deserialize the workflow again. It's serialized as a blob, so no easy transformation. I've written extensively about problems surrounding updating workflows here.

Your options pretty much exist of running side by side (which gives you a world of even more hurt, because now you have your data exchange services to version as well, and the activity library you have built) or use dynamic changes to alter the structure.
The latter being your best bet, but so much work that it takes away from the flexibility and speed of development that workflow brings to the table.

In my previous post I concluded that you would be best of just destroying your old workflow and create a new one. I stand by that! Today I was finally able to revisit the problem, and I hacked together a solution that might be interesting to people.

This solution has the following restriction:

It will only work for statemachines, that are waiting inside a state for an eventdriven activity, not inside an eventdriven activity. In other words: it is only able to update workflows that have entered a state and started waiting, not ones that have executed a few activities and is now waiting on some other input within a sequence.

Luckily for me, that is no problem at all, and it should not be a problem for you either. Statemachines should be modeled such that waiting happens when entered in a state, never inside a sequence. You can model waits inside a sequence, but I would suggest you make the delays short (minutes, as opposed to days/months/years).

My goal here is to be able to do a relatively easy update, where I have control over how I update (what to do with state etc.) and get my delays initialized to the correct timeouts again. So, in workflow1 I had a delay of 11 months, with 8 months left. When I start workflow2 and update, I need to have 8 months left again, and not 11.

Getting the delays right is the hard part.

I use some nice reflection to get to the actual type of a workflow instance. I described how to do that here. However, I was being silly. It's much easier:

            Workflow1 oldWF = workflowRuntime.GetRootActivity(instance) as Workflow1;

Made possible by these extensions:

    public static class WFExtensions
    {
        public static object GetExecutor(this WorkflowRuntime workflowRuntime, WorkflowInstance instance)
        {
            return workflowRuntime.GetType().InvokeMember(
                "Load", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, workflowRuntime,
                new object[] { instance.InstanceId, null, instance });
        }
        public static object GetRootActivity(this WorkflowRuntime workflowRuntime, WorkflowInstance instance)
        {
            object executor = workflowRuntime.GetExecutor(instance);
            return executor.GetType().GetField("rootActivity",
                    BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField).GetValue(executor) as CompositeActivity;
        }
    }

So, here goes:

  1. Get to your old workflow instance. In my sample I use types Workflow1 and Workflow2.
                WorkflowInstance instance = runtime.GetWorkflow(g);
    
                WorkflowRuntime workflowRuntime = runtime;
    
                 
                Workflow1 oldWF = workflowRuntime.GetRootActivity(instance) as Workflow1;
    
                if (oldWF == null)
    
                    return;
    
                object executor = workflowRuntime.GetExecutor(instance);
    
                instance.Suspend("asdf");   // need not to unload, otherwise the database record would be unlocked

    I suspend the workflow, so it does not get into the way, but I can not unload, or worse: terminate. That would kill the record in the database.

  2. Create a new workflow, of your desired type, and copy the workflowInstanceID to it:
                // get a handle to the instanceid property
    
                DependencyProperty instanceidDP = (DependencyProperty)executor.GetType().GetField("WorkflowInstanceIdProperty",
    
                    BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).GetValue(executor);
    
                // create new wf2, not starting it yet
    
                WorkflowInstance newWFInstance = workflowRuntime.CreateWorkflow(typeof(Workflow2));
    
                Workflow2 newWF = workflowRuntime.GetRootActivity(newWFInstance) as Workflow2;
    
                // copy the guid
    
                newWF.SetValue(instanceidDP, instance.InstanceId);
  3. Build up a list of activities that are on timers and remember their name and when they expire:
                Dictionary<string, DateTime> activitiesExpireList = new Dictionary<string, DateTime>();
    
                TimerEventSubscriptionCollection subscriptions = ((TimerEventSubscriptionCollection)
    
                    oldWF.GetValue(TimerEventSubscriptionCollection.TimerCollectionProperty));
    
                foreach (TimerEventSubscription subscription in subscriptions)
    
                {
    
                    // find out what activity was subscribed
    
                    var x = from queueInfo in instance.GetWorkflowQueueData()
    
                            where subscription.QueueName.GetType().Equals(queueInfo.QueueName.GetType())
    
                            where subscription.QueueName.CompareTo(queueInfo.QueueName) == 0
    
                            select new { ExpiresAt = subscription.ExpiresAt, Activities = queueInfo.SubscribedActivityNames };
    
                    foreach (var combination in x)
    
                    {
    
                        foreach (string activityname in combination.Activities)
    
                        {
    
                            activitiesExpireList.Add(activityname, combination.ExpiresAt);
    
                        }
    
                    }
    
                }

    The weird part being the fact that the queue names are mostly guids (for delays atleast).

  4. Call a method on your new type. See how cool it is we can actually communicate this way with it, instead of having to go through communication services!!
                // allow new workflow to read information from old workflow to init itself.
    
                newWF.Update(oldWF, instance, activitiesExpireList);
  5. Copy the new workflow to the rootactivity of our executor. Ouch.. yeah.. don't worry.
                // copy the new rootactivity to the executor
    
                executor.GetType().GetField("rootActivity",
    
                    BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField).SetValue(executor, newWF);
    
  6. Last bits:
                // start it up
    
                newWFInstance.Start();
    
                newWFInstance.Unload(); // overwrites current record in persistence store
    
                instance.Abort();   // kills of our original
    
                newWFInstance = runtime.GetWorkflow(g);
    
                StateMachineWorkflowInstance statemachine = new StateMachineWorkflowInstance(runtime, g);
    
                statemachine.SetState(newWF.DefineNewStateAfterUpdate(oldWF.CurrentStateName));
    
                // still need to unload or unload the runtime to get all timers correctly!
    
                Console.WriteLine("updated" + newWFInstance.InstanceId);

    You can see me starting and unloading, then killing our old instance. Finally I am trying to be smart by using the statemachineworkflowinstance to do a transition to a new state on the new workflow. The newstate can be determined by the new workflow (who has knowledge of these things) but is usually the same as in your old workflow. (This was build so that you could rename a state).

  7. That's it. In the Workflow2 class, I have an update method, which will set a boolean to true. The initialization activity will look for it in an if/else and not do anything if it is set to true. All the delays in the new workflow have an initTimeout method like so:
            private void initTimeout(object sender, EventArgs e)
    
            {
    
                DelayActivity delay = (DelayActivity)sender;
    
                if (activitiesExpireList.ContainsKey(delay.Name))
    
                {
    
                    delay.TimeoutDuration = activitiesExpireList[delay.Name].Subtract(DateTime.Now.ToUniversalTime());
    
                    activitiesExpireList.Remove(delay.Name);
    
                }
    
            }
     

I have uploaded the complete sample here.

When you run it, you can press 'c' to create a new workflow of type Workflow1. Then you can press 'u' and paste in the guid of the workflow just created. It will update the workflow. Pressing 'b' will break and unload the workflow.
Your created workflow has this state:

image

Where the delay is 40 seconds. Workflow2 has the same state, but has a delay of only 10 seconds.

As a test you can see that after updating, you will have a workflow2 running (there is another activity present that will print out debug information). The delay was set correctly.

Obviously, you might want to deal with the delays your own way. Because you have all the information in your workflow codebehind, you can think of your own rules on how the delay timeouts should be set.

Realize that touching the internals of WF like this is not what Microsoft envisioned and should be done with care.

Have fun, and let me know what you think.

kick it on DotNetKicks.com

Thursday, May 08, 2008 10:13:58 AM (Romance Standard Time, UTC+01:00)
I've not tried it yet, but assuming it works, thank you!
David
Friday, May 09, 2008 4:35:27 PM (Romance Standard Time, UTC+01:00)
I've had several mails from people actually using the technique in live systems, so it will work.
However, the tracking system might need to be taken care of.
Ruurd
Tuesday, June 24, 2008 2:43:15 PM (Romance Standard Time, UTC+01:00)
Hi
I tried implementing above solution but i am using .net 3.0 framework and above solution works in .Net 3.5
Can we do the same thing in .net 3.0 ? Somehow i tried implmenting it in .Net 3.0 But not abel to set the state of new workflow with the old state. However i am abel to replace the old workflow with the new one.
I guess it happnes because of the framework version. Can you please tell me how exactly can i implement it in .net 3.0 ?

Regards
Atul
Atul Bachhav
Thursday, July 24, 2008 11:55:54 AM (Romance Standard Time, UTC+01:00)
Hi Atul,

sorry. I don't have a 3.0 only machine here. It shouldn't really be a problem though! I'm quite sure you'll get it to work.

Good luck,
Ruurd
Ruurd
Wednesday, December 17, 2008 11:30:31 PM (Romance Standard Time, UTC+01:00)
Very interesting solution Ruurd. We came across the EXACT problem when putting together our own set of long running state machine instances. We will be going through a NUMBER of changes to our workflows, so we needed a way to apply those changes but didn't want to go through the hurdles that you describe above (maintaining multiple assembly versions or going through the WF Upadating objects - both are BAAAAD).

I was never able to get an instance of the workflow once the version number had changed or the XOML behind it had changed. I assume that your reflection tricks and the code in step one rectify that hurdle. If so, I'll have to give that a try.

I eventually came to an unfortunate solution that leaves the old workflow orphaned only to use the new one (until it changes again too). However, the transition to the new workflow is very easy. The only problem was history. I have a customer who needs very good historical information regarding state change history. I had to come up with a solution that keeps track of my instance Ids in the tracking database and gives the user an ongoing history for the state changes regardless of which workflow instance is running at the given time.

How does your solution work with keeping track of state changes in the tracking database?

I'll give your solution a try soon.

Thanks for the post!
Friday, December 19, 2008 6:34:31 PM (Romance Standard Time, UTC+01:00)
Hi Paul,

the situation is very unfortunate and I have come to the conclusion that you need to design workflows with this upgrade plan in mind.
All in all I have not done anything to the tracking service, so you will be in bad shape there.

It might be possible to just rename the guid of the new workflow to the new one in the database though.

Tricks like that should not be possible, and i'm hoping wf 4 will mitigate the situation.

Success!
RJ
Ruurd
Thursday, October 14, 2010 3:01:18 PM (Romance Standard Time, UTC+01:00)
<p>Of the 30 million in the <a href="http://www.fivefingeronline.com/">vibram fivefingers</a> activities, 2010 non-environment-majors on China walk first stage <a href="http://www.fivefingeronline.com/">vibram five</a> activities, the poem ended Co., LTD. Offers outdoor products to both the retail price is lucky <a href="http://www.fivefingeronline.com/products_all.html">five finger</a> skywalker prize for ¥1980 German professional outdoor shoe in <a href="http://www.fivefingeronline.com/featured_products.html">vibram shoes</a> on GTX RENEGADE for hiking shoes. They will be wearing this pair of the world’s most powerful boots origin produces the boots top <a href="http://www.fivefingeronline.com/featured_products.html">five fingers</a> product officially began to walk.Called “outside” of the ID of the winners is walking <a href="http://www.fivefingeronline.com/products_new.html">vibram five finger</a> across the big horqin sandy land “green channel”. Inner Mongolia horqin sandy land and <a href="http://www.fivefingeronline.com/vibram-five-fingers-kso-c-67.html">fivefingers kso</a> liaoning, jilin across three provinces, 92% of eastern Inner Mongolia above <a href="http://www.fivefingeronline.com/products_new.html">vibram five fingers</a> distribution in chifeng city, area and the tongliao 5 million square <a href="http://www.fivefingeronline.com/specials.html">vibram five fingers shoes</a> kilometers, ranking first in four sand. Genghis khan’s <a href="http://www.fivefingeronline.com/vibram-five-fingers-kso-c-67.html">vibram kso</a> brother, and its subordinate subordinates’ called cheap five finger shoes, then formed by <a href="http://www.fivefingeronline.com/vibram-five-fingers-classic-c-76.html">vibram classic</a> tribe.In the first stage, the activity ended at the second stage, the second phase will start to Tibet, xinjiang, “stand in xinjiang, ningxia, Tibet, shaanxi, gansu, qinghai and within the <a href="http://www.fivefingeronline.com/vibram-five-fingers-flow-c-73.html">vibram flow</a> scope of land.</p>
<p>If you want to take part in outdoor activities, persistent and very love this <a href="http://www.fivefingeronline.com/vibram-five-fingers-kso-c-67.html">vibram fivefingers kso</a> sport, so choose cheap vibram is very important. A set of suitable equipment can indeed for our outdoor activities to provide protection, let us be more close to nature, feel nature.We choose to suit <a href="http://www.fivefingeronline.com/reviews.html">5fingers</a> oneself is suitable for environmental equipment, so we first concern is the applicability of the equipment, rather than spend less money for price, event, although is good, but usually sale <a href="http://www.fivefingeronline.com/vibram-five-fingers-speed-c-115.html">five finger speed</a> is reality, the most expensive sometimes are not is most appropriate. There is not expected to equipment can be armed to the teeth. A bite to eat dinner.</p>
<p><a href="http://www.fivefingeronline.com/">MBT shoes</a> have become increasingly popular. Because of its tremendous role to humans. <a href="http://www.fivefingeronline.com/products_all.html">mbt walking shoes</a> not only fashionable, but also healthy. I think this is more important is fashion. Swiss <a href="http://www.fivefingeronline.com/specials.html">mbt shoes sale</a>, Karl Mueller invented. His invention came to the United Kingdom. In tourism this journey, it can be found wearing <a href="http://www.fivefingeronline.com/products_new.html">mbt sale</a> can easily get ahead, he who is like a walk in the soft grass. It takes years of research and development, <a href="http://www.fivefingeronline.com/reviews.html">mbt shoes review</a> Sale market development began in 1996, selling over one million every year is growing <a href="http://www.fivefingeronline.com/">mbt footwear</a> rapidly.<a href="http://www.fivefingeronline.com/">http://www.fivefingeronline.com/</a> </p>
Thursday, October 14, 2010 3:02:12 PM (Romance Standard Time, UTC+01:00)
3_103.html"><strong> </strong></a> are made more unique by the inclusion of a large wooden button with the Ugg logo on it and an elastic band closure. So it allows you to wear t
Just as with the <a href="http://www.shoeslockers.com/ugg-boots-ugg-classic-cardy-c-53_108.html"><strong>UGG boots</strong></a> the new calf high boot that this <a href="http://www.shoeslockers.com/ugg-boots-ugg-knightbridge-boots-c-53_118.html"><strong>Ugg Australia</strong></a> company have introduced the Bailey Button offers the same level of comfort. Certainly for women who are looking for a pair of <a href="http://www.shoeslockers.com/ugg-boots-ugg-lo-pro-button-c-53_124.html"><strong>UGG sale</strong></a> to team with jeans or a long skirt these are ideal.But as you will soon discover these <a href="http://www.shoeslockers.com/ugg-boots-ugg-bailey-button-c-5hem with this detailing on shown or you can undo the button and then turn the top over to create a cuff.With all the boots included in <a href="http://www.shoeslockers.com/ugg-boots-ugg-new-style-ultra-short-boots-c-53_122.html"><strong>Cheap UGG boots</strong></a> Australia’s Classic Collection range the <a href="http://www.shoeslockers.com/ugg-boots-ugg-classic-mini-c-53_114.html"><strong>UGG bailey button</strong></a> is lined with twin faced Grade A sheepskin. Also there is the traditional suede heel guard to ensure that not only is this area protected but offers this part of the <a href="http://www.shoeslockers.com/ugg-boots-ugg-bailey-button-fancy-c-53_102.html"><strong>UGG boots sale</strong></a> support. Plus within the <a href="http://www.shoeslockers.com/ugg-boots-ugg-classic-short-c-53_113.html"><strong>UGG classic cardy</strong></a> you find each pair has sock liners that are made from sheepskin PU foam so increasing the level of comfort a person feels when wearing <a href="http://www.shoeslockers.com/ugg-boots-ugg-classic-short-c-53_113.html"><strong>UGG short boots</strong></a>.<a href="http://www.shoeslockers.com/ugg-boots-ugg-classic-tall-c-53_110.html"><strong>zigtech shoes</strong></a> is very unique. It uses an innovative lightweight foam materials,<a href="http://www.shoeslockers.com/reebok-zigtech-shoes-c-127.html"><strong>ZigTech trainers</strong></a> are designed to help you run further and faster, allowing you to train while using their unique <a href="http://www.shoeslockers.com/reebok-easytone-shoes-c-126.html"><strong>Easytone shoes</strong></a> harder for longer with reduced risk of injury. They do this by providing lots of cushioning Yesterday we saw the promo video featuring Thierry Henry and Lewis Hamilton talking about the trainers. So today we’re looking at the latest technical video released by Reebok to explain what <a href="http://www.shoeslockers.com/women-mbt-shoes-mbt-fora-shoes-c-49_61_66.html"><strong>Discount Reebok Easytone</strong></a> is and how the <a href="http://www.shoeslockers.com/ugg-boots-ugg-knightbridge-boots-c-53_118.html"><strong>zigtech sale</strong></a> sole works.in the Slinky-esque midsole/outsole that is said to reduce muscle wear and transfer energy. <a href="http://www.shoeslockers.com/ugg-boots-ugg-lo-pro-button-c-53_124.html"><strong>Reebok zigtech shoes</strong></a> Stadium bring an innovative technology shoes- <a href="http://www.shoeslockers.com/men-mbt-shoes-mbt-casual-shoes-c-49_54_55.html"><strong>Reebok easytone shoes</strong></a> training run. We series, is the best <a href="http://www.shoeslockers.com/products_new.html"><strong>Easy tone shoes</strong></a> sport technology to date. It can reduce 20% of the leg muscle fatigue and wear, in order to cultivate strength and degree of difficulty is significantly increased.<a href="http://www.shoeslockers.com/men-mbt-shoes-mbt-sandals-shoes-c-49_54_57.html"><strong>MBT shoes</strong></a> effect abdominal, leg and buttock muscles.cheap <a href="http://www.shoeslockers.com/mbt-shoes-men-mbt-shoes-c-49_54.html"><strong>Cheap mbt shoes</strong></a> looks sunny, so it is a good choice <a href="http://www.shoeslockers.com/mbt-shoes-c-49.html"><strong>MBT shoes sale</strong></a> believe that human fitness majestic, <a href="http://www.shoeslockers.com/skechers-shapeups-shapeups-sleek-fit-c-50_75.html"><strong>Skecher Shape Ups</strong></a> walk to a large extent. This view wholesale <a href="http://www.shoeslockers.com/women-mbt-shoes-mbt-lami-shoes-c-49_61_67.html"><strong>Discount MBT Shoes</strong></a> a number of scientific ,Therefore, majestic mbt shoes and strengthen peoples understanding <a href="http://www.shoeslockers.com/skechers-shapeups-shape-ups-stability-c-50_74.html"><strong>shape ups Shoes</strong></a> .in <a href="http://www.shoeslockers.com/ugg-boots-ugg-classic-tall-stripe-cable-c-53_101.html"><strong>shape up skechers</strong></a> through disseminate the majestic <a href="http://www.shoeslockers.com/ugg-boots-ugg-classic-tall-metallic-c-53_112.html"><strong>shape ups discount</strong></a> ideas and concepts.
Friday, October 15, 2010 8:56:25 AM (Romance Standard Time, UTC+01:00)
href="http://www.feetlockers.com/reebok-easytone-reebok-easytone-womens-shoes-c-25_28.html">Reebok Easytone Shoes</a> syndication. And then there are those who are having <a href="http://www.feetlockers.com/specials.html">Discount Reebok zigtech</a> trouble getting out of their own way.The folks who are not succeeding are the ones whose first question is “where do I find a good deal?” Early stage syndicators think they need to find a great deal, then raise capital, and they will be on their way toward success and profit. I usually counter that with some tough love and a reality check. Instead of looking for deals and Reebok shoes, a prudent <a href="http://www.feetlockers.com/reebok-easytone-reebok-zig-pulse-running-shoes-c-25_26.html">Reebok EasyTone</a> syndicator needs to put the horse in front of the deal, and start with the question of how to raise capital.
Friday, October 15, 2010 8:56:33 AM (Romance Standard Time, UTC+01:00)
back pain and lumbosacral joint.Moreover, the girls who wear high heels early will lead to <a href="http://www.zigtechshoesonsale.com/mbt-men-shoes-mbt-casual-shoes-c-8_9.html"><strong>Discount Mbt Shoes</strong></a> the developing pelvic tilt, which make the <a href="http://www.zigtechshoesonsale.com/mbt-men-shoes-mbt-sandals-shoes-c-8_11.html"><strong>Mbt shoes clearance</strong></a>, is demanding.However, they are not only for people on their feet but for anyone who wants to wear a pair of comfortable <a href="http://www.zigtechshoesonsale.com/mbt-women-shoes-mbt-sports-shoes-c-15_26.html"><strong>Mbt shoes outlet</strong></a> that takes the pain away from your body when you are standing and walking. That’s why you need to wear <a href="http://www.zigtechshoesonsale.com/mbt-men-shoes-mbt-casual-shoes-c-8_9.html"><strong>MBT Sandals Shoes</strong></a>.
Tuesday, October 26, 2010 2:58:13 AM (Romance Standard Time, UTC+01:00)
href="http://www.feetlockers.com/reebok-easytone-c-25.html">Reebok ZigTech</a> tests the shoes on real runners who fill in a detailed questionnaire at the start, mid-way and end of 300-mile increments. They’re light, flexible and so comfortable. <a href="http://www.feetlockers.com/reebok-easytone-c-25.html">Reebok ZigTech shoes</a> footwear, training longer just got easier.<a href="http://www.feetlockers.com/reebok-easytone-reebok-easytone-womens-shoes-c-25_28.html">Reebok Easytone Shoes</a> has the top technology which uses shoes’ energy and transfers it horizontally. ZigTech helps to transfer weight through the whole range of motion by pushing the force of the strike through to the front of your foot. <a href="http://www.feetlockers.com/specials.html">Discount Reebok zigtech</a> footwear is designed to protect your feet and return energy to the athlete for a soft and springy ride. The foam is lightweight. Forefoot flex grooves for added <a href="http://www.feetlockers.com/reebok-easytone-reebok-zig-pulse-running-shoes-c-25_26.html">Reebok EasyTone</a> flexibility. This ultimately enables athletes to stay healthier during in this season.<br>
Tuesday, October 26, 2010 2:58:23 AM (Romance Standard Time, UTC+01:00)
href="http://www.zigtechshoesonsale.com/mbt-men-shoes-mbt-sandals-shoes-c-8_11.html"><strong>Cheap mbt shoes</strong></a> is not too high, if the heel is higher than 3.5 cm, it will increase the risk of lumbar lordosis and lumbosacral angle, which easily lead to fatigue and lower back pain and lumbosacral joint.Moreover, the girls who wear high heels early will lead to <a href="http://www.zigtechshoesonsale.com/mbt-men-shoes-mbt-casual-shoes-c-8_9.html"><strong>Discount Mbt Shoes</strong></a> the developing pelvic tilt, which make the <a href="http://www.zigtechshoesonsale.com/mbt-men-shoes-mbt-sandals-shoes-c-8_11.html"><strong>Mbt shoes clearance</strong></a>, is demanding.However, they are not only for people on their feet but for anyone who wants to wear a pair of comfortable <a href="http://www.zigtechshoesonsale.com/mbt-women-shoes-mbt-sports-shoes-c-15_26.html"><strong>Mbt shoes outlet</strong></a> that takes the pain away from your body when you are standing and walking. That’s why you need to wear <a href="http://www.zigtechshoesonsale.com/mbt-men-shoes-mbt-casual-shoes-c-8_9.html"><strong>MBT Sandals Shoes</strong></a>.
Thursday, November 04, 2010 4:37:29 AM (Romance Standard Time, UTC+01:00)
http://www.coachusabags.com/
http://www.asics-us.com/
http://www.ukuggus.com/
http://www.mbt-usa.com/
Thursday, December 02, 2010 11:25:07 AM (Romance Standard Time, UTC+01:00)
Do you want to buy Jimmy Choo shoes,why not have a try at http://www.discountjimmychoo-uk.com. We supply all kinds of shoes with different styles,you will find what your like.All Jimmy Choo products here have good quality and low price,act now!
Thursday, December 02, 2010 11:25:21 AM (Romance Standard Time, UTC+01:00)
there is a good website to purchase MBT Sale,it is http://www.mbtantishoesstore.com, MBT Shoes are sold at high discount and all MBT products are very cheap,don't hesitate,get them now
Comments are closed.