Table of Contents

Getting Started with Plugin Development

Check out the API reference and the samples below.

Plugin Basics

Plugins receive a "context" parameter which contains a variety of things but the most important are:

  • context.Targets - When registered to CreateMultiple or UpdateMultiple, this contains a list of records that are being created or updated. Each item in the list is a TargetItem object which contains the Target, PreImage, and PostImage information.
  • context.XrmContext - Provides access to the IOrganizationService instance.
  • context.PluginContext - Provides access to the IPluginExecutionContext instance.
using Hsl.Xrm.Sdk;
using Hsl.Xrm.Sdk.Plugin;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sample.Plugins
{
    public class ExampleBulkPlugin : PluginBase2, IPlugin
    {
        public ExampleBulkPlugin()
        {
        }

        public ExampleBulkPlugin(string unsecureConfig, string secureConfig) : base(unsecureConfig, secureConfig)
        {
        }

        protected override void LoadConfiguration(string unsecureConfig, string secureConfig)
        {
            base.LoadConfiguration(unsecureConfig, secureConfig);
        }

        public override void ExecuteAction(IPluginEventContext2 pluginEventContext)
        {
            var systemService = pluginEventContext.XrmContext.SystemOrgService;
            var targets = pluginEventContext.Targets;
            foreach(var target in targets)
            {
                if (!target.Input.Contains("hsl_field"))
                    continue;

                // More code here
            }

            var parentAccountIds = targets.Select(x => x.GetValue<Guid>("hsl_accountid")).Distinct();

            var parentAccountsById = systemService.BulkRetrieveByIds("account", parentAccountIds, new ColumnSet("name", "customertypecode"))
                .ToDictionary(x => x.Id);

            foreach (var target in targets)
            {
                var parent = parentAccountsById[target.GetValue<Guid>("hsl_accountid")];

                var parentType = parent.GetFieldValue<int>("customertypecode");
                var priorOwner = target.PreImage.GetAttributeValue<Guid>("ownerid");
                // Do something with the target
            }
        }
    }
}

Configuration

See plugin configuration or legacy plugin configuration.

Debugging

Images are not yet available when debugging CreateMultiple and UpdateMultiple plugins. To work around this:

  1. Add the following setting to plugin step's secure config

     { "EnableNonBulkMessageCompatibility": true }
    

    This will allow IPluginEventContext2.Targets to be used with the those messages so your code written for CreateMultiple should work against the Create message unchanged.

  2. Change the plugin to be registered to the Create or Update message temporarily.

  3. Capture your debugging profile.

  4. Cleanup

    1. Change the message back to CreateMultiple or UpdateMultiple
    2. Remove EnableNonBulkMessageCompatibility from the secure config.
Note

Be sure to cleanup before deploying to downstream environments!

Plugin Fields

Be careful with fields in your plugin class. The instance is reused by multiple executions which might be executing in parallel.

using Hsl.Xrm.Sdk;
using Hsl.Xrm.Sdk.Plugin;
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sample.Plugins
{
    public class ExamplePluginFieldsBad : PluginBase2
    {
        public ExamplePluginFieldsBad()
        {
        }

        public ExamplePluginFieldsBad(string unsecureConfig, string secureConfig) : base(unsecureConfig, secureConfig)
        {
        }

        // WRONG: plugin classes should not use instance fields to store state
        private bool shouldDoSomething;
        private List<string> firstCharacterList = new List<string>();

        public override void ExecuteAction(IPluginEventContext2 pluginEventContext)
        {
            //pluginEventContext.GetSharedVariable<Entity>("name", true, );
            var targets = pluginEventContext.Targets;
            foreach (var item in targets)
            {
                // WRONG: don't use instance fields this way
                // The plugin class is reused and different executions
                // might interfere with each other.
                shouldDoSomething = item.GetValue<string>("hsl_abc") == "def";

                MethodCall(item, "hsl_string1");
                MethodCall(item, "hsl_string2");

                item["hsl_firstcharacters"] = String.Join(", ", firstCharacterList);
            }
        }

        private void MethodCall(ITargetItem<Entity> target, string field)
        {
            var value = target.GetValue<string>(field);
            // WRONG: don't use instance fields this way
            // The plugin class is reused and different executions
            // might interfere with each other.
            if (value != null && (value.Length > 3 || shouldDoSomething))
            {
                firstCharacterList.Add(value.Substring(0, 1));
            }
        }
    }
}

Using local functions is a great workaround. Alternatively, create a business logic class and use it in the plugin.

using Hsl.Xrm.Sdk;
using Hsl.Xrm.Sdk.Plugin;
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sample.Plugins
{
    public class ExamplePluginFieldsGood : PluginBase2
    {
        public ExamplePluginFieldsGood()
        {
        }

        public ExamplePluginFieldsGood(string unsecureConfig, string secureConfig) : base(unsecureConfig, secureConfig)
        {
        }

        public override void ExecuteAction(IPluginEventContext2 pluginEventContext)
        {
            // This is not necessarily the best way to achieve what this code is doing. It is just a contrived example
            // to should how switching fields to local variables and local functions might work when 
            // comparing to ExamplePluginFieldsBad.

            var targets = pluginEventContext.Targets;

            foreach (var item in targets)
            {
                // The fields from our previous example become local variables.
                var shouldDoSomething = item.GetValue<string>("hsl_abc") == "def";
                var firstCharacterList = new List<string>();


                MethodCall("hsl_string1");
                MethodCall("hsl_string2");

                item["hsl_firstcharacters"] = String.Join(", ", firstCharacterList);
                // I like to put a return statement before any local functions to make it clear that this is the end of the main execution of the method.
                // This isn't too important here since the local function is short. If you have several local functions or a long one, it can help.
                return;

                // our private method becomes a local function.
                void MethodCall(string field)
                {
                    var value = item.GetValue<string>(field);
                    if (value != null && (value.Length > 3 || shouldDoSomething))
                    {
                        firstCharacterList.Add(value.Substring(0, 1));
                    }
                }
            }
        }
    }
}

Read contents of trace log

There are a few pain points you may encounter when developing plugins for Custom APIs:

  • The platform's plugin trace logs truncate earlier messages if trace messages exceed a certain length.
  • When testing execution of a Custom API, finding the trace log that corresponds to your execution can be challenging.
  • If a flow calls a Custom API, finding the associated trace log can be difficult (or, if enough time has passed, impossible).

Plugins inheriting from PluginBase2 have the option to programmatically access the trace log added by the execution. Plugins of Custom APIs can leverage this feature to return the trace log as an output, which resolves the pain points mentioned above.

To configure your plugin to be able to read trace log messages:

  1. Your plugin must inherit from PluginBase2
  2. The constructor must set this.EnableGetTraceLog = true;
  3. Adds messages to trace log via IPluginEventContext2.Logger.LogInfo() (or any of the other extension methods such as LogDebug, LogWarning, etc).
  4. Read and return the trace log via IPluginEventContext2.GetTraceLog().
Note

Enabling this feature will cause your plugin execution to consume more memory, as all trace log messages are stored in a StringBuilder.

In the following example, the Custom API has an input parameter which allows the caller to specify whether the trace log should be returned or not.

using Hsl.Logging;
using Hsl.Xrm.Sdk;
using Hsl.Xrm.Sdk.Plugin;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Linq;
using System.Net.Http;
using System.Text.Json;

namespace Sample.Plugins;

/// <summary>
/// This option uses the plugin base class
/// </summary>
public class ExamplePluginEnableGetTraceLog : PluginBase2, IPlugin
{
    public override void ExecuteAction(IPluginEventContext2 pluginEventContext)
    {
        // This example demonstrates how a plugin can acess the trace log messages that it
        // added.  This is particularly useful for plugins of custom APIs where it is
        // sometimes desirable to return the trace log messages in an output parameter.
        // 
        // To enable this functionality:
        // 1. Constructor must set EnableGetTraceLog to true
        // 2. Trace log becomes available via pluginEventContext.GetTraceLog()
        //
        // Please note that this will cause your plugin execution to consume more memory, 
        // as all of the trace log messages are collected in a StringBuilder object.


        // ...custom api business logic goes here...
        pluginEventContext.Logger.LogInfo("some info message");
        pluginEventContext.Logger.LogDebug("some debug message");
        pluginEventContext.Logger.LogError("some error message");


        // return the trace log messages as an output parameter if requested
        if (pluginEventContext.PluginContext.InputParameters.Contains("ReturnTraceLog") &&
            (bool)pluginEventContext.PluginContext.InputParameters["ReturnTraceLog"] == true)
        {
            // constructor must set EnableGetTraceLog to true in order to use GetTraceLog()
            pluginEventContext.PluginContext.OutputParameters["TraceLog"] = pluginEventContext.GetTraceLog();
        }
    }

    #region Constructors
    public ExamplePluginEnableGetTraceLog()
    {
        
    }

    public ExamplePluginEnableGetTraceLog(string unsecureConfig, string secureConfig) : base(unsecureConfig, secureConfig)
    {
        // required in order to be able to call IPluginEventContext2.GetTraceLog()
        this.EnableGetTraceLog = true;
    }
    #endregion

}