Friday, October 12, 2012

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:

AccountContact2
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

  1. Configuration cache being loaded on demand and cleared whenever a Cascade Delete rule is being changed.
  2. Pre Validation step to retrieve all child records that should be deleted prior to the parent record being deleted.
  3. 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.

1. Plugin configuration

I use an EntityCollection as a cache for all Cascade Delete rules defined in the configuration entity (see previous post).

public class CascadeDelete : IPlugin
{
    private EntityCollection _cascadeDeleteRules = null;

If the plugin is triggered by a change to any of these rules, it should be cleared to force a reload on next delete message from CRM.

if (context.PrimaryEntityName == "jr_cascade_delete" &&
    (context.MessageName == "Create" ||
     context.MessageName == "Update" || 
     context.MessageName == "Delete"))
{
    tracer.Trace("Cascade Delete rule changed, clearing cache");
    _cascadeDeleteRules = null;
    return;
}

When the plugin is triggered by an entity being deleted, the cache is loaded if it has not already been loaded.

if (_cascadeDeleteRules == null)
{
    QueryByAttribute query = new QueryByAttribute("jr_cascade_delete");
    query.AddAttributeValue("statecode", 0);
    query.ColumnSet = new ColumnSet("jr_parent_entity", "jr_child_entity", "jr_lookup_attribute");
    _cascadeDeleteRules = service.RetrieveMultiple(query);
}

2. Pre Validation – Retrieve children to delete

In this block, all rules in the cache will be examined to determine for each rule if it is applicable to the (parent) entity being deleted.

EntityCollection RecordsToDelete = new EntityCollection();
foreach (Entity rule in _cascadeDeleteRules.Entities)
{
    string parent = (string)rule["jr_parent_entity"];
    string child = (string)rule["jr_child_entity"];
    string attribute = (string)rule["jr_lookup_attribute"];
    tracer.Trace("Relation: {0} {1} {2}", parent, child, attribute);
    if (parent == context.PrimaryEntityName)
    {
        ...
    }
}

The collection RecordsToDelete is used to contain all child records, regardless of child entity type, that shall be deleted according to all applicable rules.

This list is populated from the child entities defined in the rules in this way.

QueryByAttribute qba = new QueryByAttribute(child);
qba.AddAttributeValue(attribute, context.PrimaryEntityId);
EntityCollection relatingEntities = service.RetrieveMultiple(qba);
tracer.Trace("Found {0} {1} to delete", relatingEntities.Entities.Count, child);
RecordsToDelete.Entities.AddRange(relatingEntities.Entities);

After iterating through all rules, the collection of child records to delete is added to the SharedVariables collection of the context.

if (RecordsToDelete.Entities.Count > 0)
{
    tracer.Trace("Adding total of {0} records to delete to SharedVariables", RecordsToDelete.Entities.Count);
    context.SharedVariables.Add(sharedVarName, RecordsToDelete);
}

The sharedVarName variable is defined based on entityname and id of the record being deleted.

3. Pre Operation – Delete children

This block is quite straightforward. If there is a collection of entities in the SharedVariables; delete them one by one.

if (context.ParentContext != null && context.ParentContext.SharedVariables != null && context.ParentContext.SharedVariables.Contains(sharedVarName))
{
    EntityCollection RecordsToDelete = (EntityCollection)context.ParentContext.SharedVariables[sharedVarName];
    tracer.Trace("Found {0} records to delete", RecordsToDelete.Entities.Count);
    foreach (Entity relatingEntity in RecordsToDelete.Entities)
    {
        tracer.Trace("Deleting {0} {1}", relatingEntity.LogicalName, relatingEntity.Id);
        try
        {
            service.Delete(childEntity.LogicalName, childEntity.Id);
        }
        catch (Exception ex)
        {
            tracer.Trace("Delete failed: {0}", ex.Message);
        }
    }
    context.ParentContext.SharedVariables.Remove(sharedVarName);
}

As described in the SharedVariables documentation, objects placed there in stage 10 must be accessed from the ParentContext in stage 20.

Note that these cascaded deletes will also trigger the Cascade Delete plugin and possibly a new chain of deletes, depending on how the Cascade Delete rules have been defined. Deletes being executed as a result of relationships defined with Parental behavior will also trigger the plugin. In the example model above, the parental relation between account and contact will trigger the role records to be deleted from "both directions".

The unconditional catch at the end of the code block above is just a simple safety precaution to handle cases where a child identified to be subject to a cascade delete rule has been deleted by other parental relationship behavior when this code segment is reached.

Download Solution

If you like the functionality of this plugin, but don't really feel like implementing your own version of it – you can download a complete managed solution HERE.

Disclaimer
It is possible to configure a combination of native parental relationship behavior and cascade delete rules in this solution which together may cause a recursive effect that can produce sql deadlocks. As all deletes are executed within the original delete transaction, this problem will not result in any inconsistent data.

13 comments:

  1. Jonas - really useful couple of posts. May I ask a couple of questions:
    1 - presumably you are registering this plugin to fire for
    (a) all entities, so can catch any entity for which you have rules set
    (b) all messages too? (so you can catch the full CRUD set for the jr_cascade_delete entity?)

    2 - are you registering the same plugin at both pipeline stages and using code to work out which invocation is being processed, or is this really two plugins - one for Stage 10 and one for Stage 20?

    ReplyDelete
    Replies
    1. Hi Julian!

      1a - Yes.
      1b - Not really. It is registered on delete for all entities, plus Create and Update for jr_cascade_delete.

      2 - Yes. So in total 4 steps are registered:
      10-Delete-any entity
      20-Delete-any entity
      40-Create-jr_cascade_delete
      40-Update-jr_cascade_delete

      If you import the solution, you can se all registered steps in it or by examining the assembly steps with pluginregistration.

      Delete
  2. Hi Jonas, I'm looking for this solution, but the link it's broken. Is it posible to get new a link?

    Thanks

    ReplyDelete
    Replies
    1. Hi Flor
      I have now updated the link, please refresh the article and try again! :)
      /Jonas

      Delete
    2. Thanks Jonas
      Now I, have a new problem, CRM said that I only can import solutionts with a package version of 8.1 or earlier when i tried to import. Do you now how to solve this? I have crm 2016 SP 1.0

      Delete
    3. Well... this is a really old package for CRM 2011...
      I guess I could try to import unmanaged into 2013, export, import to 2016, export...
      Or you could copy/paste the code in the articles, it should cover most of what is needed :p

      Delete
    4. Jonas, I have a server with crm 2013, so if you can send me the unmanaged version i could do it.

      Sorry for the inconvenience , but I need to fix this as soon as possible and I don't have installed VS in the machine that I have today

      Delete
    5. Flor, send me an email: jonas dot rapp at innofactor dot com

      Delete
  3. Replies
    1. But does not work. The cascading delete link kicks in and throws errors

      Delete
    2. Well Jonas, it does work in the sence that the child records are deleted. But, in the gui, CRM will throw errors, which I believe are due to the fact that CRM tries to delete the links in the records that have already been deleted. That's my guess anyway. So, I have then opted to delete the "linkless/semi-orphan" records using an asynchronous plugin which is triggered when the "parent" is deleted.
      Cheers

      Delete
    3. Thanks Steffen - this is not a behavior I recognize. It might be due to newer version of CRM (I wrote the article for CRM 2011 I think...) or some other circumstance I did not test for.
      Glad you made it work anyway!

      Delete