Automating Plug-in Deployments – Microsoft Dynamics CRM

Overview

Microsoft Dynamics CRM makes it very easy to develop plugins that allow you to customize workflows and communicate changes to other systems. Unfortunately, Microsoft has yet to provide a convenient way to automate plugin deployment. Since CRM 2011, they recommend using a windowed application called the PluginRegistration tool that comes with the Dynamics CRM SDK.

The tool is great, except you need to manually configure each event your plugins handle. Once you move beyond 2 or 3 events, this manual process becomes tedious and error-prone. Worse, during development, you’ll find yourself needing to replace your plugin assembly and tweaking settings frequently. Having an automated deployment in this case can be the difference between development taking a few hours and it taking a few days.

Hidden in the CRM SDK is a mechanism to automate plugin deployments. I will show how to tap into this mechanism throughout this article. Your team should be able to take what I describe and adapt it to your development environment. I think you’ll find that some aspects of the deployment are unbelievably straight-forward and other aspects unnervingly complex. For those less intuitive areas, I will provide code snippets to save you from needing to reinvent the wheel and needless head banging.

Attributes and Reflection

Every plugin we write responds to an event in the CRM system. Some example events would be creating an account, adding a role to a system user or updating contact information. In CRM, each event handler is referred to as a plugin step. Plugins are configured so that they are executed whenever an event fires. The IPlugin interface in the SDK exposes the details of these events within the IPluginExecutionContext service: the initiating user, the event type (a.k.a. message) and the entity being modified (a.k.a. target).

As part of automating deployments, we need to provide information about which steps need to be registered. Technically, you could define these steps anywhere, but I’ve found defining them in the code provides the most flexibility. In general, only two pieces of information are needed to register a step: the message and the entity type(s). To capture this information, we define an interface named IPluginStep.

public interface IPluginStep
{
       string Message { get; }

       string PrimaryEntity { get; }
}

Some plugins require a secondary entity. In that case, you would provide a third property named SecondaryEntity. If a message does not involve a primary or secondary entity, these values are left null. You could also define a property Description to provide a meaningful description of your steps; this can be used later during plugin registration.

The next step is to decorate your plugin classes with this information. To do this, we’ll use homegrown Attribute classes. During the deployment process, we will use reflection to inspect the attributes on our plugin types to help configure the steps. I recommend creating separate attributes for each plugin you write. Later on, you can add additional properties to your attributes for plugin-specific settings.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class PluginStepAttribute : Attribute, IPluginStep
{
       public string Message { get; set; }

       public string PrimaryEntity { get; set; }
}

Place this attribute at the top of a plugin for each step that needs registered. Microsoft Dynamics CRM is case-sensitive, so be sure to use the correct message and entity names.

[PluginStep(Message = "Create", PrimaryEntity = "account")]
[PluginStep(Message = "Update", PrimaryEntity = "account")]
public class NotificationPlugin : IPlugin
{
       public NotificationPlugin(string unsecureConfig)
{
              // ...
}

       public void Execute(IServiceProvider serviceProvider)
{
              // ...
}
}

The code above will be used to tell the plugin registration code to create two steps for the NotificationPlugin. Once our plugin attributes are in place, we’re ready to continue.

Wiping out previous installations

Before we get into deploying our plugin, we need to clear out any previous installations. Plugins are stored in the CRM database with a simple structure: Plugin Assembly > Plugin Types > Plugin Steps > Secure Configurations.

The plugin assembly literally holds the contents of your plugin DLL. A plugin type will exist for each plugin (IPlugin) in the assembly that was registered. Each step that was configured will appear under that, with a secure configuration record, if applicable. Since these records are linked to each other using foreign keys, they must be removed from the CRM system from the bottom up. To do this you first find the assembly, then the plugins, then the steps and finally the secure configurations. You then delete them in reverse order. The next section provides the code to do this.

Finding the previous installation

You can look up a previous installation of your assembly by searching for it by name. The name can be hard-coded or found by loading the assembly in memory (via Assembly.LoadFrom) and using the following code:

private Guid? getAssemblyId(Assembly assembly)
{

QueryExpression query = new QueryExpression("pluginassembly");
AssemblyName assemblyName = assembly.GetName();
query.Criteria.AddCondition(

"name",
ConditionOperator.Equal,
assemblyName.Name);

EntityCollection collection = service.RetrieveMultiple(query);
return collection.Entities.Select(e => e.Id).SingleOrDefault();

}

Here, service is an instance of IOrganizationService or OrganizationServiceProxy. We look up the assembly name using the AssemblyName object and search for the plugin assembly using a QueryExpression. If an assembly exists with that name in the CRM system, this will return its unique identifier; otherwise, it will return null.

Once we have the assembly ID, we can look up the plugin types.

private IEnumerable<Guid> getPluginIds(Guid assemblyId)
{

QueryExpression query = new QueryExpression("plugintype");
query.Criteria.AddCondition(

"pluginassemblyid",
ConditionOperator.Equal,
assemblyId.ToString());

EntityCollection entities = service.RetrieveMultiple(query);
return entities.Entities.Select(e => e.Id).ToArray();

}

Similarly, we can look up the plugin steps.

private IEnumerable<Guid> getStepIds(Guid pluginId)
{

QueryExpression query = new QueryExpression("sdkmessageprocessingstep");
query.Criteria.AddCondition(

"plugintypeid",
ConditionOperator.Equal,
pluginId.ToString());

EntityCollection entities = service.RetrieveMultiple(query);
return entities.Entities.Select(e => e.Id).ToArray();

}

Finally, each plugin step has a nullable secure configuration ID field (sdkmessageprocessingstepsecureconfigid). You can grab these along with the step ID(s) above or in a separate query.

private IEnumerable<Guid> getSecureConfigurationIds(IEnumerable<Guid> stepIds)
{

QueryExpression query = new QueryExpression("sdkmessageprocessingstep");
query.ColumnSet.AddColumn("sdkmessageprocessingstepsecureconfigid");
query.Criteria.AddCondition(

"sdkmessageprocessingstepid",
ConditionOperator.In,
stepIds.Select(id => id.ToString()));

EntityCollection entities = service.RetrieveMultiple(query);
return entities.Entities

.Select(e => e.GetAttributeValue<Guid?>("sdkmessageprocessingstepsecureconfigid"))
.Where(id => id != null)
.Select(id => id.Value)
.Distinct()
.ToArray();

}

Removing the previous installation

Once you have your IDs, it is very easy to delete the corresponding entity. Simply call Delete for each entity ID that is found, just be sure to call Delete in the right order:

service.Delete("sdkmessageprocessingstepsecureconfig", configId);

service.Delete("sdkmessageprocessingstep", stepId);

service.Delete("plugintype", pluginId);

service.Delete("pluginassembly", assemblyId);

Deploying the plugin

Now that the previous installation has been removed, we can deploy the latest code.

Uploading the assembly

The first thing we must do is create a new plugin assembly entity in the database. Again, load the assembly using Assembly.LoadFrom. This will bring the assembly into memory, so you can inspect it with reflection.

private Guid registerAssembly(Assembly assembly)
{

AssemblyName assemblyName = assembly.GetName();

Entity entity = new Entity("pluginassembly");
entity.Attributes.Add("name", assemblyName.Name);
const int databaseAssemblySourceType = 0;
entity.Attributes.Add("sourcetype", new OptionSetValue(databaseAssemblySourceType));
const int noneAssemblyIsolationMode = 1;
entity.Attributes.Add("isolationmode", new OptionSetValue(noneAssemblyIsolationMode));
entity.Attributes.Add("culture", assemblyName.CultureInfo.Name);
byte[] publicKey = assemblyName.GetPublicKeyToken();
string publicKeyToken = String.Join(

String.Empty,
publicKey.Select(b => b.ToString("X2", CultureInfo.InvariantCulture)));

entity.Attributes.Add("publickeytoken", publicKeyToken);
entity.Attributes.Add("version", assemblyName.Version.ToString());
entity.Attributes.Add("description", null);
byte[] assemblyData = File.ReadAllBytes(assembly.Location);
string assemblyFormatted = Convert.ToBase64String(assemblyData);
entity.Attributes.Add("content", assemblyFormatted);

return service.Create(entity);

}

I’ve highlighted the two most complicated and important blocks of code. First, you must provide the public token of the assembly. This can be retrieved from the AssemblyName object, but even then it must be formatted as a hexadecimal string. Second, you must include the assembly itself as a base 64 encoded string.

You’ll also notice the sourcetype and isolationmode fields were set to OptionSetValues. OptionSetValues represent lookup values and we will need to use them to register the remaining entities. If you need to change these values, you can look in the CRM database to determine the available values.

For source type, here are the available options:

  • Database (0)
  • Disk (1)
  • GAC (2)

For isolation mode:

  • Invalid (0)
  • None (1)
  • Sandbox (2)

Registering the plugins

After calling registerAssembly, we will have the unique identifier of the plugin assembly. We can now search through the assembly for classes implementing IPlugin.

private IEnumerable<Type> getPluginTypes(Assembly assembly)
{

var query = from type in assembly.GetTypes()

where !type.IsInterface
where !type.IsAbstract
where typeof(IPlugin).IsAssignableFrom(type)
select type;

return query.ToArray();

}

Notice that this code filters out interfaces and abstract classes. This will bring back only the plugin types that can be instantiated. You may need to adjust this code if there are plugins you do not want to deploy.

To register the plugin,

private Guid registerPlugin(Guid assemblyId, Type pluginType)
{

Entity entity = new Entity("plugintype");
entity.Attributes.Add("pluginassemblyid", new EntityReference()
{

LogicalName = "pluginassembly",
Id = assemblyId

});
entity.Attributes.Add("typename", pluginType.FullName);
entity.Attributes.Add("name", pluginType.FullName);
entity.Attributes.Add("friendlyname", pluginType.Name);

return service.Create(entity);

}

Registering the steps

Given the plugin IDs and types from the previous code, we can now search for our custom attributes. If you’ll recall, our custom attributes implement IPluginStep, which provides information about what event the step is handling.

private IEnumerable<dynamic> getPluginSteps(Assembly assembly, Type pluginType)
{

Type pluginStepType = assembly.GetType(

"MyNamespace.IPluginStep",
throwOnError: true,
ignoreCase: false);

var steps = pluginType.GetCustomAttributes(true)

.Where(a => pluginStepType.IsAssignableFrom(a.GetType()))
.Select(a => (dynamic)a)
.OrderBy(a => a.PrimaryEntity)
.ThenBy(a => a.Message)
.ToArray();

return steps;

}

Obviously, you will need to replace “MyNamespace” with the actual namespace in your code.

You might be surprised how complicated this code is. Remember that this assembly is loaded using Assembly.LoadFrom. Since it is loaded dynamically, IPluginStep needs to be looked up dynamically, too. From this point forward, any information we grab will work with a dynamic entity.

The next step is to actually add the step entity to the CRM system. Most of the configuration takes place at the plugin step level, so this is by the far the most complicated code. I will go over it in detail.

private Guid registerStep(Guid pluginId, Type pluginType, dynamic step)
{

Entity entity = new Entity("sdkmessageprocessingstep");
entity.Attributes.Add("configuration", "my unsecure config");
entity.Attributes.Add("eventhandler", new EntityReference("plugintype", pluginId));
entity.Attributes.Add("name", getStepName(pluginType, step));
const int synchronousStepMode = 0;
entity.Attributes.Add("mode", new OptionSetValue(synchronousStepMode));
entity.Attributes.Add("rank", 1);
const int parentInvocationSource = 0;
entity.Attributes.Add("invocationsource", new OptionSetValue(parentInvocationSource));
Guid? messageId = getMessageId(step);
if (messageId != null)
{

entity.Attributes.Add("sdkmessageid", new EntityReference("sdkmessage", messageId.Value));
Guid ? filterId = getMessageFilterId(messageId, step);
if (filterId != null)
{

entity.Attributes.Add(

"sdkmessagefilterid",
new EntityReference("sdkmessagefilter", filterId.Value));

}

}
Guid? impersonatingUserId = getUserId();
EntityReference impersonator = impersonatingUserId == null

? null
: new EntityReference("systemuser", impersonatingUserId.Value);

entity.Attributes.Add("impersonatinguserid", impersonator);
const int postOperationStage = 40;
entity.Attributes.Add("stage", new OptionSetValue(postOperationStage));
const int serverOnlyDeployment = 0;
entity.Attributes.Add("supporteddeployment", new OptionSetValue(serverOnlyDeployment));
entity.Attributes.Add("filteringattributes", String.Empty);
entity.Attributes.Add("asyncautodelete", false);
return service.Create(entity);

}

private static string getStepName(Type pluginType, dynamic step)
{

string pluginName = pluginType.FullName;
string entityName = step.PrimaryEntity ?? "any entity";
const string format = "{0}: {1} of {2}";
string stepName = String.Format(format, pluginName, step.Message, step.PrimaryEntity);
return stepName;

}

private Guid? getUserId()
{

const string userName = @"domainname\username";
QueryExpression query = new QueryExpression("systemuser");
query.Criteria.AddCondition("domainname", ConditionOperator.Equal, userName);
EntityCollection entities = service.RetrieveMultiple(query);
return entities.Entities.Select(e => e.Id).SingleOrDefault();

}

private Guid? getMessageId(dynamic Step)
{

QueryExpression query = new QueryExpression("sdkmessage");
query.Criteria.AddCondition("name", ConditionOperator.Equal, Step.Message);
EntityCollection entities = service.RetrieveMultiple(query);
return entities.Entities.Select(e => e.Id).SingleOrDefault();

}

private Guid? getMessageFilterId(Guid? messageId, dynamic Step)
{

EntityMetadata metadata = getEntityMetadata(Step.PrimaryEntity);
if (metadata == null)
{

return null;

}
QueryExpression query = new QueryExpression("sdkmessagefilter");
query.Criteria.AddCondition("sdkmessageidname", ConditionOperator.Equal, Step.Message);
query.Criteria.AddCondition(

"primaryobjecttypecode",
ConditionOperator.Equal,
metadata.ObjectTypeCode);

EntityCollection entities = service.RetrieveMultiple(query);
return entities.Entities.Select(e => e.Id).SingleOrDefault();

}

private EntityMetadata getEntityMetadata(string logicalName)
{

if (logicalName == null)
{

return null;

}
RetrieveEntityRequest metaRequest = new RetrieveEntityRequest();
metaRequest.EntityFilters = EntityFilters.Entity;
metaRequest.LogicalName = logicalName;
var response = (RetrieveEntityResponse)service.Execute(metaRequest);
EntityMetadata metadata = response.EntityMetadata;
return metadata;

}

A plugin step must be linked back the plugin so it will be run when an event takes place. This requires knowing the following information:

  • what message to handle (Create, Update, etc.)
  • which entity type is being handled (account, contact, etc.),
  • whether to run before or after the operation takes place
  • which user will be running the plugin (impersonation)
  • and various less-common settings

The message and entity type come from our IPluginStep interface. However, a good portion of the code deals with making sure the CRM system is properly configured to handle a given message/entity type combination. To do that, we must query the CRM system for a sdkmessage entity and metadata about the entity type. Together, we can look for a message filter (sdkmessagefilter), which is an entity representing a valid combination. Once we gather this information, we use it to build the step entity.

Plugins can be set up to run as the current user, the SYSTEM user or as a custom user. Leaving the impersonatinguserid attribute null means using the current user. Setting it to Guid.Empty indicates SYSTEM (not recommended). Otherwise, we must look up the ID of the user we want to use.

Every step requires a name. This can be any arbitrary text, but the PluginRegistration tool uses a very specific naming convention. The getStepName method is designed to capture this naming convention.

The following values are available for mode:

  • Synchronous (0)
  • Asynchronous (1)

The invocation source can be one of:

  • Parent (0)
  • Child (1)

A step can be run at these stages:

  • Invalid (0)
  • Pre-Validation (10)
  • Pre-Operation (20)
  • Post-Operation (40)
  • Post-Operation Deprecated (50) (CRM 4 only)

Finally, the following values can be set for the deployment:

  • Server Only (0)
  • Offline Only (1)
  • Both (2)

If you are using an unsecure configuration, you can simply pass it as a string to the step entity (highlighted above). Otherwise, if you are using a secure configuration, you must create a sdkmessageprocessingstepsecureconfig entity. The only attribute you need to provide is secureconfig. Once you get an ID back from calling Create, you can set the plugin step’s sdkmessageprocessingstepsecureconfigid attribute to link the step to the secure configuration.

Summary

As you can see, registering plugins is no more involved than building up entities and calling Create. The challenge is setting the entity attributes, and hopefully my code snippets will get you where you need to go. My recommendation is to put this code in a simple console application, taking the path to the assembly as a command line argument.

If you feel like you need more control over the registration process, feel free to add additional properties to the IPluginStep interface. This is a great place to switch between synchronous and asynchronous and control when the plugin fires. Additionally, I usually add a Description property that allows me to provide a short description about what my steps are doing. The plugin assembly, plugin type and plugin step entities all supply an optional description property just for this purpose.

The beauty of this approach is you only need to write it one time and it shortens deployments from taking a few minutes to taking just a few seconds.