Dataverse: Create Console For Debugging Plugin Code

Your colleague reported to you about a bug that is very hard to catch. It is reproducible in Production, but not in your Dev. The only easy way to check it is to debug it. This is a blog post about how to create a console app for debugging purposes (you also can use it for Dependent Assembly plug-ins!).

The hardest part about this blog post is how to serialize + deserialize the IPluginExecutionContext object. The idea is to get the IPluginExecutionContext that we can store and replay it manually (when running the exe). If you know Azure-aware plug-in, then you will know the object used for capturing IPluginExecutionContext is RemoteExecutionContext class. So the easiest way to Serialize (make JSON string) the object is to convert the IPluginExecutionContext object from Plugin to RemoteExecutionContext object.

Flow debug the plugin

Plugin Code

Here is my sample plugin + how to convert IPluginExecutionContext to RemoteExecutionContext object:

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization.Json;
using System.Threading;
using Microsoft.Xrm.Sdk;

namespace DemoPlugin
{
    public class Plugin1Demo : PluginBase
    {
        public Plugin1Demo() : base(nameof(Plugin1Demo))
        {
        }

        protected override void ExecuteCdsPlugin(ILocalPluginContext localPluginContext)
        {
            var remote = localPluginContext.PluginExecutionContext.ToRemoteExecutionContext();
            localPluginContext.TracingService.Trace(remote.ToJson());

            var target = (Entity)localPluginContext.PluginExecutionContext.InputParameters["Target"];

            if (target.Attributes.Contains("lastname") &&
                target.GetAttributeValue<string>("lastname") != "error") return;

            Thread.Sleep(TimeSpan.FromSeconds(5));
            throw new InvalidPluginExecutionException("Random error..");
        }
    }

    public static class Helpers
    {
        public static string ToJson(this IPluginExecutionContext context)
        {
            var serializer = new DataContractJsonSerializer(typeof(RemoteExecutionContext), new DataContractJsonSerializerSettings
            {
                DateTimeFormat = new DateTimeFormat("yyyy-MM-ddTHH\\:mm\\:ss.ffFFFFFzzz")
            });
            using (MemoryStream ms = new MemoryStream())
            using (StreamReader sr = new StreamReader(ms))
            {
                serializer.WriteObject(ms, context);
                ms.Position = 0;
                return sr.ReadToEnd();
            }
        }


        public static RemoteExecutionContext ToRemoteExecutionContext(this IPluginExecutionContext context)
        {
            var destination = new RemoteExecutionContext();
            var destFields = destination.GetType()
                .GetFields(BindingFlags.NonPublic |
                           BindingFlags.Instance)
                .ToArray();
            foreach (var sourceProperty in context.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                foreach (var destField in destFields)
                {
                    if (sourceProperty.Name == "PreEntityImages" && destField.Name == "_preImages")
                    {
                        destField.SetValue(destination, sourceProperty.GetValue(
                            context, new object[] { }));
                        break;
                    }
                    if (sourceProperty.Name == "PostEntityImages" && destField.Name == "_postImages")
                    {
                        destField.SetValue(destination, sourceProperty.GetValue(
                            context, new object[] { }));
                        break;
                    }
                    if (!destField.Name.ToLower().Contains(sourceProperty.Name.ToLower()) ||
                        !destField.FieldType.IsAssignableFrom(sourceProperty.PropertyType)) continue;
                    destField.SetValue(destination, sourceProperty.GetValue(
                        context, new object[] { }));
                    break;
                }
            }
            return destination;
        }
    }
}

As you can see, in line 20, we actually convert the IPluginExecutionContext object and change it to RemoteExecutionContext. The problem with the class RemoteExecutionContext is all the setter is using private fields. So in order to set it, we are using the FieldInfo.SetValue method. Then to make the correct JSON string, we need to use DataContractJsonSerializer and pass the type RemoteExecutionContext.

Once it is done, when you trigger the plugin, you can get the JSON string from Plugin Trace-Logs:

Get the JSON string

JSON sample:

{
  "BusinessUnitId": "37c97d50-3a20-ed11-b83b-00224828e219",
  "CorrelationId": "1e3394ce-717b-413c-a402-fbd4df278289",
  "Depth": 1,
  "InitiatingUserAzureActiveDirectoryObjectId": "4e8a594f-68a6-4ad8-be7c-771ed0f9e657",
  "InitiatingUserId": "2ad07d50-3a20-ed11-b83b-00224828e219",
  "InputParameters": [
    {
      "key": "Target",
      "value": {
        "__type": "Entity:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
        "Attributes": [
          {
            "key": "lastname",
            "value": "error"
          },
          {
            "key": "contactid",
            "value": "b3d1dd04-2426-ed11-9db1-002248210d56"
          },
          {
            "key": "fullname",
            "value": "Contact error"
          },
          {
            "key": "yomifullname",
            "value": "Contact error"
          },
          {
            "key": "modifiedon",
            "value": "2022-10-31T08:18:40.00+00:00"
          },
          {
            "key": "modifiedby",
            "value": {
              "__type": "EntityReference:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Id": "2ad07d50-3a20-ed11-b83b-00224828e219",
              "KeyAttributes": [],
              "LogicalName": "systemuser",
              "Name": null,
              "RowVersion": null
            }
          },
          {
            "key": "modifiedonbehalfby",
            "value": null
          }
        ],
        "EntityState": null,
        "FormattedValues": [],
        "Id": "b3d1dd04-2426-ed11-9db1-002248210d56",
        "KeyAttributes": [],
        "LogicalName": "contact",
        "RelatedEntities": [],
        "RowVersion": null
      }
    },
    {
      "key": "ConcurrencyBehavior",
      "value": 0
    }
  ],
  "IsExecutingOffline": false,
  "IsInTransaction": true,
  "IsOfflinePlayback": false,
  "IsolationMode": 2,
  "MessageName": "Update",
  "Mode": 0,
  "OperationCreatedOn": "2022-10-31T08:18:40.00+00:00",
  "OperationId": "d2e40395-5b0f-4f3f-b3a7-f5bddc37bed4",
  "OrganizationId": "ce0a27ca-edd1-4d54-beb7-4810b78b89e4",
  "OrganizationName": "unqce0a27caedd14d54beb74810b78b8",
  "OutputParameters": [],
  "OwningExtension": {
    "Id": "1da3ace2-4341-ed11-9db0-0022481e615e",
    "KeyAttributes": [],
    "LogicalName": "sdkmessageprocessingstep",
    "Name": "DemoPlugin.Plugin1Demo: Update of contact",
    "RowVersion": null
  },
  "ParentContext": null,
  "PostEntityImages": [],
  "PreEntityImages": [
    {
      "key": "PreImage",
      "value": {
        "Attributes": [
          {
            "key": "customertypecode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "address2_addresstypecode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "merged",
            "value": false
          },
          {
            "key": "territorycode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "haschildrencode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "exchangerate",
            "value": 1.000000000000
          },
          {
            "key": "preferredappointmenttimecode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "isbackofficecustomer",
            "value": false
          },
          {
            "key": "modifiedon",
            "value": "2022-10-15T13:52:08.00+00:00"
          },
          {
            "key": "owninguser",
            "value": {
              "__type": "EntityReference:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Id": "2ad07d50-3a20-ed11-b83b-00224828e219",
              "KeyAttributes": [],
              "LogicalName": "systemuser",
              "Name": null,
              "RowVersion": null
            }
          },
          {
            "key": "lastname",
            "value": "test 3"
          },
          {
            "key": "donotpostalmail",
            "value": false
          },
          {
            "key": "marketingonly",
            "value": false
          },
          {
            "key": "donotphone",
            "value": false
          },
          {
            "key": "preferredcontactmethodcode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "educationcode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "ownerid",
            "value": {
              "__type": "EntityReference:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Id": "2ad07d50-3a20-ed11-b83b-00224828e219",
              "KeyAttributes": [],
              "LogicalName": "systemuser",
              "Name": "Temmy Wahyu Raharjo",
              "RowVersion": null
            }
          },
          {
            "key": "customersizecode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "firstname",
            "value": "Contact"
          },
          {
            "key": "yomifullname",
            "value": "Contact test 3"
          },
          {
            "key": "donotemail",
            "value": false
          },
          {
            "key": "address2_shippingmethodcode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "fullname",
            "value": "Contact test 3"
          },
          {
            "key": "address1_addressid",
            "value": "b2a0ef36-3b83-410c-a41a-4a907ad85654"
          },
          {
            "key": "address2_freighttermscode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "statuscode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "createdon",
            "value": "2022-08-27T16:19:27.00+00:00"
          },
          {
            "key": "donotsendmm",
            "value": false
          },
          {
            "key": "donotfax",
            "value": false
          },
          {
            "key": "leadsourcecode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "creditonhold",
            "value": false
          },
          {
            "key": "transactioncurrencyid",
            "value": {
              "__type": "EntityReference:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Id": "9b3d2315-6f20-ed11-b83b-00224828e219",
              "KeyAttributes": [],
              "LogicalName": "transactioncurrency",
              "Name": "USD",
              "RowVersion": null
            }
          },
          {
            "key": "address3_addressid",
            "value": "5dc7686d-1a19-442c-9d43-9d85961cdd30"
          },
          {
            "key": "donotbulkemail",
            "value": false
          },
          {
            "key": "modifiedby",
            "value": {
              "__type": "EntityReference:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Id": "2ad07d50-3a20-ed11-b83b-00224828e219",
              "KeyAttributes": [],
              "LogicalName": "systemuser",
              "Name": "Temmy Wahyu Raharjo",
              "RowVersion": null
            }
          },
          {
            "key": "followemail",
            "value": true
          },
          {
            "key": "shippingmethodcode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 1
            }
          },
          {
            "key": "modifiedbyyominame",
            "value": "Temmy Wahyu Raharjo"
          },
          {
            "key": "createdby",
            "value": {
              "__type": "EntityReference:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Id": "2ad07d50-3a20-ed11-b83b-00224828e219",
              "KeyAttributes": [],
              "LogicalName": "systemuser",
              "Name": "Temmy Wahyu Raharjo",
              "RowVersion": null
            }
          },
          {
            "key": "donotbulkpostalmail",
            "value": false
          },
          {
            "key": "createdbyyominame",
            "value": "Temmy Wahyu Raharjo"
          },
          {
            "key": "owneridyominame",
            "value": "Temmy Wahyu Raharjo"
          },
          {
            "key": "parentcustomerid",
            "value": {
              "__type": "EntityReference:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Id": "7926d5ca-0226-ed11-9db1-002248210d56",
              "KeyAttributes": [],
              "LogicalName": "account",
              "Name": "Test Account",
              "RowVersion": null
            }
          },
          {
            "key": "contactid",
            "value": "b3d1dd04-2426-ed11-9db1-002248210d56"
          },
          {
            "key": "participatesinworkflow",
            "value": false
          },
          {
            "key": "statecode",
            "value": {
              "__type": "OptionSetValue:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Value": 0
            }
          },
          {
            "key": "owningbusinessunit",
            "value": {
              "__type": "EntityReference:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
              "Id": "37c97d50-3a20-ed11-b83b-00224828e219",
              "KeyAttributes": [],
              "LogicalName": "businessunit",
              "Name": "org81fd8e53",
              "RowVersion": null
            }
          },
          {
            "key": "address2_addressid",
            "value": "f9a50e7d-015e-4b6f-9657-450f67d46857"
          }
        ],
        "EntityState": null,
        "FormattedValues": [
          {
            "key": "customertypecode",
            "value": "Default Value"
          },
          {
            "key": "address2_addresstypecode",
            "value": "Default Value"
          },
          {
            "key": "merged",
            "value": "No"
          },
          {
            "key": "territorycode",
            "value": "Default Value"
          },
          {
            "key": "haschildrencode",
            "value": "Default Value"
          },
          {
            "key": "exchangerate",
            "value": "1.000000000000"
          },
          {
            "key": "preferredappointmenttimecode",
            "value": "Morning"
          },
          {
            "key": "isbackofficecustomer",
            "value": "No"
          },
          {
            "key": "modifiedon",
            "value": "2022-10-15T21:52:08+08:00"
          },
          {
            "key": "donotpostalmail",
            "value": "Allow"
          },
          {
            "key": "marketingonly",
            "value": "No"
          },
          {
            "key": "donotphone",
            "value": "Allow"
          },
          {
            "key": "preferredcontactmethodcode",
            "value": "Any"
          },
          {
            "key": "educationcode",
            "value": "Default Value"
          },
          {
            "key": "ownerid",
            "value": "Temmy Wahyu Raharjo"
          },
          {
            "key": "customersizecode",
            "value": "Default Value"
          },
          {
            "key": "donotemail",
            "value": "Allow"
          },
          {
            "key": "address2_shippingmethodcode",
            "value": "Default Value"
          },
          {
            "key": "address2_freighttermscode",
            "value": "Default Value"
          },
          {
            "key": "statuscode",
            "value": "Active"
          },
          {
            "key": "createdon",
            "value": "2022-08-28T00:19:27+08:00"
          },
          {
            "key": "donotsendmm",
            "value": "Send"
          },
          {
            "key": "donotfax",
            "value": "Allow"
          },
          {
            "key": "leadsourcecode",
            "value": "Default Value"
          },
          {
            "key": "creditonhold",
            "value": "No"
          },
          {
            "key": "transactioncurrencyid",
            "value": "USD"
          },
          {
            "key": "donotbulkemail",
            "value": "Allow"
          },
          {
            "key": "modifiedby",
            "value": "Temmy Wahyu Raharjo"
          },
          {
            "key": "followemail",
            "value": "Allow"
          },
          {
            "key": "shippingmethodcode",
            "value": "Default Value"
          },
          {
            "key": "createdby",
            "value": "Temmy Wahyu Raharjo"
          },
          {
            "key": "donotbulkpostalmail",
            "value": "No"
          },
          {
            "key": "parentcustomerid",
            "value": "Test Account"
          },
          {
            "key": "participatesinworkflow",
            "value": "No"
          },
          {
            "key": "statecode",
            "value": "Active"
          },
          {
            "key": "owningbusinessunit",
            "value": "org81fd8e53"
          }
        ],
        "Id": "b3d1dd04-2426-ed11-9db1-002248210d56",
        "KeyAttributes": [],
        "LogicalName": "contact",
        "RelatedEntities": [],
        "RowVersion": null
      }
    }
  ],
  "PrimaryEntityId": "b3d1dd04-2426-ed11-9db1-002248210d56",
  "PrimaryEntityName": "contact",
  "RequestId": "d2e40395-5b0f-4f3f-b3a7-f5bddc37bed4",
  "SecondaryEntityName": "none",
  "SharedVariables": [
    {
      "key": "IsAutoTransact",
      "value": true
    },
    {
      "key": "x-ms-app-name",
      "value": "tmy_BlogApp"
    }
  ],
  "Stage": 20,
  "UserAzureActiveDirectoryObjectId": "4e8a594f-68a6-4ad8-be7c-771ed0f9e657",
  "UserId": "2ad07d50-3a20-ed11-b83b-00224828e219"
}

Console Debugger

Then the next step is to create the Console for debugging purposes. You can create a Console app and install below NuGet Packages:

Microsoft.CrmSdk.XrmTooling.CoreAssembly
NSubstitute

Once done, you also need to import the plugin project to this console app:

Add the Plugin Project as a reference

For this demonstration, I use CrmServiceClient to connect to the Dataverse (but you can change it using DataverseServiceClient). Then to make the necessary object, I’m using NSubstitute. Here is the code for the Program.cs:

using System;
using System.IO;
using System.Text;
using DemoPlugin;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Extensions;
using Microsoft.Xrm.Sdk.PluginTelemetry;
using Microsoft.Xrm.Tooling.Connector;
using NSubstitute;

namespace DebugPlugin
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var connectionString = "AuthType=OAuth;Username=yourusername;Password=yourpassword;Url=https://yourorganization.crm.dynamics.com/;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97";
           
            var pluginExecutionContextString = File.ReadAllText($"{Directory.GetCurrentDirectory()}//Input.json");
            var pluginExecutionContext = DeserializeJsonString(pluginExecutionContextString);

            var serviceProvider = Substitute.For<IServiceProvider>();
            serviceProvider.Get<IPluginExecutionContext>().Returns(pluginExecutionContext);
            serviceProvider.Get<IServiceEndpointNotificationService>()
                .Returns(Substitute.For<IServiceEndpointNotificationService>());
            serviceProvider.Get<IExecutionContext>()
                .Returns(Substitute.For<IExecutionContext>());
            serviceProvider.Get<ITracingService>()
                .Returns(Substitute.For<ITracingService>());
            serviceProvider.Get<ILogger>()
                .Returns(Substitute.For<ILogger>());
            var factory = Substitute.For<IOrganizationServiceFactory>();
            factory.CreateOrganizationService(Arg.Any<Guid?>()).Returns((param) =>
            {
                var service = new CrmServiceClient(connectionString);
                var userId = param.ArgAt<Guid?>(0);
                if (userId != null) service.CallerId = userId.GetValueOrDefault();
                return service;
            });
            serviceProvider.Get<IOrganizationServiceFactory>().Returns(factory);

            new Plugin1Demo().Execute(serviceProvider);
            Console.ReadKey();
        }

        public static RemoteExecutionContext DeserializeJsonString(string jsonString)
        {
            var obj = Activator.CreateInstance<RemoteExecutionContext>();
            MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(jsonString));
             var serializer = new DataContractJsonSerializer(typeof(RemoteExecutionContext), new DataContractJsonSerializerSettings
            {
               DateTimeFormat = new DateTimeFormat("yyyy-MM-ddTHH\\:mm\\:ss.ffFFFFFzzz")
               
            });
            obj = (RemoteExecutionContext)serializer.ReadObject(ms);
            ms.Close();
            return obj;
        }
    }
}

Then I also create a JSON file named Input.json > right click >”Copy to Output Directory” to “Copy if newer“. The purpose of this file is to get the string from the file (hardcoded file path) and then Deserialize (read the JSON and convert it back to C# object) it. Then as you can see from the above code, most of the code is to create the necessary object for the Plugin to run (this can vary in every organization. Got some that usually just need IPluginExecutionContext and IOrganizationService).

Demo:

Demo

Happy CRM-ing!

Advertisement

One thought on “Dataverse: Create Console For Debugging Plugin Code

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.