Removing all references to some content in Episerver

There are a reasonable number of scenarios where you may wish to want to remove all references to a piece of content in Episerver (maybe it’s expired, you’re performing a migration or you just like screwing with editors).

The following scheduled job is something I threw together to remove all references to a specific content type, however, I’m sure it can easily be repurposed hence why I’m sharing it as is:

[ScheduledPlugIn(DisplayName = "Remove all references to a Content Type", GUID = "BED51C18-7DC9-4977-B014-7D6E77483D78")]
public class RemoveReferencesJob : ScheduledJobBase
{
    private readonly IContentTypeRepository _contentTypeRepository;
    private readonly IContentModelUsage _contentModelUsage;
    private readonly IContentRepository _contentRepository;

    private readonly ILogger _logger = LogManager.GetLogger();

    private static readonly object SyncObject = new object();

    private int _handledContentAreas;
    private int _handledContentLinks;
    private int _handledXhtmlStrings;
    private int _handledLinkItemCollections;

    public RemoveReferencesJob(IContentTypeRepository contentTypeRepository, IContentModelUsage contentModelUsage,
        IContentRepository contentRepository)
    {
        _contentTypeRepository = contentTypeRepository;
        _contentModelUsage = contentModelUsage;
        _contentRepository = contentRepository;

        IsStoppable = false;
    }

    public override string Execute()
    {
        OnStatusChanged($"Starting execution of {GetType()}");

        var lockTaken = false;

        Monitor.TryEnter(SyncObject, ref lockTaken);

        if (!lockTaken)
        {
            return "Job already running.";
        }

        _handledContentAreas = 0;
        _handledContentLinks = 0;
        _handledXhtmlStrings = 0;
        _handledLinkItemCollections = 0;

        var contentType = _contentTypeRepository.Load<StandardPage>();
        var contentUsages = _contentModelUsage.ListContentOfContentType(contentType);

        var uniqueUsages = contentUsages.Select(x => x.ContentLink.ToReferenceWithoutVersion()).Distinct();

        foreach (var contentLink in uniqueUsages)
        {
            if (!_contentRepository.TryGet(contentLink, out IContent content))
            {
                continue;
            }

            var referenceInfos = _contentRepository.GetReferencesToContent(contentLink, true);

            foreach (var referenceInfo in referenceInfos)
            {
                if (!_contentRepository.TryGet(referenceInfo.OwnerID, referenceInfo.OwnerLanguage, out ContentData owner))
                {
                    continue;
                }

                owner = (ContentData) owner.CreateWritableClone();

                if (owner == null)
                {
                    continue;
                }

                var updated = false;

                foreach (var property in owner.Property.Where(x => !x.IsMetaData))
                {
                    if (property.PropertyValueType == typeof(ContentArea))
                    {
                        updated = updated | TryUpdateContentArea(content, property);
                    }

                    if (typeof(ContentReference).IsAssignableFrom(property.PropertyValueType))
                    {
                        updated = updated | TryUpdateContentReference(content, property);
                    }

                    if (property.PropertyValueType == typeof(XhtmlString))
                    {
                        updated = updated | TryUpdateXhtmlString(content, property);
                    }

                    if (property.PropertyValueType == typeof(LinkItemCollection))
                    {
                        updated = updated | TryUpdateLinkItemCollection(content, property);
                    }
                }

                if (!updated)
                {
                    continue;
                }

                // Saving first is necessary otherwise softlinks for content areas are not updated
                // (for a reason I couldn't quite determine!).
                _contentRepository.Save((IContent)owner, SaveAction.Default | SaveAction.SkipValidation, AccessLevel.NoAccess);

                _contentRepository.Save((IContent)owner, SaveAction.Publish | SaveAction.SkipValidation, AccessLevel.NoAccess);
            }
        }

        Monitor.Exit(SyncObject);

        return
            $"Success! Removed references from {_handledContentAreas} content area(s), {_handledContentLinks} content/page reference(s), {_handledXhtmlStrings} XHTML string(s), and {_handledLinkItemCollections} link item collection(s). 👌";
    }

    private bool TryUpdateContentArea(IContent content, PropertyData property)
    {
        var contentArea = property.Value as ContentArea;

        if (contentArea?.Items == null)
        {
            return false;
        }

        var items = contentArea.Items.Where(x => x.ContentLink.CompareToIgnoreWorkID(content.ContentLink)).ToList();

        if (!items.Any())
        {
            return false;
        }

        foreach (var item in items)
        {
            contentArea.Items.Remove(item);
        }

        property.Clear();
        property.Value = contentArea;

        _handledContentAreas++;
        _logger.Information(
            $"Removed reference to content with ID {content.ContentLink.ID} in content area '{property.Name}' on {property.Parent.OwnerLink.ID}");
        return true;
    }

    private bool TryUpdateContentReference(IContent content, PropertyData property)
    {
        if (!(property.Value is ContentReference contentReference))
        {
            return false;
        }

        if (!ContentReferenceComparer.IgnoreVersion.Equals(content.ContentLink, contentReference))
        {
            return false;
        }

        property.Clear();

        _handledContentLinks++;
        _logger.Information(
            $"Removed reference to content with ID {content.ContentLink.ID} in content reference '{property.Name}' on {property.Parent.OwnerLink.ID}");
        return true;
    }

    private bool TryUpdateXhtmlString(IContent content, PropertyData property)
    {
        var xhtmlString = property.Value as XhtmlString;

        if (xhtmlString?.Fragments == null)
        {
            return false;
        }

        var contentFragments = xhtmlString.Fragments.OfType<ContentFragment>().Where(x => x.ContentLink.CompareToIgnoreWorkID(content.ContentLink)).ToList();
        var urlFragments = xhtmlString.Fragments.OfType<UrlFragment>().Where(x => x.ReferencedPermanentLinkIds.Contains(content.ContentGuid)).ToList();

        if (!contentFragments.Any() && !urlFragments.Any())
        {
            return false;
        }

        foreach (var contentFragment in contentFragments)
        {
            xhtmlString.Fragments.Remove(contentFragment);
        }

        foreach (var urlFragment in urlFragments)
        {
            xhtmlString.Fragments.Remove(urlFragment);
        }

        xhtmlString.IsModified = true;

        property.Clear();
        property.Value = xhtmlString;

        _handledXhtmlStrings++;
        _logger.Information(
            $"Removed reference to content with ID {content.ContentLink.ID} in XHTML string '{property.Name}' on {property.Parent.OwnerLink.ID}");
        return true;
    }

    private bool TryUpdateLinkItemCollection(IContent content, PropertyData property)
    {
        if (!(property.Value is LinkItemCollection linkItemCollection))
        {
            return false;
        }

        var linkItems = linkItemCollection.Where(x => x.ReferencedPermanentLinkIds.Contains(content.ContentGuid)).ToList();

        if (!linkItems.Any())
        {
            return false;
        }

        foreach (var linkItem in linkItems)
        {
            linkItemCollection.Remove(linkItem);
        }

        property.Clear();
        property.Value = linkItemCollection;

        _handledLinkItemCollections++;
        _logger.Information(
            $"Removed reference to content with ID {content.ContentLink.ID} in link item collection '{property.Name}' on {property.Parent.OwnerLink.ID}");
        return true;
    }
}

That feels like a lot of code when you scroll by it, but I think overall, it’s self-explanatory. Having said that, here is a brief explanation of what it does:

  1. It gets all references to the content, using the IContentRepository GetReferencesToContent method.
  2. It then iterates over all properties (that aren’t metadata), identifies the property type and if it’s a ContentArea, ContentReference (or PageReference), XhtmlString or LinkItemCollection it tries to remove any references.
  3. If there were any changes to the content, it’s then saved.

Two minor things I want to call out:

  1. Interestingly (maybe!) if you remove an item from a ContentArea and publish, the softlinks aren’t updated. This means it keeps being returned by GetReferencesToContent, although the reference is removed. I began to investigate why this is, but it wasn’t obvious. The DefaultContentProviderDatabase has some logic to determine whether softlinks should be updated, but it appears that it is meeting the criteria. It of course works when an editor edits it, so I inferred that saving first would solve it, which it did.
  2. You could easily remove the ContentArea-specific method handling since they are just XhtmlStrings, I left it for reference.

Anyway, enough waffle, feel free to purloin, pinch or pilfer whatever you need!

 

Comments

There are zero comments 😢