Thursday, June 14, 2018

Working with the Marketing Automation api programatically

The new Marketing Automation interface is awesome and it has a lot of great functionalities. However it seems that much of the implementation has been made with the interface/frontend in mind.

I wanted to be able to duplicate a plan programmatically, much like the "Copy" button in the interface.

The copy plan button in the Marketing Automation interface

However I found that some of this functionality is implemented purely in frontend, what happens when you click the button is:

  1. Javascript: Set the ID of the currently loaded plan to null
  2. Javascript: Prefix the plans name with "Copy of"
  3. Javascript: Go through all actions and give them a new Guid
  4. Javascript: Post the changed plan to the Api controller, and redirect to the resulting ID

The Api controller then creates this entity with the repository.

I ended up making my own small service to enable this duplication from the backend. The service has the DuplicateAutomationPlan method which gets the given plan, performs step 1 and 3 from above, and then creates it as a new plan.
The method takes two optional parameters which are actions that will be run on the plan and the individual actions. These can be used to do further needed changes, such as changing the name of the new plan, or setting parameters on individual actions.
See my WorldCup post for an example of setting parameters and name.

Below is the full source code for my service.

using Microsoft.Extensions.DependencyInjection;
using Sitecore.DependencyInjection;
using Sitecore.Marketing.Automation.Data;
using Sitecore.Marketing.Automation.Model;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace WorldCup.Services
{
    public class AutomationPlanService
    {
        private IAutomationPlanRepository _repository;

        public AutomationPlanService()
        {
            _repository = ServiceLocator.ServiceProvider.GetService<IAutomationPlanRepository>();
        }

        /// <summary>
        /// Duplicates the automationplan with the given ID (if it exists).
        /// Takes a delegate which can alter 
        /// </summary>
        public async Task<Guid> DuplicateAutomationPlan(Guid planId, string cultureName, Action<AutomationPlanDefinitionViewModel> planAction = null, Action<AutomationActivityDefinitionViewModel> activityAction = null)
        {
            var plan = await _repository.GetById(planId, cultureName);

            if (plan != null)
            {
                try
                {
                    //Set empty id on the plan
                    plan.Id = Guid.Empty;
                    //Set a new id on all activities
                    PerformActionOnAllActivities(plan.Children, activity => activity.Id = Guid.NewGuid());
                    //If a custom plan action is specified, perform that
                    planAction?.Invoke(plan);
                    //If a custom activity action is specified, perform that too
                    if (activityAction != null)
                    {
                        PerformActionOnAllActivities(plan.Children, activityAction);
                    }

                    return await _repository.Add(plan, cultureName, true, false);
                }
                catch (Exception)
                {
                    //Panic!
                }
            }
            return Guid.Empty;
        }

        /// <summary>
        /// For some reason the call to repository activate never returns (I can see in Sitecore's usage in the controller they dont wait for it either
        /// Thus, we don't implement the await
        /// </summary>
        public void ActivateAutomationPlan(Guid planId, string cultureName)
        {
            _repository.Activate(planId, cultureName);
        }
        
        /// <summary>
        /// Sitecore has a frontend script that creates new ID's for all activities, we mimic this backend.
        /// </summary>
        private void PerformActionOnAllActivities(IEnumerable<AutomationActivityDefinitionViewModel> activities, Action<AutomationActivityDefinitionViewModel> activityAction)
        {
            foreach (var activity in activities)
            {
                activityAction(activity);
                PerformActionOnAllActivities(activity.Children, activityAction);
            }
        }

        /// <summary>
        /// Just sets the parameter regardless if it was there before or not
        /// Also pads the value with extra "" around them, this is for some reason needed or there's a json parsing error
        /// </summary>
        public static void SetActivityParameter(AutomationActivityDefinitionViewModel activity, string key, string value)
        {
            var paddedValue = $"\"{value}\"";
            if (activity.Parameters.ContainsKey(key))
            {
                activity.Parameters[key] = paddedValue;
            }
            else
            {
                activity.Parameters.Add(key, paddedValue);
            }
        }
    }
}

Notice the comment on the ActivateAutomationPlan method. For some reason this never returns. This resulted in me not giving that option for the duplicate action at all, since that resulted in me never getting the id for the created plan and the process just hanging infinitely.

Working with the parameters

When saving the plan after setting parameters I encountered the very non-informative exception "Unexpected character encountered while parsing value: P. Path '', line 1, position 1." coming from the repository while mapping between entities.
It took me a while to figure out the source of this error. Apparently all parameter values on activities needs to be double encoded. This is the reason for the paddedValue variable in the SetActivityParameter method. This is meant to be used as a helper for the activityAction delegate passed to the Duplicate method.