Converting FormContainerBlock Types in Optimizely Forms for CMS 12

While this is not something you would commonly do, here's how I approached converting a FormContainerBlock type to our custom ExtendedFormContainerBlock in Optimizely Forms for CMS 12. You may need to do this if you want to add custom properties or functionality to the standard FormContainerBlock type, and you don't want to rebuild or recreate all previously created forms.

A Disclaimer

Treat this code as a proof of concept. Treat it as not production-ready. You should use it as a starting point for you to build upon. You may lose data. Backup your databases. Test it multiple times locally. Test it in a staging environment. Test it again. And then test it one more time. It works on my machine. It may not work on yours.

Converting FormContainerBlocks

For this approach, I just used a scheduled job to execute the code. This way, I could run it once and then remove the job once the conversion is completed.

You'll see in the code that I'm updating the tblContent, tblContentProperty, and tblWorkContentProperty tables directly. Obviously, it's not recommended to excute straight SQL commands, but there's not really a better way to do this by just using the APIs that Optimizely provides.

Here's the scheduled job I created:

[ScheduledPlugIn(DisplayName = "Convert FormContainerBlocks", GUID = "00000000-0000-0000-0000-000000000000", Description = "Converts all FormContainerBlock instances to ExtendedFormContainerBlock.")]
public class ConvertFormContainerBlocksScheduledJob : ScheduledJobBase
{
	private readonly IConfiguration _configuration;
	private readonly IContentTypeRepository _contentTypeRepository;
	private readonly IContentModelUsage _contentModelUsage;
	private readonly IContentVersionRepository _contentVersionRepository;
	private readonly IPropertyDefinitionRepository _propertyDefinitionRepository;
	private readonly IContentRepository _contentRepository;

	private bool _stopSignaled;

	public ConvertFormContainerBlocksScheduledJob(
		IConfiguration configuration,
		IContentTypeRepository contentTypeRepository,
		IContentModelUsage contentModelUsage,
		IPropertyDefinitionRepository propertyDefinitionRepository,
		IContentRepository contentRepository,
		IContentVersionRepository contentVersionRepository)
    {
        _configuration = configuration;
        _contentTypeRepository = contentTypeRepository;
        _contentModelUsage = contentModelUsage;
        _propertyDefinitionRepository = propertyDefinitionRepository;
        _contentRepository = contentRepository;
        _contentVersionRepository = contentVersionRepository;

        IsStoppable = true;
    }

    /// <summary>
    /// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
    /// </summary>
    public override void Stop()
    {
        _stopSignaled = true;
    }

    /// <summary>
    /// Called when a scheduled job executes
    /// </summary>
    /// <returns>A status message to be stored in the database log and visible from admin mode</returns>
    public override string Execute()
    {
        //Call OnStatusChanged to periodically notify progress of job for manually started jobs
        OnStatusChanged($"Starting execution of {GetType()}");

        //Add implementation
        throw new Exception("Don't run untested code");

        var formContainersContentLinks = GetFormContainerContentLinks();

        if (formContainersContentLinks == null || !formContainersContentLinks.Any())
        {
            return "No FormContainerBlocks found";
        }

        OnStatusChanged($"Found {formContainersContentLinks.Count()} FormContainerBlocks");

        var connectionString = _configuration.GetConnectionString("EPiServerDB");
        var extendedFormContainerBlockContentType = _contentTypeRepository.Load<ExtendedFormContainerBlock>();

        int total = formContainersContentLinks.Count();
        int succeeded = 0;
        int failed = 0;

        foreach (var contentLink in formContainersContentLinks)
        {
            OnStatusChanged($"Converting FormContainerBlock with ID {contentLink.ID}");

            var fcbInstance = _contentRepository.Get<FormContainerBlock>(contentLink);

            var fcbContentType = _contentTypeRepository.Load<FormContainerBlock>();
            var efcbContentType = _contentTypeRepository.Load<ExtendedFormContainerBlock>();

            var fcbPropertyDefinitionList = _propertyDefinitionRepository.List(fcbContentType.ID);
            var efcbPropertyDefinitionList = _propertyDefinitionRepository.List(efcbContentType.ID);

            try
            {
                using var connection = new SqlConnection(connectionString);

                connection.Open();

                using var updateContentCommand = new SqlCommand("UPDATE tblContent SET fkContentTypeID = @ContentTypeID WHERE pkID = @ContentLinkID", connection);

                updateContentCommand.Parameters.AddWithValue("@ContentTypeID", extendedFormContainerBlockContentType.ID);
                updateContentCommand.Parameters.AddWithValue("@ContentLinkID", contentLink.ID);

                updateContentCommand.ExecuteNonQuery();
                updateContentCommand.Dispose();

                foreach (var propertyDefinition in fcbPropertyDefinitionList)
                {
                    var newPropertyDefinition = efcbPropertyDefinitionList.FirstOrDefault(pd => pd.Name == propertyDefinition.Name);

                    if (newPropertyDefinition == null)
                    {
                        continue;
                    }

                    using var updatePropertyCommand = new SqlCommand("UPDATE tblContentProperty SET fkPropertyDefinitionID = @NewPropertyDefinitionId WHERE fkPropertyDefinitionID = @OldPropertyDefinitionId AND fkContentID = @ContentLinkId", connection);

                    updatePropertyCommand.Parameters.AddWithValue("@NewPropertyDefinitionId", newPropertyDefinition.ID);
                    updatePropertyCommand.Parameters.AddWithValue("@OldPropertyDefinitionId", propertyDefinition.ID);
                    updatePropertyCommand.Parameters.AddWithValue("@ContentLinkID", contentLink.ID);

                    updatePropertyCommand.ExecuteNonQuery();
                    updatePropertyCommand.Dispose();

                    var versions = _contentVersionRepository.List(contentLink);

                    foreach (var version in versions)
                    {
                        using var updateWorkPropertyCommand = new SqlCommand("UPDATE tblWorkContentProperty SET fkPropertyDefinitionID = @NewPropertyDefinitionId WHERE fkPropertyDefinitionID = @OldPropertyDefinitionId AND fkWorkContentID = @WorkContentLinkId", connection);

                        updateWorkPropertyCommand.Parameters.AddWithValue("@NewPropertyDefinitionId", newPropertyDefinition.ID);
                        updateWorkPropertyCommand.Parameters.AddWithValue("@OldPropertyDefinitionId", propertyDefinition.ID);
                        updateWorkPropertyCommand.Parameters.AddWithValue("@WorkContentLinkID", version.ContentLink.WorkID);

                        updateWorkPropertyCommand.ExecuteNonQuery();
                        updateWorkPropertyCommand.Dispose();
                    }
                }

                OnStatusChanged($"Converted FormContainerBlock with ID {contentLink.ID}");

                succeeded++;
            }
            catch (Exception ex)
            {
                //Log exception
                OnStatusChanged($"Failed to convert FormContainerBlock with ID {contentLink.ID} - {ex.Message}");

                failed++;
            }

            //For long running jobs periodically check if stop is signaled and if so stop execution

            if (_stopSignaled)
            {
                return $"Stop of job was called. Found {total} FormContainerBlocks. Converting {succeeded} succeeded, {failed} failed.";
            }
        }

        return $"Job completed. Found {total} FormContainerBlocks. Converting {succeeded} succeeded, {failed} failed.";
    }

    private IEnumerable<ContentReference> GetFormContainerContentLinks()
    {
        var formContainerBlockContentType = _contentTypeRepository.Load<FormContainerBlock>();

        if (formContainerBlockContentType == null)
        {
            return Enumerable.Empty<ContentReference>();
        }

        var formContainerBlockUsage = _contentModelUsage.ListContentOfContentType(formContainerBlockContentType);

        if (formContainerBlockUsage == null || !formContainerBlockUsage.Any())
        {
            return Enumerable.Empty<ContentReference>();
        }

        return formContainerBlockUsage.DistinctBy(usage => usage.ContentLink.ID).Select(usage => usage.ContentLink);
    }
}

Did I mention that you should test this code multiple times before running it in production? I did? Good.