Programming Microsoft Dynamics CRM 4.0: Plug-ins

  • 12/15/2008

Deployment

At the beginning of the chapter we briefly touched on one of the tools used to deploy plug-ins. In this section we’ll take a deeper look at what happens during plug-in registration and how you can write your own registration tools.

Plug-in Entities

Microsoft Dynamics CRM stores plug-in information in a series of entities as listed in Table 5-4.

Table 5-4. Plug-in Entities

Entity Name

Description

pluginassembly

Represents the registered plug-in assembly. Can have multiple plugintype entities associated with it.

plugintype

Represents the class in the plug-in assembly that implements IPlugin. Can have multiple steps associated with it.

sdkmessageprocessingstep

Represents a step in the event execution pipeline when a plug-in type should be executed. Can have multiple sdkmessageprocessingimage entities and multiple sdkmessageprocessingstepsecureconfig entities associated with it.

sdkmessageprocessingstepimage

Represents the definition of which types of images should be provided to a plug-in for a particular step. Images are essentially snapshots of the entity before or after the core operation has taken place.

sdkmessageprocessingstepsecureconfig

Represents secure configuration information for a particular plug-in step. Passed to the plug-in constructor if provided.

Programmatic Plug-in Registration

You can register and deploy plug-ins programmatically through the API, which allows you to implement your own deployment tools without a lot of code. To demonstrate this, we will implement a plug-in registration tool that uses custom .NET attributes to specify how to register our plug-ins. This approach offers the benefit of letting the developer implement the plug-in to specify its use as he codes it.

Because both the plug-in assembly and our installation tool reference our custom .NET attributes, we need to put them in their own class library. Follow these steps to add the project to our existing solution.

Adding the custom attribute project

  1. On the File Menu, select Add and then click New Project.

  2. In the New Project dialog box, select the Visual C# project type targeting the .NET Framework 3.0, and then select the Class Library template.

  3. Type the name ProgrammingWithDynamicsCrm4.Plugins.Attributes in the Name box, and then click OK.

  4. Delete the default Class.cs file.

  5. Right-click the ProgrammingWithDynamicsCrm4.Plugins.Attributes project in Solution Explorer and then click Properties.

  6. On the Signing tab, select the Sign The Assembly box and select <New...> from the drop-down list below it.

  7. Type the key file name ProgrammingWithDynamicsCrm4.Plugins.Attributes and then clear the Protect My Key File With A Password check box. Click OK.

  8. Close the project properties window.

Adding the PluginStepAttribute class

Next we need to define the custom attribute class.

  1. Right-click the ProgrammingWithDynamicsCrm4.Plugins.Attributes project in Solution Explorer. Under Add, click Class.

  2. Type PluginStepAttribute.cs in the Name box and click Add.

Example 5-2 shows the full source code for the PluginStepAttribute class.

Example 5-2. PluginStepAttribute source code

using System;

namespace ProgrammingWithDynamicsCrm4.Plugins.Attributes
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
    public class PluginStepAttribute: Attribute
    {
        public PluginStepAttribute(string message, PluginStepStage stage)
        {
            this.Message = message;
            this.Stage = stage;
        }

        public PluginStepStage Stage { get; private set; }
        public string Message { get; private set; }

        public string PrimaryEntityName { get; set; }
        public string SecondaryEntityName { get; set; }

        public PluginStepMode Mode { get; set; }
        public int Rank { get; set; }
        public string Description { get; set; }
        public string FilteringAttributes { get; set; }
        public PluginStepInvocationSource InvocationSource { get; set; }
        public PluginStepSupportedDeployment SupportedDeployment { get; set; }
    }
}

Custom attributes like the one we have defined here allow us to embed extra informationinto our compiled types and assemblies. We can use this attribute to attach information about our plug-in registration to the plug-in class itself. The following example demonstrates how this attribute might be applied to a plug-in class.

[PluginStep("Update", PluginStepStage.PreEvent, PrimaryEntityName = "account")]
public class MyPluginClass: IPlugin
{
    ...
}

Notice that even though the class name is PluginStepAttribute, we can omit the trailing Attribute—which is just a shortcut supplied by .NET—when applying it to a class. Also worth noticing is that we’ve exposed some of the arguments as constructor arguments and others as public properties. In general, when you work with custom attributes you want to make anything required a constructor argument and anything optional a public property. You might argue that PrimaryEntityName should have been in the list of required attributes, but you can register for a few messages that do not have an entity associated with them.

If you have a sharp eye, you probably noticed that we still need to define the types for a few properties in PluginStepAttribute. These four types are all defined as enums so that you will receive IntelliSense in Visual Studio 2008 when you apply this attribute to a plug-in class. The enums are defined as shown in Example 5-3.

Example 5-3. Enum type definitions

namespace ProgrammingWithDynamicsCrm4.Plugins.Attributes
{
    public enum PluginStepInvocationSource
    {
        ParentPipeline = 0,
        ChildPipeline = 1,
    }

    public enum PluginStepMode
    {
        Synchronous = 0,
        Asynchronous = 1,
    }

    public enum PluginStepStage
    {
        PreEvent = 10,
        PostEvent = 50,
    }

    public enum PluginStepSupportedDeployment
    {
        ServerOnly=0,
        OutlookClientOnly=1,
        Both=2,
    }
}

You can place these definitions in their own files or just after the PluginStepAttribute class in PluginStepAttribute.cs.

Now we should be able to go back to our original ProgrammingWithDynamicsCrm4.Plugins project and add a reference to our new attributes project.

Adding a reference to the attributes project

  1. Right-click the ProgrammingWithDynamicsCrm4.Plugins project in Solution Explorer and then click Add Reference.

  2. On the Projects tab, select ProgrammingWithDynamicsCrm4.Plugins.Attributes. Click OK.

Now we can add our attribute to the AccountNumberValidator plug-in. You will need to add the following using statement at the top of AccountNumberValidator.cs:

using ProgrammingWithDynamicsCrm4.Plugins.Attributes;

Then you can add the following two attributes to the class definition:

[PluginStep("Create", PluginStepStage.PreEvent, PrimaryEntityName = "account",
    FilteringAttributes = "accountnumber")]
[PluginStep("Update", PluginStepStage.PreEvent, PrimaryEntityName = "account",
    FilteringAttributes = "accountnumber")]
public class AccountNumberValidator: IPlugin
{
    ...
}

At this point, the only remaining step is creating the actual tool to register the plug-in.

Creating the ProgrammingWithDynamicsCrm4.PluginDeploy project

  1. On the File Menu, select Add and then click New Project.

  2. In the New Project dialog box, select the Visual C# project type targeting the .NET Framework 3.0 and then select the Console Application template.

  3. Type the name ProgrammingWithDynamicsCrm4.PluginDeploy in the Name box and click OK.

  4. Right-click the ProgrammingWithDynamicsCrm4.PluginDeploy project in Solution Explorer and then click Add Reference.

  5. On the Browse tab, navigate to the CRM SDK’s bin folder and select microsoft.crm.sdk.dll and microsoft.crm.sdktypeproxy.dll. Click OK.

  6. Right-click the ProgrammingWithDynamicsCrm4.PluginDeploy project in Solution Explorer and then click Add Reference.

  7. On the .NET tab, select System.Web.Services and System.Configuration. Click OK.

  8. Right-click the ProgrammingWithDynamicsCrm4.PluginDeploy project in Solution Explorer and then click Add Reference.

  9. On the Projects tab, select ProgrammingWithDynamicsCrm4.Plugins.Attributes and click OK.

Now we can proceed to the Main method. Replace the generated code in Program.cs with the code shown in Example 5-4.

Example 5-4. PluginDeploy’s Main method

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Web.Services.Protocols;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;
using ProgrammingWithDynamicsCrm4.Plugins.Attributes;

namespace ProgrammingWithDynamicsCrm4.PluginDeploy
{
    static void Main(string[] args)
    {
        if (args.Length != 3)
        {
            string exeName = Path.GetFileName(Environment.GetCommandLineArgs()[0]);
            Console.WriteLine(
                "Usage: {0} <pluginAssembly> <crmServerUrl> <organizationName>",
                exeName);
            Environment.Exit(1);
        }

        try
        {
            string pluginAssemblyPath = args[0];
            string crmServer = args[1];
            string organizationName = args[2];

            DeployPlugin(pluginAssemblyPath, crmServer, organizationName);
        }
        catch (SoapException e)
        {
            Console.WriteLine(e.Detail.InnerText);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

The Main method doesn’t do much more than look for simple usage errors and display unhandled exceptions to the user. All of the real work is left to the DeployPlugin method, which is shown in Example 5-5.

Example 5-5. The DeployPlugin method

private static void DeployPlugin(
    string pluginAssemblyPath,
    string crmServer,
    string organizationName)
{
    Console.Write("Initializing CrmService... ");
    CrmService crmService = CreateCrmService(crmServer, organizationName);
    Console.WriteLine("Complete");

    pluginassembly pluginAssembly = LoadPluginAssembly(pluginAssemblyPath);

    UnregisterExistingSolution(crmService, pluginAssembly.name);

    SdkMessageProcessingStepRegistration[] steps =
        LoadPluginSteps(pluginAssemblyPath);

    RegisterSolution(crmService, pluginAssembly, steps);
}

The first thing DeployPlugin does is create an instance of the CrmService. CreateCrmService is a helper method that creates a CrmService in a fairly straightforward way. Example 5-6 shows the implementation of CreateCrmService.

Example 5-6. The CreateCrmService method

private static CrmService CreateCrmService(
    string crmServer, string organizationName)
{
    UriBuilder crmServerUri = new UriBuilder(crmServer);
    crmServerUri.Path = "/MSCRMServices/2007/CrmService.asmx";

    string userName = ConfigurationManager.AppSettings["crmUserName"];
    string password = ConfigurationManager.AppSettings["crmPassword"];
    string domain = ConfigurationManager.AppSettings["crmDomain"];

    CrmService crmService = new CrmService();
    if (String.IsNullOrEmpty(userName))
    {
        crmService.UseDefaultCredentials = true;
    }
    else
    {
        crmService.Credentials = new NetworkCredential(userName, password, domain);
    }

    crmService.Url = crmServerUri.ToString();
    crmService.CrmAuthenticationTokenValue = new CrmAuthenticationToken();
    crmService.CrmAuthenticationTokenValue.AuthenticationType =
        AuthenticationType.AD;
    crmService.CrmAuthenticationTokenValue.OrganizationName = organizationName;

    return crmService;
}

CreateCrmService checks in the application configuration file to see whether any credentials are specified to use when communicating with Microsoft Dynamics CRM. If it does not find any, it uses the credentials of the user that started the process.

After DeployPlugin aquires a CrmService, it calls LoadPluginAssembly to load an instance of the pluginassembly class from the plug-in DLL. The source for LoadPluginAssembly is shown in Example 5-7.

Example 5-7. The LoadPluginAssembly method

private static pluginassembly LoadPluginAssembly(string pluginAssemblyPath)
{
    Assembly assembly = Assembly.LoadFile(pluginAssemblyPath);
    pluginassembly pluginAssembly = new pluginassembly();
    pluginAssembly.name = assembly.GetName().Name;
    pluginAssembly.sourcetype = new Picklist(AssemblySourceType.Database);
    pluginAssembly.culture = assembly.GetName().CultureInfo.ToString();
    pluginAssembly.version = assembly.GetName().Version.ToString();

    if (String.IsNullOrEmpty(pluginAssembly.culture))
    {
        pluginAssembly.culture = "neutral";
    }

    byte[] publicKeyToken = assembly.GetName().GetPublicKeyToken();
    StringBuilder tokenBuilder = new StringBuilder();
    foreach (byte b in publicKeyToken)
    {
        tokenBuilder.Append(b.ToString("x").PadLeft(2, '0'));
    }
    pluginAssembly.publickeytoken = tokenBuilder.ToString();

    pluginAssembly.content = Convert.ToBase64String(
        File.ReadAllBytes(pluginAssemblyPath));

    return pluginAssembly;
}

Most of the pluginassembly class’s properties are populated using reflection on the assembly after it is loaded. The publickeytoken property is a little bit more work because we need to convert the byte array to a hexadecimal string. The content property is a Base64-formatted string that contains the raw bytes from the assembly DLL. Also note that we have just hard-coded sourcetype to be a database deployment.

After PluginDeploy receives pluginassembly, it calls UnregisterExistingSolution to make sure that no pre-existing version of this assembly is registered on the CRM server. The UnregisterExistingSolution source code is shown in Example 5-8.

Example 5-8. The UnregisterExistingSolution method

private static void UnregisterExistingSolution(
    CrmService crmService,
    string assemblyName)
{
    QueryByAttribute query = new QueryByAttribute();
    query.EntityName = EntityName.pluginassembly.ToString();
    query.ColumnSet = new ColumnSet(new string[] { "pluginassemblyid" });
    query.Attributes = new string[] { "name" };
    query.Values = new object[] { assemblyName };

    RetrieveMultipleRequest request = new RetrieveMultipleRequest();
    request.Query = query;

    RetrieveMultipleResponse response;
    Console.Write("Searching for existing solution... ");
    response = (RetrieveMultipleResponse)crmService.Execute(request);
    Console.WriteLine("Complete");

    if (response.BusinessEntityCollection.BusinessEntities.Count > 0)
    {
        pluginassembly pluginAssembly = (pluginassembly)
            response.BusinessEntityCollection.BusinessEntities[0];
        Console.Write("Unregistering existing solution {0}... ",
            pluginAssembly.pluginassemblyid.Value);

        UnregisterSolutionRequest unregisterRequest =
            new UnregisterSolutionRequest();
        unregisterRequest.PluginAssemblyId = pluginAssembly.pluginassemblyid.Value;

        crmService.Execute(unregisterRequest);
        Console.WriteLine("Complete");
    }
}

The UnregisterExistingSolution method starts by querying CrmService to see whether any pluginassembly entities are already registered with the same name. If it finds one, it executes an UnregisterSolutionRequest, passing in the Guid of the assembly that was determined to be a match.

DeployPlugin is now ready to use our custom attribute and create an array of SdkMessageProcessingStepRegistration instances. SdkMessageProcessingStepRegistration is a part of the Microsoft.Crm.Sdk namespace and is used to simplify the registration of plug-ins. Example 5-9 shows the source code for LoadPluginSteps.

Example 5-9. The LoadPluginSteps method

private static SdkMessageProcessingStepRegistration[] LoadPluginSteps(string
pluginAssemblyPath)
{
    List<SdkMessageProcessingStepRegistration> steps =
        new List<SdkMessageProcessingStepRegistration>();

    Assembly assembly = Assembly.LoadFile(pluginAssemblyPath);
    foreach (Type pluginType in assembly.GetTypes())
    {
        if (typeof(IPlugin).IsAssignableFrom(pluginType) && !pluginType.IsAbstract)
        {
            object[] stepAttributes =
                pluginType.GetCustomAttributes(typeof(PluginStepAttribute), false);

            foreach (PluginStepAttribute stepAttribute in stepAttributes)
            {
                steps.Add(CreateStepFromAttribute(pluginType, stepAttribute));
            }
        }
    }

    return steps.ToArray();
}

LoadPluginSteps loads the assembly from the disk and then uses reflection to iterate through all the types defined in the assembly. If it finds a concrete implementation of IPlugin, it determines whether our PluginStepAttribute is associated with that type. For each PluginStepAttribute associated with the plugin type, it calls CreateStepFromAttribute to create an instance of SdkMessageProcessingStepRegistration. The CreateStepFromAttribute source code is shown in Example 5-10.

Example 5-10. The CreateStepFromAttribute method

private static SdkMessageProcessingStepRegistration CreateStepFromAttribute(
    Type pluginType,
    PluginStepAttribute stepAttribute)
{
    SdkMessageProcessingStepRegistration step =
        new SdkMessageProcessingStepRegistration();
    step.Description = stepAttribute.Description;
    step.FilteringAttributes = stepAttribute.FilteringAttributes;
    step.InvocationSource = (int)stepAttribute.InvocationSource;
    step.MessageName = stepAttribute.Message;
    step.Mode = (int)stepAttribute.Mode;
    step.PluginTypeName = pluginType.FullName;
    step.PluginTypeFriendlyName = pluginType.FullName;
    step.PrimaryEntityName = stepAttribute.PrimaryEntityName;
    step.SecondaryEntityName = stepAttribute.SecondaryEntityName;
    step.Stage = (int)stepAttribute.Stage;
    step.SupportedDeployment = (int)stepAttribute.SupportedDeployment;

    if (String.IsNullOrEmpty(step.Description))
    {
        step.Description = String.Format("{0} {1} {2}",
            step.PrimaryEntityName, step.MessageName, stepAttribute.Stage);
    }

    return step;
}

Almost all the SdkMessageProcessingStepRegistration values are assigned directly from corresponding values on our PluginStepAttribute class. The PluginTypeName property comes from the actual plug-in type (and we use that for the PluginTypeFriendlyName too). If no Description is provided, we derive one from the PrimaryEntityName, MessageName, and Stage properties.

Finally, DeployPlugin is ready to send all this information over to the CRM server. RegisterSolution is called, passing our previously loaded pluginassembly and our newly initialized array of SdkMessageProcessingStepRegistrations. The RegisterSolution source code is shown in Example 5-11.

Example 5-11. The RegisterSolution method

private static void RegisterSolution(CrmService crmService, pluginassembly
pluginAssembly, SdkMessageProcessingStepRegistration[] steps)
{
    RegisterSolutionRequest registerRequest = new RegisterSolutionRequest();
    registerRequest.PluginAssembly = pluginAssembly;
    registerRequest.Steps = steps;
    Console.Write("Registering solution... ");
    crmService.Execute(registerRequest);
    Console.WriteLine("Complete");
}

RegisterSolution is a straightforward method that simply creates a RegisterSolutionRequest and executes it with the CrmService.

At this point you should be able to compile the solution and use ProgrammingWithDynamicsCrm4.PluginDeploy.exe to deploy ProgrammingWithDynamicsCrm4.Plugins to your CRM server. The command line used to deploy a plug-in is:

ProgrammingWithDynamicsCrm4.PluginDeploy.exe <pathToAssembly> <crmServerUrl>
<organizationName>

Images

One concept that our previous example did not touch on is the ability to request entity images during registration. An image is essentially a snapshot of an entity and it can be taken either before or after the core operation is performed. Images allow a plug-in access to attribute values that would otherwise not be available. For example, an audit log plug-in could be provided with an image that contains the original attribute values for an entity that has just been modified. Using this image, the plug-in could record both the new and old attribute values in a log. Another example of using images would be a plug-in that needs to keep a calculated value on a parent entity up to date. When the child entity is associated with anew parent, a plug-in can use a pre-image to retrieve the previous parent and ensure that both the new and the old parent are kept up to date.

We refer to images that are taken before the core operation as pre-images and images taken after the core operation as post-images. These images are then passed to a plug-in through the IPIuginExecutionContext interface’s PreEntityImages and PostEntityImages properties.

When your plug-in requires an image, you need to specify the type of image and the name of the message property that contains the entity you want an image of. In addition, not all messages can produce images. Table 5-5 lists all supported messages and their corresponding message property names.

Table 5-5. Messages That Support Images

Message

Message Property Name

Notes

Assign

Target

Create

Id

Does not support pre-images

Delete

Target

Does not support post-images

DeliverIncoming

EmailId

DeliverPromote

EmailId

Merge

Target

Merge

SubordinateId

Route

Target

Send

EmailId

SetState

EntityMoniker

SetStateDynamicEntity

EntityMoniker

Update

Target

Programmatic Image Registration

To add image support to ProgrammingWithDynamicsCrm4.PluginDeploy, we need an additional attribute class. Using the steps outlined earlier, add a new class to the ProgrammingWithDynamicsCrm4.Plugins.Attributes project named PluginImageAttribute.The source code for this class is shown in Example 5-12.

Example 5-12. PluginImageAttribute source code

using System;

namespace ProgrammingWithDynamicsCrm4.Plugins.Attributes
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
    public class PluginImageAttribute: Attribute
    {
        public PluginImageAttribute(
            ImageType imageType,
            string stepId,
            string messagePropertyName,
            string entityAlias)
        {
            this.ImageType = imageType;
            this.StepId = stepId;
            this.MessagePropertyName = messagePropertyName;
            this.EntityAlias = entityAlias;
        }
        public ImageType ImageType { get; private set; }
        public string StepId { get; private set; }
        public string MessagePropertyName { get; private set; }
        public string EntityAlias { get; private set; }
        public string Attributes { get; set; }
    }

    public enum ImageType
    {
        PreImage = 0,
        PostImage = 1,
        Both = 2,
    }
}

All the properties except Attributes are required for PluginImageAttribute, so they are all passed in to the constructor. If no attributes are specified for an image, it is populated with all of the entity’s attributes that have values.

One property that might not have a readily apparent use is StepId. Because an image is associated with a particular step and we can have multiple steps per plug-in, we need a way to tie the two attributes together. To do this, we assign a unique value to the StepId property on both the PluginStepAttribute and PluginImageAttribute classes. We need to modify the PluginStepAttribute class to include the following new property:

public string StepId { get; set; }

Notice that StepId is optional on the PluginStepAttribute class because it is only needed if the developer wants to specify an image for that step.

Now a developer can register for an image on her plug-in class by using two attributes together:

[PluginStep("Update", PluginStepStage.PreEvent, PrimaryEntityName = "account",
    StepId="AccountPreUpdate")]
[PluginImage(ImageType.PreImage, "AccountPreUpdate", "Target", "Account")]
public class MyPlugin: IPlugin
{
    ...
}

Notice that the StepId for this example is arbitrarily set to AccountPreUpdate. It doesn’t matter what value you use as long as the values for the step and the image match.

Finally, we need to modify our console application to use the new PluginImageAttribute type. We need to insert the code from Example 5-13 into the CreateStepFromAttribute method just before the return statement.

Example 5-13. Modifications to the CreateStepFromAttribute method

if (!String.IsNullOrEmpty(stepAttribute.StepId))
{
    List<SdkMessageProcessingStepImageRegistration> images =
        new List<SdkMessageProcessingStepImageRegistration>();
    object[] imageAttributes = pluginType.GetCustomAttributes(
        typeof(PluginImageAttribute), false);

    foreach (PluginImageAttribute imageAttribute in imageAttributes)
    {
        if (imageAttribute.StepId == stepAttribute.StepId)
        {
            images.Add(CreateImageFromAttribute(imageAttribute));
        }
    }

    if (images.Count > 0)
    {
        step.Images = images.ToArray();
    }
}

This change checks whether the current step attribute has a step ID assigned. If it does, the method looks for image attributes on the plugin type. If it finds any image attributes, it calls the CreateImageFromAttribute method, which is shown in Example 5-14.

Example 5-14. The CreateImageFromAttribute method

private static SdkMessageProcessingStepImageRegistration
CreateImageFromAttribute(PluginImageAttribute imageAttribute)
{
    SdkMessageProcessingStepImageRegistration image =
            new SdkMessageProcessingStepImageRegistration();

    if (!String.IsNullOrEmpty(imageAttribute.Attributes))
    {
        image.Attributes = imageAttribute.Attributes.Split(',');
    }

    image.EntityAlias = imageAttribute.EntityAlias;
    image.ImageType = (int)imageAttribute.ImageType;
    image.MessagePropertyName = imageAttribute.MessagePropertyName;

    return image;
}

CreateImageFromAttribute creates a new instance of the SdkMessageProcessingStepImage-Registration class and populates it from the image attribute. The appropriate images are assigned back to the step’s Images property and are automatically registered when the call to RegisterSolution is made.

The final code for Program.cs is shown in Example 5-15.

Example 5-15. Source code for ProgrammingWithDynamicsCrm4.PluginDeploy’s Program.cs

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Web.Services.Protocols;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;
using ProgrammingWithDynamicsCrm4.Plugins.Attributes;

namespace ProgrammingWithDynamicsCrm4.PluginDeploy
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 3)
            {
                string exeName = Path.GetFileName(
                    Environment.GetCommandLineArgs()[0]);
                Console.WriteLine(
                    "Usage: {0} <pluginAssembly> <crmServerUrl> <organizationName>",
                    exeName);
                Environment.Exit(1);
             }

             try
             {
                string pluginAssemblyPath = args[0];
                string crmServer = args[1];
                string organizationName = args[2];

                DeployPlugin(pluginAssemblyPath, crmServer, organizationName);
             }
             catch (SoapException e)
             {
                 Console.WriteLine(e.Detail.InnerText);
             }
             catch (Exception e)
             {
                 Console.WriteLine(e.Message);
             }
        }

        private static void DeployPlugin(
            string pluginAssemblyPath,
            string crmServer,
            string organizationName)
        {
            Console.Write("Initializing CrmService... ");
            CrmService crmService = CreateCrmService(crmServer, organizationName);
            Console.WriteLine("Complete");

            pluginassembly pluginAssembly = LoadPluginAssembly(pluginAssemblyPath);

            UnregisterExistingSolution(crmService, pluginAssembly.name);

            SdkMessageProcessingStepRegistration[] steps =
                LoadPluginSteps(pluginAssemblyPath);

            RegisterSolution(crmService, pluginAssembly, steps);
        }

        private static pluginassembly LoadPluginAssembly(string pluginAssemblyPath)
        {
            Assembly assembly = Assembly.LoadFile(pluginAssemblyPath);
            pluginassembly pluginAssembly = new pluginassembly();
            pluginAssembly.name = assembly.GetName().Name;
            pluginAssembly.sourcetype = new Picklist(AssemblySourceType.Database);
            pluginAssembly.culture = assembly.GetName().CultureInfo.ToString();
            pluginAssembly.version = assembly.GetName().Version.ToString();

            if (String.IsNullOrEmpty(pluginAssembly.culture))
            {
                pluginAssembly.culture = "neutral";
            }

            byte[] publicKeyToken = assembly.GetName().GetPublicKeyToken();
            StringBuilder tokenBuilder = new StringBuilder();
            foreach (byte b in publicKeyToken)
            {
                tokenBuilder.Append(b.ToString("x").PadLeft(2, '0'));
            }
            pluginAssembly.publickeytoken = tokenBuilder.ToString();

            pluginAssembly.content =
                Convert.ToBase64String(File.ReadAllBytes(pluginAssemblyPath));

            return pluginAssembly;
        }

        private static void UnregisterExistingSolution(
            CrmService crmService,
            string assemblyName)
        {
            QueryByAttribute query = new QueryByAttribute();
            query.EntityName = EntityName.pluginassembly.ToString();
            query.ColumnSet = new ColumnSet(new string[] { "pluginassemblyid" });
            query.Attributes = new string[] { "name" };
            query.Values = new object[] { assemblyName };

            RetrieveMultipleRequest request = new RetrieveMultipleRequest();
            request.Query = query;

            RetrieveMultipleResponse response;
            Console.Write("Searching for existing solution... ");
            response = (RetrieveMultipleResponse)crmService.Execute(request);
            Console.WriteLine("Complete");

            if (response.BusinessEntityCollection.BusinessEntities.Count > 0)
            {
                pluginassembly pluginAssembly = (pluginassembly)
                    response.BusinessEntityCollection.BusinessEntities[0];
                Console.Write("Unregistering existing solution {0}... ",
                    pluginAssembly.pluginassemblyid.Value);

                UnregisterSolutionRequest unregisterRequest =
                    new UnregisterSolutionRequest();
                unregisterRequest.PluginAssemblyId =
                    pluginAssembly.pluginassemblyid.Value;

                 crmService.Execute(unregisterRequest);
                 Console.WriteLine("Complete");
            }
        }

        private static SdkMessageProcessingStepRegistration[] LoadPluginSteps(
            string pluginAssemblyPath)
        {
            List<SdkMessageProcessingStepRegistration> steps =
                new List<SdkMessageProcessingStepRegistration>();

            Assembly assembly = Assembly.LoadFile(pluginAssemblyPath);
            foreach (Type pluginType in assembly.GetTypes())
            {
                if (typeof(IPlugin).IsAssignableFrom(pluginType)
                    && !pluginType.IsAbstract)
                {
                    object[] stepAttributes = pluginType.GetCustomAttributes(
                        typeof(PluginStepAttribute), false);
                    foreach (PluginStepAttribute stepAttribute in stepAttributes)
                    {
                        steps.Add(CreateStepFromAttribute(pluginType,
                            stepAttribute));
                    }
                }
            }

            return steps.ToArray();
        }

        private static SdkMessageProcessingStepRegistration CreateStepFromAttribute(
            Type pluginType,
            PluginStepAttribute stepAttribute)
        {
            SdkMessageProcessingStepRegistration step =
                new SdkMessageProcessingStepRegistration();
            step.Description = stepAttribute.Description;
            step.FilteringAttributes = stepAttribute.FilteringAttributes;
            step.InvocationSource = (int)stepAttribute.InvocationSource;
            step.MessageName = stepAttribute.Message;
            step.Mode = (int)stepAttribute.Mode;
            step.PluginTypeName = pluginType.FullName;
            step.PluginTypeFriendlyName = pluginType.FullName;
            step.PrimaryEntityName = stepAttribute.PrimaryEntityName;
            step.SecondaryEntityName = stepAttribute.SecondaryEntityName;
            step.Stage = (int)stepAttribute.Stage;

            if (String.IsNullOrEmpty(step.Description))
            {
                step.Description = String.Format("{0} {1} {2}",
                    step.PrimaryEntityName, step.MessageName, stepAttribute.Stage);
            }

        if (!String.IsNullOrEmpty(stepAttribute.StepId))
        {
           List<SdkMessageProcessingStepImageRegistration> images =
               new List<SdkMessageProcessingStepImageRegistration>();
           object[] imageAttributes = pluginType.GetCustomAttributes(
               typeof(PluginImageAttribute), false);
           foreach (PluginImageAttribute imageAttribute in imageAttributes)
           {
               if (imageAttribute.StepId == stepAttribute.StepId)
               {
                   images.Add(CreateImageFromAttribute(imageAttribute));
               }
           }

           if (images.Count > 0)
           {
               step.Images = images.ToArray();
           }
        }
           return step;
        }

        private static SdkMessageProcessingStepImageRegistration
            CreateImageFromAttribute(PluginImageAttribute imageAttribute)
        {
            SdkMessageProcessingStepImageRegistration image =
                new SdkMessageProcessingStepImageRegistration();

            if (!String.IsNullOrEmpty(imageAttribute.Attributes))
            {
                image.Attributes = imageAttribute.Attributes.Split(',');
            }
                image.EntityAlias = imageAttribute.EntityAlias;
                image.ImageType = (int)imageAttribute.ImageType;
                image.MessagePropertyName = imageAttribute.MessagePropertyName;

                return image;
        }

        private static void RegisterSolution(
            CrmService crmService,
            pluginassembly pluginAssembly,
            SdkMessageProcessingStepRegistration[] steps)
        {
            RegisterSolutionRequest registerRequest = new RegisterSolutionRequest();
            registerRequest.PluginAssembly = pluginAssembly;
            registerRequest.Steps = steps;
            Console.Write("Registering solution... ");
            crmService.Execute(registerRequest);
            Console.WriteLine("Complete");
        }

        private static CrmService CreateCrmService(
            string crmServer,
            string organizationName)
        {
            UriBuilder crmServerUri = new UriBuilder(crmServer);
            crmServerUri.Path = "/MSCRMServices/2007/CrmService.asmx";

            string userName = ConfigurationManager.AppSettings["crmUserName"];
            string password = ConfigurationManager.AppSettings["crmPassword"];
            string domain = ConfigurationManager.AppSettings["crmDomain"];

            CrmService crmService = new CrmService();
            if (String.IsNullOrEmpty(userName))
            {
                crmService.UseDefaultCredentials = true;
            }
            else
            {
                crmService.Credentials = new NetworkCredential(
                    userName, password, domain);
            }

            crmService.Url = crmServerUri.ToString();
            crmService.CrmAuthenticationTokenValue = new CrmAuthenticationToken();
            crmService.CrmAuthenticationTokenValue.AuthenticationType =
                AuthenticationType.AD;
            crmService.CrmAuthenticationTokenValue.OrganizationName =
                organizationName;

            return crmService;
        }
    }
}

Custom Configuration

We have not yet touched on one entity tied to plug-in registration: sdkmessageprocessingstepsecureconfig. You use this entity to pass a step-specific configuration value to the plug-in. The data in these entities is secure because only users with high security roles (for example, System Administrator or System Customizer) have permission to read these entities; therefore, you can safely use them to store sensitive information such as database connection strings. If you don’t need the security, you can also specify a value to the sdkmessageprocessingstep class’s configuration property. In our previous example you would specify the value to the CustomConfiguration property on the SdkMessageProcessingStepRegistration class.

For a plug-in to get the custom configuration value that you registered it with it must implement a constructor that takes one or two string arguments. If the version that takes two arguments exists, it will be called with the nonsecure configuration and the secure configuration as the two values. If the single argument version is implemented, it will be called with the nonsecure configuration value. Example 5-16 shows an example of the two argument version.

Example 5-16. A plug-in constructor accepting custom configuration value

public class MyPlugin: IPlugin
{
    private string _connectionString;
    private Guid _defaultAccount;

    public MyPlugin(string unsecureConfig, string secureConfig)
    {
        if (!String.IsNullOrEmpty(unsecureConfig))
        {
            _defaultAccount = new Guid(unsecureConfig);
        }
        _connectionString = secureConfig;
    }

    ...
}

Deploying Referenced Assemblies

Frequently a plug-in includes dependencies on other assemblies. If those assemblies are not a part of the .NET Framework or the CRM SDK, you need to consider how to deploy them. The simplest option is to deploy them into the GAC on the CRM server. Depending on how frequently those referenced assemblies change, keeping the server’s GAC up to date can be a hassle. The GAC is not easily maintained remotely, and you usually end up using Remote Desktop or something equivalent to manually copy the files into the GAC.

An alternative is to use a tool called ILMerge. You use ILMerge to combine multiple .NET assemblies into a single one. This allows you to merge your plug-in DLL with any of the DLLs it references and then deploy the single DLL to the CRM database. We frequently create a post-build step on our plug-in class library project to merge the output with the dependencies.

To add a post-build step in Visual Studio, right-click the project in Solution Explorer and select Properties. Then click the Build Events tab. You can then enter command-line commands into the post-build event command line.

Here is an example of what the post-build command line might look like:

if not exist PreMerge mkdir PreMerge
del /Q PreMerge\*.*

move ProgrammingWithDynamicsCrm4.Plugins.dll PreMerge
move ProgrammingWithDynamicsCrm4.Plugins.pdb PreMerge
move <referencedDll> PreMerge

$(SolutionDir)Tools\ILMerge.exe /keyfile:$(ProjectDir)
ProgrammingWithDynamicsCrm4.Plugins.snk /lib:PreMerge /out:
ProgrammingWithDynamicsCrm4.Plugins.dll ProgrammingWithDynamicsCrm4.Plugins.dll
<referencedDll>

In this example, we want the final DLL to be in the same folder and have the same name as the original DLL, so we create a subfolder called PreMerge within the output folder. We then proceed to copy the recently compiled DLL and its dependencies into the PreMerge folder. Notice that we do not include Microsoft.Crm.Sdk.dll or Microsoft.Crm.SdkTypeProxy.dll. Because those files will be on the server, we do not need to merge them into our DLL. The final step is to execute ILMerge.exe specifying the keyfile to use to sign the assembly, the folder where it can find the DLLs to include, the name of the output file, and the list of DLLs to include in the merge.