Plugins – Which message and which pipeline

A CRM MVP, we welcome our guest blogger David Jennaway who is the technical director at Excitation .

In Microsoft Dynamics CRM 4.0, plugins offer a very powerful mechanism to attach code to a wide range of platform events. Each event is identified by the message that was used to cause the event. However, the very range of messages available can cause confusion, as it is not always obvious which message to use for a given operation.

In this article I’ll describe a way to identify the messages and associated data for any platform. This uses 2 components; the main one comprises extensions to the Plugin Developer tool that provide a simple way to attach a plugin to all message permutations, or a designated set of messages. The other component is a sample plugin to log message details to a SQL database. Both components are implemented as .Net assemblies, and I’ve posted the source code to each on the MSDN Code Gallery. See the links later in the article

Plugin Message and Entities

The Microsoft Dynamics CRM 4.0 SDK contains a list of all message and entity combinations that are permitted on the parent pipeline. You could use this information to register for each combination, but there is an easier way.

All the supported message and entity combinations are stored within the MSCRM database, in the sdkmessagefilter entity. This contains the following attributes:

  • sdkmessageid. This is a lookup to the sdkmessage entity which contains an entry for each message
  • primaryobjecttypecode. An EntityNameReference for the primary entity
  • secondaryobjecttypecode. An EntityNameReference for the secondary entity
  • availability. An enumeration that identifies if this combination is available on the client, server or both
  • iscutomprocessingstepallowed. A boolean field that specifies whether you can register a custom plugin for this combination

The way I use this information is to extend the PluginDeveloper tool that is included in the Microsoft Dynamics CRM 4.0 SDK. Within the register.xml file I’ve added support for a BulkStep element; this allows registration of a plugin against all message combinations, with optional filtering. The following example shows how to register for all messages on the post event of the parent pipeline.

<Steps>

<BulkStep

CustomConfiguration = “&lt;Config ConnectionString=&quot;Data Source=CRM;Initial Catalog=PlugInLogger;Integrated Security=SSPI&quot; ThrowException=&quot;False&quot; /&gt;”

Description = “Test Post Event”

FilteringAttributes = “”

ImpersonatingUserId = “”

InvocationSource = “0”

Mode = “0”

PluginTypeFriendlyName = “PlugInLogger”

PluginTypeName = “PlugInMessageLogger.PlugInLogger”

Stage = “50”

>

</BulkStep>

</Steps>

The core of the code is as follows. It uses a query against the sdkmessagefilter entity to find the appropriate messages.

   1: class MessageFilter
   2:  {
   3:   CrmService _svc;
   4:   Dictionary<string, Guid> _msgByName;
   5:   Dictionary<Guid, string> _msgById;
   6:
   7:   public MessageFilter(CrmService Service)
   8:   {
   9:    _svc = Service;
  10:    _msgByName = new Dictionary<string, Guid>();
  11:    _msgById = new Dictionary<Guid, string>();
  12:    QueryExpression qe = new QueryExpression();
  13:    qe.EntityName = EntityName.sdkmessage.ToString();
  14:    qe.ColumnSet = new AllColumns();
  15:    BusinessEntityCollection bec = _svc.RetrieveMultiple(qe);
  16:    foreach (sdkmessage msg in bec.BusinessEntities)
  17:    {
  18:     _msgByName.Add(msg.name, msg.sdkmessageid.Value);
  19:     _msgById.Add(msg.sdkmessageid.Value, msg.name);
  20:    }
  21:   }
  22:   public FilterDetails[] GetAllFiltersForChildPipeline(FilterDetails FilterTemplate)
  23:   {
  24:    FilterDetails[] allFilters = GetAllFilters(FilterTemplate);
  25:    Queue<FilterDetails> q = new Queue<FilterDetails>();
  26:    foreach (FilterDetails fd in allFilters)
  27:    {
  28:     if (fd.MessageName == "Create" || fd.MessageName == "Delete" || fd.MessageName == "Update" || fd.MessageName == "RetrieveExchangeRate")   // Hard-coded restriction on child pipeline messages
  29:      q.Enqueue(fd);
  30:    }
  31:    FilterDetails[] ret = new FilterDetails[q.Count];
  32:    q.CopyTo(ret, 0);
  33:    return ret;
  34:   }
  35:   public FilterDetails[] GetAllFilters(FilterDetails FilterTemplate)
  36:   {
  37:    FilterDetails[] ret = new FilterDetails[0];
  38:    QueryExpression qe = new QueryExpression();
  39:    qe.EntityName = EntityName.sdkmessagefilter.ToString();
  40:    qe.ColumnSet = new AllColumns();
  41:    Queue<ConditionExpression> q = new Queue<ConditionExpression>();
  42:    q.Enqueue(SimpleCondition("iscustomprocessingstepallowed", true));
  43:    if (FilterTemplate.SupportedDeployment != FilterDetails.DefaultInt)
  44:    {
  45:     q.Enqueue(SimpleCondition("availability", FilterTemplate.SupportedDeployment));
  46:    }
  47:    if (!String.IsNullOrEmpty(FilterTemplate.PrimaryEntityName))
  48:    {
  49:     q.Enqueue(SimpleCondition("primaryobjecttypecode", FilterTemplate.PrimaryEntityName));
  50:    }
  51:    if (!String.IsNullOrEmpty(FilterTemplate.SecondaryEntityName))
  52:    {
  53:     q.Enqueue(SimpleCondition("secondaryobjecttypecode", FilterTemplate.SecondaryEntityName));
  54:    }
  55:    if (!String.IsNullOrEmpty(FilterTemplate.MessageName))
  56:    {
  57:     if (_msgByName.ContainsKey(FilterTemplate.MessageName))
  58:     {
  59:      q.Enqueue(SimpleCondition("sdkmessageid", _msgByName[FilterTemplate.MessageName]));
  60:     }
  61:    }
  62:    qe.Criteria = new FilterExpression();
  63:    qe.Criteria.Conditions = new ConditionExpression[q.Count];
  64:    q.CopyTo(qe.Criteria.Conditions, 0);
  65:    LinkEntity le = new LinkEntity();
  66:    le.LinkToAttributeName = "sdkmessageid";
  67:    le.LinkToEntityName = "sdkmessage";
  68:    le.LinkFromAttributeName = "sdkmessageid";
  69:    le.LinkFromEntityName = "sdkmessagefilter";
  70:    le.LinkCriteria = new FilterExpression();
  71:    le.LinkCriteria.Conditions = new ConditionExpression[] { SimpleCondition("isprivate", false) };
  72:    qe.LinkEntities = new LinkEntity[] { le };
  73:    BusinessEntityCollection bec = _svc.RetrieveMultiple(qe);
  74:    ret = new FilterDetails[bec.BusinessEntities.Length];
  75:    for (int i = 0; i < bec.BusinessEntities.Length; i++ )
  76:    {
  77:     sdkmessagefilter filt = (sdkmessagefilter)bec.BusinessEntities[i];
  78:     FilterDetails fd = new FilterDetails();
  79:     fd.MessageName = _msgById[filt.sdkmessageid.Value];
  80:     if (filt.primaryobjecttypecode != null && !filt.primaryobjecttypecode.IsNull)
  81:      fd.PrimaryEntityName = filt.primaryobjecttypecode.Value;
  82:     if (filt.secondaryobjecttypecode != null && !filt.secondaryobjecttypecode.IsNull)
  83:      fd.SecondaryEntityName = filt.secondaryobjecttypecode.Value;
  84:     fd.SupportedDeployment = filt.availability.Value;
  85:     ret[i] = fd;
  86:    }
  87:    return ret;
  88:   }
  89:   private ConditionExpression SimpleCondition(string Name, object Value)
  90:   {
  91:    ConditionExpression ret = new ConditionExpression();
  92:    ret.AttributeName = Name;
  93:    ret.Operator = ConditionOperator.Equal;
  94:    ret.Values = new object[] { Value };
  95:    return ret;
  96:   }
  97:  }
  98:
  99:  class FilterDetails
 100:  {
 101:   public const int DefaultInt = 999;
 102:   public string MessageName;
 103:   public string PrimaryEntityName;
 104:   public string SecondaryEntityName;
 105:   public int SupportedDeployment = DefaultInt;
 106:  }

The Plugin Logger

This plugin is one example of how to capture plugin data. I use a couple of SQL tables to record the plugin event, as this makes for easy analysis via SQL queries. The two SQL tables are as follows:

  • PlugInEvent. This contains one record for every event, and includes the time, the message, stage and primary and secondary entities
  • PlugInParameter. This contains one record for every item in a PropertyBag that is passed to the event – i.e. each InputParameter, OutputParameter, PreEntityImage, PostEntityImage and SharedVariable. Each record contains an XML serialisation of the property data

These tables can be created with the following script:

CREATE TABLE [dbo].[PlugInEvent](

[EventID] [int] IDENTITY(1,1) NOT NULL,

[EventDate] [datetime] NOT NULL,

[Message] [nvarchar](64) NOT NULL,

[PrimaryEntity] [nvarchar](64) NULL,

[SecondaryEntity] [nvarchar](64) NULL,

[Pipeline] [nvarchar](10) NOT NULL,

[Stage] [int] NOT NULL

)

GO

CREATE TABLE [dbo].[PlugInParameter](

[EventID] [int] NOT NULL,

[ParamName] [nvarchar](64) NULL,

[ParamDirection] [nvarchar](10) NULL,

[ParamType] [nvarchar](256) NULL,

[ParamXml] [ntext] NULL

)

GO

The plugin expects a configuration parameter in the constructor. This contains 2 pieces of information; the SQL connection string to the database that contains the above tables, and a flag indicating whether to throw any exceptions during execution. As a plugin can only take one string configuration parameter, I pass this data as an encoded XML document.

In addition to the message registration covered in the previous section, you’ll need the following steps to install and use this plugin:

  • Create a SQL database for the PlugInEvent and PlugInParameter tables
  • Use the SQL script included in the release package in the MSDN Code Gallery to create the PlugInEvent and PlugInParameter tables
  • Assign appropriate SQL permissions on the database and tables. The plugin runs under the Active Directory context of the identity of the CrmAppPool application pool. This account will need Insert permission on both tables. Note that, if the database is on a different server from the CRM platform, then in-built accounts (Network Service, Local System, and Local Service) are identified by the Active Directory machine account
  • Identify the connection string to use for the database. You’ll need this for the message registration
Additional Comments and Usage Notes

One web service call is made to create all the message registrations. This can potentially take several minutes, so it may be necessary to increase the Timeout on the web service proxy from the default of 100 seconds.

A separate BulkStep element is required for the pre and post events, and the child and parent pipelines, so a total of 4 BulkStep elements is required to register for every supported event.

The child pipeline only supports simple messages. These are Create, Update, Delete and RetrieveExchangeRate. The extension code described here is hard-coded to limit registration on the child pipeline to these messages.