Tags: CMS12 Optimizely/Episerver Scheduled jobs

Updated Optimizely Link Status Report – Missing property names!

A couple of months ago Optimizely CMS introduced Deep linking from broken links report to Optimizely edit mode!

One problem I discovered was that the «Property Name» does not show up in the Link Status report for existing content until you publish a change. This is because the column fkOwnerPropertyDefinitionID in the database table tblContentSoftLink doesn't get populated when the scheduled link validation job runs. It's only populated when the content is published!

Link status report with missing property name

This is fine if you are starting a brand new website without any content today! If you have some existing content, you would probably want the latest features without manually republishing all your content, right?

I threw together a quick scheduled job that clears the table tblContentSoftlink, and rebuilds it.

Use and modify at your own risk and pleasure, and remember that you'll need to run the default scheduled link validation job after running this job. You should not need to run this job more than once.

The «Rebuild softlinks» job only populates the table tblContentSoftlink with links, the link validation job actually validates them.

using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer;
using EPiServer.Core;
using EPiServer.Core.Internal;
using EPiServer.DataAbstraction;
using EPiServer.PlugIn;
using EPiServer.Scheduler;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;

namespace Gulla.Web.Business.Jobs
{
    [ScheduledPlugIn(
        DisplayName = "Rebuild softlinks",
        GUID = "36cb07ce-7deb-4212-8b99-b234b23885eb",
        Restartable = true)]
    public class RebuildSoftlinksJob : ScheduledJobBase
    {
        private readonly IContentRepository _contentRepository;
        private readonly IContentTypeRepository _contentTypeRepository;
        private readonly IContentModelUsage _contentModelUsage;
        private readonly IContentSoftLinkRepository _contentSoftLinkRepository;
        private readonly ContentSoftLinkIndexer _contentSoftLinkIndexer;
        private readonly IConfiguration _configuration;
        private bool _stopSignaled;

        public RebuildSoftlinksJob(
            IContentRepository contentRepository,
            IContentTypeRepository contentTypeRepository,
            IContentModelUsage contentModelUsage,
            IContentSoftLinkRepository contentSoftLinkRepository,
            ContentSoftLinkIndexer contentSoftLinkIndexer,
            IConfiguration configuration)
        {
            _contentRepository = contentRepository;
            _contentTypeRepository = contentTypeRepository;
            _contentModelUsage = contentModelUsage;
            _contentSoftLinkRepository = contentSoftLinkRepository;
            _contentSoftLinkIndexer = contentSoftLinkIndexer;
            _configuration = configuration;
            IsStoppable = true;
        }


        public override string Execute()
        {
            var numOfLinksPreRun = ExecuteScalar("SELECT COUNT(1) FROM tblContentSoftlink");
            var numOfLinksWithPropertyPreRun = ExecuteScalar("SELECT COUNT(1) FROM tblContentSoftlink WHERE fkOwnerPropertyDefinitionID is NOT NULL");

            // Delete all softlinks
            var cmd = @"DELETE FROM tblContentSoftlink";
            ExecuteCmd(cmd);

            // Rebuild softlinks for all content
            RebuildSoftlinksForContentAndChildren(ContentReference.StartPage, OnStatusChanged);
            RebuildSoftlinksForBlocks(OnStatusChanged);

            if (_stopSignaled)
            {
                return "The job was stopped!";
            }

            var numOfLinksPostRun = ExecuteScalar("SELECT COUNT(1) FROM tblContentSoftlink");
            var numOfLinksWithPropertyPostRun = ExecuteScalar("SELECT COUNT(1) FROM tblContentSoftlink WHERE fkOwnerPropertyDefinitionID is NOT NULL");

            return $"Before: {numOfLinksPreRun} softlinks, {numOfLinksWithPropertyPreRun} with property. After: {numOfLinksPostRun} softlinks, {numOfLinksWithPropertyPostRun} with property.";
        }

        public override void Stop()
        {
            _stopSignaled = true;
        }

        private void RebuildSoftlinksForContentAndChildren(ContentReference contentLink, Action<string> statusChangeAction = null)
        {
            RebuildSoftLinks(contentLink, statusChangeAction);
        }

        private void RebuildSoftlinksForBlocks(Action<string> statusChangeAction = null)
        {
            var contentTypes = _contentTypeRepository.List();
            var contentReferences = new List<ContentReference>();
            foreach (var contentType in contentTypes)
            {
                if (_stopSignaled)
                {
                    return;
                }

                if (contentType.Base == ContentTypeBase.Block)
                {
                    var contentUsages = _contentModelUsage.ListContentOfContentType(contentType);

                    contentReferences.AddRange(contentUsages
                        .Select(x => x.ContentLink.ToReferenceWithoutVersion())
                        .Distinct());
                }
            }

            foreach (var contentLink in contentReferences)
            {
                if (_stopSignaled)
                {
                    return;
                }

                RebuildSoftLinks(contentLink, statusChangeAction, true);
            }                
        }

        private void RebuildSoftLinks(ContentReference contentLink, Action<string> statusChangeAction, bool isBlock = false)
        {
            var contentInSpesificLanguage = _contentRepository.GetLanguageBranches<IContent>(contentLink);
            if (!contentInSpesificLanguage.Any())
            {
                contentInSpesificLanguage = new[] { _contentRepository.Get<IContent>(contentLink) };
            }                

            foreach (var content in contentInSpesificLanguage.Where(x => x is PageData || x is BlockData))
            {
                if (_stopSignaled)
                {
                    return;
                }

                var langKey = "N/A";
                try
                {
                    var links = _contentSoftLinkIndexer.GetLinks(content);
                    var cultureInfo = (content as ILocalizable)?.Language;
                    langKey = cultureInfo?.TwoLetterISOLanguageName ?? langKey;
                    _contentSoftLinkRepository.Save(content.ContentLink.ToReferenceWithoutVersion(), cultureInfo, links, false);
                }
                catch (Exception)
                {
                    // Ignore
                }

                statusChangeAction?.Invoke($"Rebuilding softlinks for {content.Name}...");
            }

            if (isBlock)
            {
                return;
            }

            var children = _contentRepository.GetChildren<IContent>(contentLink, LanguageSelector.AutoDetect(true));
            foreach (var child in children)
            {
                if (_stopSignaled)
                {
                    return;
                }

                RebuildSoftLinks(child.ContentLink, statusChangeAction);
            }                
        }

        private void ExecuteCmd(string cmd)
        {
            using var conn = new SqlConnection(_configuration.GetConnectionString("EPiServerDB"));
            var sqlCmd = new SqlCommand(cmd, conn);
            conn.Open();
            sqlCmd.ExecuteNonQuery();
            conn.Close();
        }

        private int ExecuteScalar(string cmd)
        {
            using var conn = new SqlConnection(_configuration.GetConnectionString("EPiServerDB"));
            var sqlCmd = new SqlCommand(cmd, conn);
            conn.Open();
            int rval = (int)sqlCmd.ExecuteScalar();
            conn.Close();            
            return rval;
        }
    }
}

After running both jobs, the Property Name is shown.

Graphical user interface

That's it! Happy validating!