Category: Plugin
I get by with a little help from my [base class]
Developing plugins for Microsoft Dynamics 365 (CRM) only using bare SDK libraries make you do the same stuff over and over.
This is why one of the first things I did when starting to work with the platform was to create a helping hand in the form of a plugin base class, implementing IPlugin.
Continue reading “I get by with a little help from my [base class]” CRM Saturday – XrmToolBox with Jonas Rapp
CRM Saturday is a recurring event where Microsoft Dynamics CRM/365 experts and MVPs gather for a day filled with sessions from both strategic and technical perspectives on everything from user adoption to plugin unit testing to IoT and intelligent analysis.
On Saturday January 28 the event was held in London, UK at the Microsoft offices in Paddington. As a “senior contributor” to the world famous XrmToolBox by MVP Tanguy Touzard I was invited to do a session on simplifying development using XrmToolBox. My session covered a brief XrmToolBox background, examples of my own favorite tools, and deep dive demos of FetchXML Builder and Plugin Trace Viewer. Of course you cannot do a demo with some customizations and plugins without using a few other XrmToolBox tools, so I did not only cover my own block busters… The presentation from the event is now available here: CRM Saturday – XrmToolBox with Jonas Rapp This contains the full presentation, and also step by step details on the demos performed, as well as some bonus demos that did not fit the tight session schedule. Note that the presentation also contains reference to a free to use GitHub repository with a simple plugin base class, that can be inherited instead of simply implementing the SDK interface IPlugin to greatly simplify plugin development and logging to the Tracing Service. The repository is available here: https://github.com/rappen/JonasPluginBase
If you would like to dig even deeper into the tracing service, XrmToolBox and the Plugin Trace Viewer – join me on my session on this topic during eXtreme365 for Partners in Lisbon, Portugal that takes place March 13-15 2017 !
If you have any questions regarding the presentation, demo or the plugin base class, don’t hesitate to contact me! More information on CRM Saturday: http://crmsaturday.com
More information on eXtreme365: http://extremecrm.com
Show hierarchically related activities in subgrid
In this article I will demonstrate how to implement a plugin to extend the possibilities for showing activities that are related through account hierarchy in a subgrid on a Microsoft Dynamics CRM form.
In my previous article I showed how to create a simple plugin to show all directly related activities in a subgrid, and not just activities related through the Regarding field.
Objective
The goal is to be able to display all activities (yellow boxes) anywhere below any account (blue box) that is opened in CRM. Some of you may recognize this model – it is respectfully borrowed from MVP Jukka Niiranen’s blog post on this problem: CRM 2011 subgrids ain’t what associated views used to be. As this article indicates, this has been a problem ever since we left CRM 4.0 behind. Continue reading “Show hierarchically related activities in subgrid”
Show ALL related activities in a subgrid
Microsoft Dynamics CRM offers great capabilities for activity entities, both out of the box and custom activities.
This post will describe a way to extend those capabilities even more, using a quite simple plugin.
During eXtremeCRM in Warsaw last week, a fellow CRMian and frequent user of FetchXML Builder approached me to ask if I knew a way to create “dynamic” queries with some contextual awareness. He had used unsupported methods to inject FetchXML to subgrids, to be able to show all activities related to current record, not just those where the record is related through the Regarding field. As I will show in this post, this can be accomplished with a simple plugin and a custom view.
Background
Activities handle e-mail, phonecalls, tasks, meetings, and even faxes and letters. Common custom activities are sms, transactions, and any other date-bound entity that involves participants of some kind. Participants can be contacts, accounts, leads, users, and the activities can be regarding eny entity with the “Activities” option selected. When looking at an associated view of a record to display associated activities, this can display activities where the current record is either recipient, sender, participant or regarding the activity. This is done with specialized views built into CRM, as this gives a useful overview of all activities concerning the party. The problem is when you want to have a similar view embedded on the form in a subgrid. In this case, you can only add activities related to the record through the Regarding lookup on the activities. This will of course only show a fraction of all activities that may be related to the contact, account, or any other entity form the subgrid is placed on.
Solution – the customizations
To solve this, we need to modify the query that is executed by CRM. Instead of filtering activities based on the value in the Regarding field, we want to verify that the current record is any kind of “party” to the activity. First, create a custom view for the activities, with a signature that can be identified by our plugin so that it does not trigger when we don’t want it to. Open the “All Activities” view, and select Save As to create your custom view. Add a filter criteria to only show records where Activity does not contain data. Save the view. The view is now pretty unusable. It will never show any records as activity is required for the activitypointer pseudo entity. Next, add a subgrid to the form where you want to show all activities, and select the new view as the default and only view. The customizations are now in place, and when we view any record with this form, nothing will show in the added subgrid.
Solution – the plugin
To get the query we want to be passed to CRM, we will intercept the RetrieveMultiple message in the execution pipeline before it goes to CRM. By analyzing the request we see that the query is formed like this, after conversion to FetchXML:
<fetch distinct='false' no-lock='true' mapping='logical' page='1' count='4' returntotalrecordcount='true' >
<entity name='activitypointer' >
<attribute name='subject' />
...
<filter type='and' >
<condition attribute='isregularactivity' operator='eq' value='1' />
<condition attribute='activityid' operator='null' />
<condition attribute='regardingobjectid' operator='eq' value='633929A2-F2E1-E511-8106-000D3A22EBB4' />
</filter>
<order attribute='scheduledend' descending='false' />
<link-entity name='systemuser' to='owninguser' from='systemuserid' link-type='outer' alias='activitypointerowningusersystemusersystemuserid' >
<attribute name='internalemailaddress' />
</link-entity>
</entity>
</fetch>
A bunch of included attributes have been excluded for readability
So, let’s create a plugin that triggers Pre RetrieveMultiple of activitypointer, and investigate if it has our “signature”.
public void Execute(IServiceProvider serviceProvider)
{
ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
if (context.MessageName != "RetrieveMultiple" || context.Stage != 20 || context.Mode != 0 || !context.InputParameters.Contains("Query") || !(context.InputParameters["Query"] is QueryExpression))
{
tracer.Trace("Not expected context");
return;
}
if (ReplaceRegardingCondition(query, tracer))
{
context.InputParameters["Query"] = query;
}
}
This code is pretty self-explaining. The interesting part however, comes in the ReplaceRegardingCondition method…
private static bool ReplaceRegardingCondition(QueryExpression query, ITracingService tracer)
{
if (query.EntityName != "activitypointer" || query.Criteria == null || query.Criteria.Conditions == null || query.Criteria.Conditions.Count < 2)
{
tracer.Trace("Not expected query");
return false;
}
ConditionExpression nullCondition = null;
ConditionExpression regardingCondition = null;
tracer.Trace("Checking criteria for expected conditions");
foreach (ConditionExpression cond in query.Criteria.Conditions)
{
if (cond.AttributeName == "activityid" && cond.Operator == ConditionOperator.Null)
{
tracer.Trace("Found triggering null condition");
nullCondition = cond;
}
else if (cond.AttributeName == "regardingobjectid" && cond.Operator == ConditionOperator.Equal && cond.Values.Count == 1 && cond.Values[0] is Guid)
{
tracer.Trace("Found condition for regardingobjectid");
regardingCondition = cond;
}
else
{
tracer.Trace($"Disregarding condition for {cond.AttributeName}");
}
}
if (nullCondition == null || regardingCondition == null)
{
tracer.Trace("Missing expected null condition or regardingobjectid condition");
return false;
}
var regardingId = (Guid)regardingCondition.Values[0];
tracer.Trace($"Found regarding id: {regardingId}");
tracer.Trace("Removing triggering conditions");
query.Criteria.Conditions.Remove(nullCondition);
query.Criteria.Conditions.Remove(regardingCondition);
tracer.Trace("Adding link-entity and condition for activity party");
var leActivityparty = query.AddLink("activityparty", "activityid", "activityid");
leActivityparty.LinkCriteria.AddCondition("partyid", ConditionOperator.Equal, regardingId);
return true;
}
A bit of explanation:
Line 3-7: First sanity check of the query to verify that we might find our signature.
Line 13-29: Looping through the conditions to find our signature.
Line 15-19: Identifies the null-condition that was introduced in the view.
Line 20-24: Identifies the relation for the view, used to extract the record id.
Line 30-34: Verification of our signature.
Line 35: Extract the id of the parent record which is being displayed.
Line 39: Remove the null-condition used only to identify our signature.
Line 40: Remove the condition that the activities must have current record as Regarding.
Line 43: Create link-entity to activityparty, where the current record should be found.
Line 44: Add condition that the associated party shall be the current record.
As can be seen in the Execute method above, if the query is updated by our code, the updated query will be put back into the plugin execution context, and thus the updated query is what will be executed by CRM.
Finally, the plugin shall be registered in CRM.
Result
The subgrid on the form now contains all activities that are related to the current contact, in this case.
If you activate Plug-in Trace Log for all executions, you can now see logs with something like this in the message block:
Checking criteria for expected conditions Disregarding condition for isregularactivity Found triggering null condition Found condition for regardingobjectid Found regarding id: 633929a2-f2e1-e511-8106-000d3a22ebb4 Removing triggering conditions Adding link-entity and condition for activity party
Resources
The complete source code for this plugin, including a bit more extra debugging information than shown here, can be downloaded here.
A CRM solution with the customized activity view, modified contact form, the plugin assembly and a plugin step can be downloaded here as managed version and unmanaged version.
CRM 2011: Multiple Cascade Delete – part 2
In my previous post CRM 2011: Multiple Cascade Delete – part 1 I discussed the limitations in relationship behavior configuration for manual intersect entities, and proposed a solution with a plugin and a configuration entity.
In this post I will go into the details of the plugin and how it is registered.
I will not go into the basics of writing a plugin, there are tons of examples out there.
You need to be somewhat familiar with the event execution pipeline in Microsoft Dynamics CRM 2011 and the SDK classes used when developing plugins using late binding.
Objective
Consider the following entity model:
Role records should be deleted if either the associated contact or account is deleted, but only one of the relations can be configured with cascade delete. Responsibility records should be deleted if associated Role is deleted. The goal is to accomplish this through a generic plugin that is configured using a dedicated CRM entity.
Plugin overview
The plugin will contain three main blocks
- Configuration cache being loaded on demand and cleared whenever a Cascade Delete rule is being changed.
- Pre Validation step to retrieve all child records that should be deleted prior to the parent record being deleted.
- Pre Operation step to perform the deletion of retrieved child records.
The reason for separating block 2 and 3 is described in more detail in the previous post.
Continue reading “CRM 2011: Multiple Cascade Delete – part 2”CRM 2011: Multiple Cascade Delete – part 1
As I have recently mentioned, the possibilities of defining cascade deletes in Microsoft Dynamics CRM 2011 are quite limited. Only one parent entity can have the relationship behavior set to Cascade Delete. When you create a manual intersect entity to connect two or more other entities, this constraint is simply not acceptable for the end users.
Scenario
Consider this classic scenario: Instead of just associating contacts with a parent account, you want to be able to define a more dynamic model.This could be accomplished using Connections and Connection Roles, but that too has a number of pros and cons, which I will not go into in this article. When creating the relations to the Role entity, only one of them (i.e. either the relation to Account, Contact or Function) can be defined with cascade delete. What you would like here is to specify Cascade for both Account and Contact, and Remove Link for Function.
When using a manual intersect entity as in this example, the Role object will loose all meaning if either the associated Contact or the associated Account is deleted, thus the Role should of course be deleted in both cases.
To solve this, I will create a plugin which can be configured to perform the cascade behavior where it is not possible to do it by customizations only.
Relationship Behavior
First a few notes about the different types of relationship behavior during delete. The Restrict behavior verifies if there are any existing associating records before stage 20 (Pre Operation). So this behavior cannot be used, as we want to perform our configured plugin delete within the triggering transaction to ensure proper rollback behavior. The Cascade behavior can only be defined for one relationship, which in this case will be to the Contact entity. The Remove Link behavior will leave the child records in CRM, which is possible as the relationship attribute will be nulled by CRM between stage 10 and 20. Using this behavior alone would leave Roles defining e.g. that “Jonas has function Consultant at company null” when deleting accounts.
Objective and Configuration
A plugin shall delete children of a parent record that is being deleted. To specify which relationships that shall invoke this function, I use a configuration entity in CRM. It is also possible to pass the configuration as parameters to the plugin constructor, but then you have to enter the configuration in the step registrations, which is not very user friendly to the sysadmin. The operation shall be performed in stage 20 (Pre Operation) as it will then be within the transaction of the triggering delete, and the children will be deleted before the parent record is actually removed from the database. As the lookup attributes are nulled before stage 20 of the event execution pipeline, the plugin will retrieve a list of the children to delete in stage 10. This list is passed to the plugin triggered in stage 20 within the context’s SharedVariables. To improve performance, a cache of Cascade Delete configurations is maintained in the plugin class. If a configuration record is created, updated or deleted, the cache will be cleared.
In the next post I demonstrate and explain the code in the plugin, and also provide a complete solution for deploying multiple cascade delete in your Microsoft Dynamics CRM. Stay tuned!
CRM Plugins: Retrieve children during Delete
Tip of the day!
If you want to write a plugin that needs to read children of a record being deleted – this must be done in the Pre Validation stage.
Why is that?
For 1:N relations with Delete Behavior: Remove Link, the lookup to the parent being deleted is set to null somewhere between stage 10 (Pre Validation) and 20 (Pre Operation), but inside the transaction of the primary record deletion. So if trying to retrieve the children in any stage after Pre Validation you will not get any results, as they all have a not-yet-committed update transaction where the relation is nulled.
Why on earth should I care?
You might agree with me that the constraints regarding cascade behavior on relationships do not quite fulfill the needs that are quite common when creating manual N:N relations. I will publish some tricks to generically cascade delete from several parents to a manual intersect entity in an article soon to come. Stay tuned! Unfamiliar with native / manual N:N relations? See Richard Knudson’s excellent article on this topic.
Execute Server-Side Code from Javascript
Background:
General business rules shall be implemented in server-side code to ensure its execution regardless of the origin of data changes; CRM UI, workflow, integration or any other application consuming the CRM web services. But on some occasions it would be handy to be able to execute this code from JavaScript to improve the user’s experience of the application.
Scenario:
When creating or updating an Account, there is a plugin registered Pre Validation Create/Update to verify that the given VAT number is in a correct format. If not, the plugin throws an exception instructing the user to enter a correct VAT number.
Another plugin is registered Pre Operation Create/Update to look up city/state/county from given zip-code to ensure correct data for the account. This function consumes an external service from a third party accessible as a web service.
Challenge
To improve user experience, the customer requires immediate verification of the VAT no and lookup of geographic information for the zip-code.
Solution 1 (bad):
Required functionality can of course be implemented entirely in JavaScript. Rules for VAT numbers and calls to external web services can be coded in client side code. Calling external services may be a problem, depending on firewall configuration, local browser settings etc. but usually it is possible to find a way around these problems. Composing and parsing SOAP-messages in javascript is neither intuitive nor fun, but of course it can be done. This solution however would duplicate the same code in two entirely different environments and languages. Duplicated code is, and I think everyone agrees to this, NOT something we want. Right?! Especially not from a maintenance perspective.
Solution 2 (good:)
Create a custom entity jr_serverfunction with one text field jr_parameter and one text field jr_result.
Server-side
- Extract logic from the two plugins mentioned to methods in an isolated C# class
- Rewrite the plugins to consume this class (to preserve existing functionality invoked on create/update)
- Create a plugin triggering Post Operation RetrieveMultiple on jr_serverfunction. This plugin shall investigate the incoming query to extract it’s condition for field jr_parameter and use this condition to execute desired server-side code
- When the result of the function is determined, an instance of entity jr_serverfunction is created (in code, not saved to the database), resulting data/information placed in field jr_result, and the entity placed in the EntityCollection that is to be returned in the query response
Note that the custom entity will actually never hold any records in the CRM database. This is why I also trigger the Create message and immediately throw an error. Plugin code:
public class ServerSideExecution : IPlugin { public void Execute(IServiceProvider serviceProvider) { IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); if (context.MessageName == "Create") throw new InvalidPluginExecutionException("Server Function entity cannot be instantiated"); if (context.MessageName == "RetrieveMultiple" && context.Stage == 40 && // Post Operation context.PrimaryEntityName == "jr_serverfunction" && context.InputParameters.Contains("Query") && context.InputParameters["Query"] is QueryExpression) { ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); tracer.Trace("Initialize service etc"); IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId); QueryExpression query = (QueryExpression)context.InputParameters["Query"]; tracer.Trace("Extract condition from query"); ConditionExpression parameterCondition = MyFunctions.GetFilterConditionByAttribute(query.Criteria, "jr_parameter"); if (parameterCondition != null && parameterCondition.Values.Count == 1) { string parameter = parameterCondition.Values[0].ToString().Trim('%'); tracer.Trace("Parameter is: {0}", parameter); string command = parameter.Split(';')[0]; string result = null; switch (command) { case "VerifyVAT": tracer.Trace("Check if VAT number is correct"); string vat = parameter.Split(';')[1]; if (MyFunctions.VerifyVAT(vat)) result = "ok"; else result = "not ok"; break; case "LookupZIP": tracer.Trace("Lookup city etc from ZIP code"); string zip = parameter.Split(';')[1]; // Returns a semi-colon separated string with city;state;country result = MyFunctions.GetZipInfo(zip); break; // ************************************** // ** Add more functions here as needed // ************************************** } if (result != null) { tracer.Trace("Create resulting serverfunction entity with result: {0}", result); Entity serverfunction = new Entity("jr_serverfunction"); Guid id = new Guid(); serverfunction.Id = id; serverfunction.Attributes.Add("jr_serverfunctionid", id); serverfunction.Attributes.Add("jr_parameter", parameter); serverfunction.Attributes.Add("jr_result", result); tracer.Trace("Replace contents of resulting entitycollection"); EntityCollection resultCollection = (EntityCollection)context.OutputParameters["BusinessEntityCollection"]; resultCollection.Entities.Clear(); resultCollection.Entities.Add(serverfunction); context.OutputParameters["BusinessEntityCollection"] = resultCollection; } } } } }
Note: The function MyFunctions.GetFilterConditionByAttribute is part of internally developed tools. Please contact me if you are interested in how we find specific conditions in a query.
The plugin can easily be tested by making an Advanced Find query on the Server Function entity in the CRM.
Client-side
In the client-side JavaScript a method is registered for the onChange event of the VAT no field. The function will use the REST endpoint to query CRM for records of jr_serverfunction where jr_parameter equals “VerifyVAT;1234567890” (where the numbers should be the number entered on the form).
The result will contain one record, and the jr_result field will contain “ok” or “not ok”, which the JavaScript can use to immediately instruct the user to correct the number.
Cinteros.Xrm.AccountServerFunction = { jr_vat_onChange: function () { try { var vatNo = Xrm.Page.getAttribute("jr_vat").getValue(); if (vatNo != null) { var parameter = "VerifyVAT;" + vatNo; var result = this.ExecuteServerFunction(parameter); if (result === "not ok") { window.alert("VAT number is not in a correct format"); } } } catch (e) { alert("Error in jr_vat_onChange:nn" + e.description); } }, ExecuteServerFunction: function (parameter) { var result = null; var serverFunctionResult = Cinteros.Xrm.REST.RetrieveMultiple("jr_serverfunction", "?$select=jr_result" + "&$filter=jr_parameter eq '" + parameter + "'"); if (serverFunctionResult && serverFunctionResult.length === 1) { result = serverFunctionResult[0].jr_result; } return result; } }
Note: The javascript-function Cinteros.Xrm.REST.RetrieveMultiple is part of our internally developed tools, it may well be replaced by similar functionality in the CrmRestKit (http://crmrestkit.codeplex.com/) or by other custom made code.
Registering this function as the onchange event of the VAT number field immediately executes the server-side functionality for validating a VAT number when the user changes the field in the CRM client.
Corresponding onChange event for the zip-code field can be implemented to get geographic information and automatically populate city/state etc. on the form.
address1_postalcode_onChange: function () { try { var zipCode = Xrm.Page.getAttribute("address1_postalcode").getValue(); if (zipCode != null) { var parameter = "LookupZIP;" + zipCode; var result = this.ExecuteServerFunction(parameter); if (result) { var city = result.split(';')[0]; var state = result.split(';')[1]; var country = result.split(';')[2]; Xrm.Page.getAttribute("address1_city").setValue(city); Xrm.Page.getAttribute("address1_stateorprovince").setValue(state); Xrm.Page.getAttribute("address1_country").setValue(country); } } } catch (e) { alert("Error in address1_postalcode_onChange:nn" + e.description); } }
Adding Duplicates in N:N-Relations
The possibility to use many-to-many relations in Microsoft Dynamics CRM 2011 is very handy for various scenarios.
using System; using System.ServiceModel; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Metadata; using Microsoft.Xrm.Sdk.Query; namespace Cinteros.Xrm { public class AssociateAllower : IPlugin { public void Execute(IServiceProvider serviceProvider) { IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); if (context.MessageName == "Associate" && context.Stage == 20 && // Before, inside transaction context.Mode == 0 && // Synchronous context.InputParameters.Contains("Target") && context.InputParameters["Target"] is EntityReference && context.InputParameters.Contains("Relationship") && context.InputParameters["Relationship"] is Relationship && context.InputParameters.Contains("RelatedEntities") && context.InputParameters["RelatedEntities"] is EntityReferenceCollection) { try { ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId); Trace(tracer, "Get the name of the intersect table and related entities from the context"); Relationship relationship = ((Relationship)context.InputParameters["Relationship"]); Trace(tracer, "Intersect: {0}", relationship); EntityReference entity1ref = (EntityReference)context.InputParameters["Target"]; Trace(tracer, "Entity1: {0} {1}", entity1ref.LogicalName, entity1ref.Id); EntityReferenceCollection entity2refs = (EntityReferenceCollection)context.InputParameters["RelatedEntities"]; Trace(tracer, "Entity2 count: {0}", entity2refs.Count); Trace(tracer, "Get metadata for intersect relation: {0}", relationship); RetrieveRelationshipResponse relationshipmetadata = (RetrieveRelationshipResponse)service.Execute(new RetrieveRelationshipRequest() { Name = relationship.SchemaName }); if (relationshipmetadata != null && relationshipmetadata.RelationshipMetadata is ManyToManyRelationshipMetadata) { ManyToManyRelationshipMetadata mmrel = (ManyToManyRelationshipMetadata)relationshipmetadata.RelationshipMetadata; Trace(tracer, "Get intersect attribute names (we cannot know which entity is entity1 and which is entity2)"); string entity1idattribute = mmrel.Entity1LogicalName == entity1ref.LogicalName ? mmrel.Entity1IntersectAttribute : mmrel.Entity2IntersectAttribute; string entity2idattribute = mmrel.Entity1LogicalName == entity1ref.LogicalName ? mmrel.Entity2IntersectAttribute : mmrel.Entity1IntersectAttribute; Trace(tracer, "Entiy1id: {0} Entity2id: {1}", entity1idattribute, entity2idattribute); Trace(tracer, "Verify if any of the new relations already exist"); foreach (EntityReference entity2ref in entity2refs) { QueryByAttribute qba = new QueryByAttribute(relationship.SchemaName); qba.AddAttributeValue(entity1idattribute, entity1ref.Id); qba.AddAttributeValue(entity2idattribute, entity2ref.Id); EntityCollection existingAssociations = service.RetrieveMultiple(qba); Trace(tracer, "Found {0} existing relations", existingAssociations.Entities.Count); if (existingAssociations.Entities.Count > 0) { EntityReferenceCollection deleteRefs = new EntityReferenceCollection(); deleteRefs.Add(entity2ref); Trace(tracer, "Disassociating entities"); service.Execute(new DisassociateRequest() { Target = entity1ref, RelatedEntities = deleteRefs, Relationship = relationship }); Trace(tracer, "Disassociated"); } } } else { throw new InvalidPluginExecutionException("Metadata for relation " + relationship + " is not of type ManyToManyRelationshipMetadata"); } Trace(tracer, "Done!"); } catch (FaultException<OrganizationServiceFault> ex) { throw new InvalidPluginExecutionException( String.Format("An error occurred in plug-in {0}. {1}: {2}", this, ex, ex.Detail.Message)); } } } private void Trace(ITracingService tracer, string format, params object[] args) { tracer.Trace(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " " + format, args); } } }