Plugin/workflow attribute filtering and updating records via web service
Recently we ran into an issue where a workflow configured to run upon changing a particular field was being triggered even though that field was not being updated. In the end it turned out that this was because of the way we were updating the record via the CRM web service. The lesson learnt was quite interesting and I’d like to share it in this post.
Attribute filtering
As you may be aware, when registering an Update plugin, we can configure the attributes that should trigger the plugin. The meaning of this configuration however is often misunderstood.
Take the screenshot below for example.
The above configuration does not mean that the plugin will be executed only when the Account Number field is changed. Rather, it means that the plugin will be executed only when the Account Number field is present in the incoming update request. If the Account Number field is present in the incoming update request, the plugin will execute, even if its incoming value is the same as the current value in the database.
So what does ‘present in the incoming update request‘ mean?
When you debug an Update plugin, retrieve the Target entity from theInputParameters collection and have a look at its Attributes collection. An attribute is present in the incoming update request if it exists in this collection.
In the screenshot below, you can see that my incoming update request contains name,telephone1, fax, websiteurl, and a few basic fields that are always passed as part of the request.
Wait… what does this have to do with workflow?!
Well, workflow has a similar configuration: it can be started automatically after certain fields have changed.
Our testing has shown that this configuration actually works the same way as plugin’s attribute filtering: the workflow will be triggered if one of the specified fields is present in the incoming update request.
OK… how is this related to updating records via web service?
When you update the record via the CRM’s UI, it is smart enough to only send through the fields that are dirty (i.e. where the value has changed), which means your plugin’s attribute filtering and workflow automatic trigger will function as one would normally expect.
When you update the record by calling the CRM’s web service however, it is up to you to ensure that only dirty fields are being included in the update request.
Consider the code to retrieve and update an entity below:
1
2
3
4
5
6
7
8
9
| var connection = CrmConnection.Parse(ConfigurationManager.ConnectionStrings[ "CRM" ].ConnectionString); using ( var service = new OrganizationService(connection)) { var entity = service.Retrieve( "account" , Guid.Parse( "F0A3C092-C076-E411-8111-B86B23AC9F7C" ), new ColumnSet( true )); entity.Attributes[ "telephone1" ] = "1234" ; service.Update(entity); } |
Even though I only updated the telephone1 field after retrieving the entity, this code will cause all fields to be included in the update request, and essentially defeat the purpose of any plugin attribute filtering or workflow automatic trigger you may have configured.
Below is the record’s audit history after the above update. Notice how it lists other fields as ‘Changed Field’ even though the old and new values are the same?
So how do we fix it?!
The problem with the above code lies in the retrieval of the record (prior to setting the field), specifically the fact that I specified to retrieve all columns using new ColumnSet(true). This causes all fields to be included in the update request, regardless of whether they were touched after the retrieval.
If the code is changed to be:
1
2
3
4
5
| var entity = service.Retrieve( "account" , Guid.Parse( "F0A3C092-C076-E411-8111-B86B23AC9F7C" ), new ColumnSet()); entity.Attributes[ "telephone1" ] = "1234" ; service.Update(entity); |
then we will find that only the telephone1 field (and a few basic fields that are always passed as part of the update request) are included in the request as expected.
Note that the above code actually specifies to not retrieve any field. This is OK as I am only updating and not reading any field.
But.. what if I need to read and update fields on the same entity?!
Let say you need to set an account’s Fax to be the same as its Telephone. Your code would look something like this:
1
2
3
4
5
6
| var entity = service.Retrieve( "account" , Guid.Parse( "F0A3C092-C076-E411-8111-B86B23AC9F7C" ), new ColumnSet( "telephone1" )); var telephone = entity.Attributes[ "telephone1" ]; entity.Attributes[ "fax" ] = telephone; service.Update(entity); |
Because you included the telephone1 field in the retrieval, and updated the fax field, both of these will be included in the update request. If you have any plugin or workflow that are triggered on changing of the Telephone field, they will now be executed even though you only meant to update the Fax field.
To address this issue, you would need to remove the telephone1 field from the entity after reading its value, like below:
1
2
3
4
5
6
7
8
9
| var entity = service.Retrieve( "account" , Guid.Parse( "F0A3C092-C076-E411-8111-B86B23AC9F7C" ), new ColumnSet( "telephone1" )); var telephone = entity.Attributes[ "telephone1" ]; entity.Attributes[ "fax" ] = telephone; //Remove this field to ensure that it is not included in the update request entity.Attributes.Remove( "telephone1" ); service.Update(entity); |
Alright… but how about early-binding?!
When using the CRM web service directly, we can control which fields are retrieved. I haven’t come across a way of doing this with early-binding.
The code to update an entity using early-binding typically looks like the below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| var connection = CrmConnection.Parse(ConfigurationManager.ConnectionStrings[ "CRM" ].ConnectionString); using ( var service = new OrganizationService(connection)) { using ( var context = new CRMServiceContext(service)) { var accountId = Guid.Parse( "F0A3C092-C076-E411-8111-B86B23AC9F7C" ); var account = context.AccountSet.First(a => a.Id == accountId); account.Telephone1 = "(02) 6373 8273" ; context.UpdateObject(account); context.SaveChanges(); } } |
The above code will cause all fields to be included in the update request.
There are two options for addressing this in early-binding:
- Remove the fields that we are not updating, or
- Create a new blank entity and use this to perform the update
Remove the fields that we are not updating
One thing you should know is that the early-binding classes inherit from the Entity class, and therefore still have the Attributes collection. All field values are stored in this collection. This means we could loop through and remove the fields that should not be included in the update request from this collection.
1
2
3
4
5
6
7
8
9
10
11
| var accountId = Guid.Parse( "F0A3C092-C076-E411-8111-B86B23AC9F7C" ); var account = context.AccountSet.First(a => a.Id == accountId); account.Telephone1 = "(02) 6373 0000" ; //Remove all fields, except for the one we just updated, to ensure that they are not included in the update request. var attributesToRemove = account.Attributes.Where(a => a.Key != "telephone1" ); attributesToRemove.ToList().ForEach(a => account.Attributes.Remove(a.Key)); context.UpdateObject(account); context.SaveChanges(); |
Create a new blank entity and use this to perform the update
The above option works fine, but can get a bit unwieldy when you need to update many fields. The alternative is to create a new blank entity and perform the update on this entity instead. This works because when you create a new blank entity, the Attributes collection is empty. Fields are added to it as you set the properties of the entity. Be sure to set the ID of the new blank entity though, and you will also need to do some Attach/Detach with the context.
Let say again we need to set the Fax of an account to its Telephone. Below is how you would do it with this approach.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| var accountId = Guid.Parse( "F0A3C092-C076-E411-8111-B86B23AC9F7C" ); var retrievedAccount = context.AccountSet.First(a => a.Id == accountId); var accountForUpdate = new Account() { Id = retrievedAccount.Id }; accountForUpdate.Fax = retrievedAccount.Telephone1; //Detach/Attach the object from the context to swap the //one we retrieved with the one we are going to update context.Detach(retrievedAccount); context.Attach(accountForUpdate); context.UpdateObject(accountForUpdate); context.SaveChanges(); |
One other thing…
Did you notice that several fields are always included in an update request? These are the ID field (e.g. accountid), modifiedon, modifiedby, modifiedonbehalfby. This means you should always exclude these fields in your plugin attribute filtering.
Conclusion
Misunderstanding plugin/workflow attribute filtering may see your system executing processes when it should not be. Take care when updating records via the CRM web service, as your code plays a vital part in ensuring your plugin/workflow attribute filtering configuration works as intended.