Answered by:
Update in Plug-in Not Working

Question
-
I'm developing a plug-in to make sure that one and only one Opportunity record is tagged as Primary ((bool) new_IsPrimary = true). I'm now working on the Update piece of the plugin assembly. This piece needs to accomplish two functions:
(1) Don't allow a record that was Primary to be changed to non-Primary (this is done automatically when a new primary record is chosen).
(2) When a record is set to Primary (that was non-Primary), change the existing Primary record to non-Primary.
Function #1 is working but #2 is not (and where I need help).
I have registered the Plug-in as Update, Pre-Operation, Synchronus with fields filtered to just new_IsPrimary.
Here is the code with the portion not working marked with "//***************" above and below.
using System; using System.Diagnostics; using System.Linq; using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Description; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using Microsoft.Xrm.Sdk.Client; using Microsoft.Xrm.Sdk.Messages; using Microsoft.Crm.Sdk.Messages; namespace WorkoutPrimaryControl { public class WomUpdate : IPlugin { Entity entity; public void Execute(IServiceProvider serviceProvider) { IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); // Check if the input parameters property bag contains a target // of the create operation and that target is of type Entity. if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) { // Obtain the target business entity from the input parameters. entity = (Entity)context.InputParameters["Target"]; // Verify that the entity represents a contact. if (entity.LogicalName != "opportunity") { return; } } else { return; } // Update Business Logic try { IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId); // Convert Late Bound to Early Bound Entity Opportunity opp = entity.ToEntity<Opportunity>(); // If IsPrimary = False (was True before Update) then abort Update issue explanitory User message if (opp.new_IsPrimary == false) { throw new InvalidPluginExecutionException("You cannot change the Primary Workout Method for a loan. Set another Workout Method as Primary and this Workout will automatically be reset as non-primary."); } //********************************************************************************** // IsPrimary = true (was false). There must be at least two WOMs else { using (var crm = new ServiceContext(service)) { // Define LINQ for all WOMs of Loan except WOM being updated var wom = from o in crm.OpportunitySet where o.CustomerId == opp.CustomerId && o.OpportunityId != opp.OpportunityId select o; // Walk the Look for the old Primary WOM and change to non-Primary foreach (var w in wom) { if (w.new_IsPrimary == true) { // Make any primary records, non-primary w.new_IsPrimary = false; crm.UpdateObject(w); crm.SaveChanges(); } } // foreach } // using } // else //********************************************************************************** } catch (FaultException<OrganizationServiceFault> ex) { throw new InvalidPluginExecutionException("An error occurred in the Workout Primary Control plug-in.", ex); } // end catch } // end method } // end class } // Namespace
The code runs without exceptions, but when I set a record from non-Primary to Primary, the existing Primary record IS NOT change from Primary to non-Primary as I had expected.
Any help would be appreciated.
- Edited by Kahuna2000 Friday, June 22, 2012 3:05 PM
Friday, June 22, 2012 3:04 PM
Answers
-
Hi Kuhana,
Your issue maybe that you are not using a PreImage to get the value of 'CustomerId' so unless the value is being updated by the save, it will not be in the Target Entity. The Target entity only contains the field values that are changing on update (and the record Id) - where on Create it will include all the field values. You must register a PreImage on your plugin step, and then if the CustomerId is null, get it from the PreImage.
More information can be found here:
http://msdn.microsoft.com/en-us/library/gg309673.aspx
hth,
Scott
Scott Durow
Read my blog: www.develop1.net/public
If this post answers your question, please click "Mark As Answer" on the post and "Mark as Helpful"- Marked as answer by Kahuna2000 Saturday, June 23, 2012 2:16 PM
Friday, June 22, 2012 5:49 PMAnswerer -
Hi Kahan,
I think this is probably caused by your own plugin preventing itself from setting isprimary to false. You can test this theory by removing the throwing of an exception when isprimary is set to false.
To work around, you could check the execution context depth and only throw the exception on setting to false if it is at the top level.
Hth,
Scott
Scott Durow
Read my blog: www.develop1.net/public
If this post answers your question, please click "Mark As Answer" on the post and "Mark as Helpful"- Proposed as answer by Scott Durow (MVP)MVP, Editor Saturday, June 23, 2012 10:32 AM
- Marked as answer by Kahuna2000 Saturday, June 23, 2012 2:16 PM
Saturday, June 23, 2012 6:51 AMAnswerer -
Hi Kuhana,
Yes, you can just set the value on the target entity and it will be written to the database. This only works for pre stages, which you are already using.
Glad you've got it sorted!
Cheers,
Scott
Scott Durow
Read my blog: www.develop1.net/public
If this post answers your question, please click "Mark As Answer" on the post and "Mark as Helpful"- Marked as answer by Kahuna2000 Saturday, June 23, 2012 3:29 PM
Saturday, June 23, 2012 3:10 PMAnswerer
All replies
-
You can debug your plugin.
Just follow the steps mentioned in this link: http://weblogs.asp.net/pabloperalta/archive/2010/12/01/how-to-remote-debug-dynamics-crm-plugins-and-workflow-assemblies.aspx
Regards Karan Mittal
Friday, June 22, 2012 4:05 PM -
We are CRM 2011 Online.Friday, June 22, 2012 5:23 PM
-
Hi Kuhana,
Your issue maybe that you are not using a PreImage to get the value of 'CustomerId' so unless the value is being updated by the save, it will not be in the Target Entity. The Target entity only contains the field values that are changing on update (and the record Id) - where on Create it will include all the field values. You must register a PreImage on your plugin step, and then if the CustomerId is null, get it from the PreImage.
More information can be found here:
http://msdn.microsoft.com/en-us/library/gg309673.aspx
hth,
Scott
Scott Durow
Read my blog: www.develop1.net/public
If this post answers your question, please click "Mark As Answer" on the post and "Mark as Helpful"- Marked as answer by Kahuna2000 Saturday, June 23, 2012 2:16 PM
Friday, June 22, 2012 5:49 PMAnswerer -
What error are you getting?
Oğuz Erdeve
Friday, June 22, 2012 7:15 PM -
Oğuz,
"The code runs without exceptions (errors), but when I set a record from non-Primary to Primary, the existing Primary record IS NOT change from Primary to non-Primary as I had expected."
Scott may have the answer, I'm coding it up now to see.
Friday, June 22, 2012 7:18 PM -
OK, thanks to Scott, I have gotten further. I got a PreImage of the Update record which gives me the parameters needed for the LINQ query. I verified that the query is returning the correct number of records, so all seems well to that point. So now when I test it by changing an existing record from non-Primary to Primary, it fails with a "Unexpected exception from plug-in (Execute): WorkoutPrimaryControl.WomUpdate: Microsoft.Xrm.Sdk.SaveChangesException: An error occured while processing this request." Here is the current code with the suspect marked with "//***********" above and below.
using System; using System.Diagnostics; using System.Linq; using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Description; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using Microsoft.Xrm.Sdk.Client; using Microsoft.Xrm.Sdk.Messages; using Microsoft.Crm.Sdk.Messages; namespace WorkoutPrimaryControl { public class WomUpdate : IPlugin { Entity entity; Opportunity opp; public void Execute(IServiceProvider serviceProvider) { IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); // Check if the input parameters property bag contains a target // of the create operation and that target is of type Entity. if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) { // Obtain the target business entity from the input parameters. entity = (Entity)context.InputParameters["Target"]; // Verify that the entity represents a contact. if (entity.LogicalName != "opportunity") { return; } } else { return; } // Update Business Logic try { IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId); // Get PreImage Entity in opp if (context.PreEntityImages.Contains("PreImage") && context.PreEntityImages["PreImage"] is Entity) { // Get PreImage Entity preMessageImage = (Entity)context.PreEntityImages["PreImage"]; // Convert Late Bound to Early Bound Entity opp = preMessageImage.ToEntity<Opportunity>(); } else { return; } // If IsPrimary = False (was True before Update) then abort Update and issue explanitory User message if ((bool)entity.Attributes["new_isprimary"] == false) { throw new InvalidPluginExecutionException("You cannot change the Primary Workout Method for a loan. Set another Workout Method as Primary and this Workout will automatically be reset as non-primary."); } // IsPrimary = true (was false). There must be at least two WOMs else { using (var crm = new ServiceContext(service)) { // Define LINQ for all WOMs of Loan except WOM being updated var wom = from o in crm.OpportunitySet where o.CustomerId == opp.CustomerId && o.OpportunityId != opp.OpportunityId select o; //int womCount = wom.ToList().Count; //throw new InvalidPluginExecutionException("We have " + womCount + " Woms"); // Look for the old Primary WOM and change them to non-Primary foreach (var w in wom) { if (w.new_IsPrimary == true) { // Make any primary records, non-primary //********************************************************************************** w.new_IsPrimary = false; crm.UpdateObject(w); crm.SaveChanges(); //********************************************************************************** } } // foreach // Committ the changes } // using } // else } catch (FaultException<OrganizationServiceFault> ex) { throw new InvalidPluginExecutionException("An error occurred in the Workout Primary Control plug-in.", ex); } // end catch } // end method } // end class } // Namespace
Any help, ideas or links would be appreciated.Saturday, June 23, 2012 2:11 AM -
By reading your code, I suspect two places that might cause problems.
The first one:
// Define LINQ for all WOMs of Loan except WOM being updated var wom = from o in crm.OpportunitySet where o.CustomerId == opp.CustomerId && o.OpportunityId != opp.OpportunityId select o;
Now your wom is of type IQueryable<Opportunity>, by definition of IQueryable, the elements are actually not retrieved here yet. They are fetched only when you enumerating through the IQueryable object. So, depending on the CRM IQueryable provider implementation, modifying the elements while iterating through the IQueryable could cause an inconsistent state error. I will try to illustrate the steps with an imaginary IQuerable implementation.
Time 1:
1. execute the code var wom = ... create an expression tree that can be used to fetch items later.
2. LINQ provider remembers express tree creation time as T1
Time 2:
1. get the first element from wom, which will cause the expression tree to be evaluated and ask the web service for one element.
2. modify it and call context.SaveChanges,
3. Context remembers last web service change time as T2
Time 3:
1. try to get second element from wom, cause the expression tree to be evaluated, ask web service for one element, but since the expression tree creation time T1 < web service change time T2. The IQueryble become "dirty" can not usable, throw an exception
To avoid the senario described above, simply modify
var wom = from o in crm.OpportunitySet where o.CustomerId == opp.CustomerId && o.OpportunityId != opp.OpportunityId select o;
to
var wom = (from o in crm.OpportunitySet where o.CustomerId == opp.CustomerId && o.OpportunityId != opp.OpportunityId select o).ToList();
This way, you are keeping a list of items instead of an expression tree in member and the "dirty" state won't occur.
The second place that may cause trouble is
foreach (var w in wom) { if (w.new_IsPrimary == true) { // Make any primary records, non-primary //********************************************************************************** w.new_IsPrimary = false; crm.UpdateObject(w); crm.SaveChanges(); //performance problem? //********************************************************************************** } } // foreach
The ServiceContext.SaveChanges() method will cause context send all the changes to web service. So, if we had 1000 items in wom, we will do this round trip 1000 times. I believe move crm.SaveChanges() out of foreach loop can save a lot of round trip and may even solve your error of inconsistent state,(The last updated time only updates after all the items are be modified)
I am on a mac right now and have no way to test my suggestions. Please let me know if they are valid.
Cheers,
Wei
Saturday, June 23, 2012 6:30 AM -
Hi Kahan,
I think this is probably caused by your own plugin preventing itself from setting isprimary to false. You can test this theory by removing the throwing of an exception when isprimary is set to false.
To work around, you could check the execution context depth and only throw the exception on setting to false if it is at the top level.
Hth,
Scott
Scott Durow
Read my blog: www.develop1.net/public
If this post answers your question, please click "Mark As Answer" on the post and "Mark as Helpful"- Proposed as answer by Scott Durow (MVP)MVP, Editor Saturday, June 23, 2012 10:32 AM
- Marked as answer by Kahuna2000 Saturday, June 23, 2012 2:16 PM
Saturday, June 23, 2012 6:51 AMAnswerer -
You can try implement suggestions given by experts.
You can also debug your online plugin following the steps mentioned in the link:
http://guruprasadcrm.blogspot.ca/2011/11/how-to-debug-crm-2011-online-plugin.html
Hope it will help.
Regards Karan Mittal
Saturday, June 23, 2012 9:54 AM -
Wei,
Thanks, I tried adding the ".ToList()" to the LINQ but it did not help this issue. Same error occurs. You would think that the placement of SaveChanges() might cause a performance issue, but in fact once this is all working there is guarnteed to be on one record it updates so the placement is irrelevent.
Saturday, June 23, 2012 12:17 PM -
Scott,
I think you may be onto something with the theory of the plug-in calling itself. I disabled the throwing the exception and no longer get the SaveChanges error. Unfortunately I now get the following exception:
Unexpected exception from plug-in (Execute): WorkoutPrimaryControl.WomUpdate: System.InvalidOperationException: The context is not currently tracking the 'opportunity'
Perhaps it is related. I read somewhere (one of Pogo's blogs?) that you can use an "Attach" mehtod to avoid this. I don't know too much about it and wopuld welcom any guidence on if it is appropriate here and how to implement it.
How do I "check the execution context depth ?"
Thanks for all you help on this. This is my first plug-in and I didn't think it would be quite this much "fun"
Saturday, June 23, 2012 12:30 PM -
Scott,
I found the context Depth property and looked it up. Unfortunately, it does not say what the base value is when the plugin is first called (0 or 1). Perhaps you know so I can implement:
if(context.Depth > 1) {return;}
or
if(context.Depth > 0) {return;}
as protection from the recursive call.
Saturday, June 23, 2012 12:42 PM -
Hi Kahuna,
I would just use service.Update(w).
The depth starts at 2 I think because of the platform framework. You might need to do some trial and error!
The other option is to use the SharedVariable to set a flag to say you are updating.
Scott
Scott Durow
Read my blog: www.develop1.net/public
If this post answers your question, please click "Mark As Answer" on the post and "Mark as Helpful"Saturday, June 23, 2012 1:58 PMAnswerer -
Scott,
Seems to work very well now. I used "if(context.Depth > 1) {return;}".
I have one more piece of this to write for the Create events. For the most part it will mirror the Update except for one situation:
If a new opportunity is created and it is the one and only opportunity for the CustomerID to which it belongs; and if the users fails to mark it as Primary, I want to force it to primary for them.
Can I do this by just setting the new_isprimary value in the Message entity to TRUE and letting the plug-in continue to process? Or do I have to take some further action?
Thanks for all your help. You have been right on in all your analysis and suggestions.
Saturday, June 23, 2012 2:26 PM -
Hi Kuhana,
Yes, you can just set the value on the target entity and it will be written to the database. This only works for pre stages, which you are already using.
Glad you've got it sorted!
Cheers,
Scott
Scott Durow
Read my blog: www.develop1.net/public
If this post answers your question, please click "Mark As Answer" on the post and "Mark as Helpful"- Marked as answer by Kahuna2000 Saturday, June 23, 2012 3:29 PM
Saturday, June 23, 2012 3:10 PMAnswerer -
Thanks Scott, I think I can get it home from here thanks again for your excellent help.Saturday, June 23, 2012 3:31 PM