DEV Community

Cover image for Fixing broken Nested Content after upgrading from Umbraco 7 to 8
Søren Kottal
Søren Kottal

Posted on

Fixing broken Nested Content after upgrading from Umbraco 7 to 8

Some time ago, I upgraded another project from Umbraco 7 to 8. This is a process I've been getting more and more used to. In this project, after upgrading we kept seeing weird bugs on the frontend, where Nested Content properties kept pulling the wrong content.

To set the stage, the site was built with a "Global Texts Folder" containing pages with Nested Content values, including the global texts needed in various parts of the website.

The site was also built in two languages, by the approach used before Vorto or the multilingual feature in Umbraco 8 by simply having two trees of content. One for Danish, and one for English.

So, the bugs we were seeing the most, was that the Danish version of a page, was showing the English version of the global text selected - and sometimes the opposite.

Every time, we could fix it by republishing the erring global text node. I thought something was simply corrupted in the cache upon the data migration. But then the same pages started acting up again. So, what could it be?

One day another problem was noticed. A block in the nested content property was duplicated on one page, and another block from the same page was missing. I remembered having a similar problem with Doc Type Grid Editor earlier, where the problem was a duplicated key in the content items. And there I had my problem.

Why Nested Content can be a PITA when upgrading from Umbraco 7

A little back story. Nested Content contains a hidden key, which is a unique Guid for every content item. Understandable, the editors had copied global text nodes from one language to another while building in Umbraco 7, so they didn't have to recreate every Nested Content item from scratch.

Umbraco 7 didn't reset these keys, when pages with Nested Content items were copied. So, we now had duplicates of these keys scattered around the website.

So, I started looking at the values of the language "variants" of some of the global text nodes - and yes, duplicate keys!

One way to fix this, is to copy all nested content items and paste them in same place through the back office. While this works, it's boring, tedious, and prone to errors - all at the same time.

So, I did a quick script to update all the global text nodes.

Get the problematic content types and properties

I wanted the script to be quite generic, as I will probably run into this problem again later, so to start, I query Umbraco for any content types that has Nested Content properties in them. I do this using the ContentTypeService with the following code:

private List<ContentTypeWithNestedContent> GetContentTypesAndTheirNestedContentPropertyAliases()
{
    var result = new List<ContentTypeWithNestedContent>();
    var contentTypes = Services.ContentTypeService.GetAll();
    foreach (var contentType in contentTypes)
    {
        var nestedContentPropertyTypes = contentType.CompositionPropertyTypes.Where(x => x.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent)).ToList();
        if (nestedContentPropertyTypes.Any())
        {
            result.Add(new ContentTypeWithNestedContent()
            {
                Id = contentType.Id,
                Alias = contentType.Alias,
                NestedContentPropertyAliases = nestedContentPropertyTypes.Select(x => x.Alias).ToList()
            });
        }
    }

    return result;
}
Enter fullscreen mode Exit fullscreen mode

This returns a list of objects, containg the id of the content type, the alias, and a list of aliases of the properties using Nested Content.

I use this method, to save a list of content types in memory for later use.

Find and fix potential problematic content nodes

With the content types found and ready, I then need to find all content using those types.

public void FindProblematicContentAndFixIt() {
    // in my constructor:
    // _contentTypes = GetContentTypesAndTheirNestedContentPropertyAliases();

    var contentTypeIds = _contentTypes.Select(x => x.Id);
    foreach (var contentTypeId in contentTypeIds)
    {
        var nodes = Services.ContentService.GetPagedOfType(contentTypeId, 0, int.MaxValue, out long totalNodes, null, null);
        foreach (var node in nodes)
        {
            ResetKeys(node);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This uses the before mentioned GetContentTypesAndTheirNestedContentPropertyAliases method, to search for content based on the found content types. For each content node found, it's passed into a new method, which will reset the keys of each nested content item.

private void ResetKeys(IContent node)
{
    var aliases = GetNestedContentPropertyAliasesByContentTypeAlias(node.ContentType.Alias);

    foreach (var alias in aliases)
    {
        if (node.HasProperty(alias))
        {
            var input = node.GetValue<string>(alias);
            var output = ResetKeys(input);

            if (input != output)
            {
                node.SetValue(alias, output);

            }
        }
    }

    if (node.IsDirty())
    {
        if (node.Published)
        {
            Services.ContentService.SaveAndPublish(node);
        }
        else
        {
            Services.ContentService.Save(node);
        }
    }
}

private List<string> GetNestedContentPropertyAliasesByContentTypeAlias(string contentTypeAlias)
{
    // in my constructor:
    // _contentTypes = GetContentTypesAndTheirNestedContentPropertyAliases();

    if (contentTypeAlias == null) return new List<string>();

    var contentType = _contentTypes.FirstOrDefault(x => x.Alias == contentTypeAlias);
    if (contentType != null)
    {
        return contentType.NestedContentPropertyAliases;
    }
    return new List<string>();
}
Enter fullscreen mode Exit fullscreen mode

So, in here, we first need to know which property aliases to update, so first, we get the aliases by calling another method, GetNestedContentPropertyAliasesByContentTypeAlias.

In this case, I set up all my code in an API Controller, so in the constructor, I saved the output of the initial GetContentTypesAndTheirNestedContentPropertyAliases, so I wouldn't have to look all that up again or pass it through to every method.

I can then look through my content type collection, to find the current type's property aliases.

With the aliases ready, I then get the values of the properties, and run that through another ResetKeys method.

Handling Nested Nested Content too

Since Nested Content can nest content types with their own Nested Content values, I needed a recursive way of resetting keys.

private string ResetKeys(string input)
{
    // get the nested content value, deserialize the string
    var nestedItems = JsonConvert.DeserializeObject<List<Dictionary<string, string>>>(input);

    // only do anything if there actually is nested content items inside
    if (nestedItems != null && nestedItems.Count > 0)
    {
        // loop through the nested items, resetting the key
        nestedItems.ForEach(item =>
        {
            item["key"] = Guid.NewGuid().ToString();

            var aliases = GetNestedContentPropertyAliasesByContentTypeAlias(item["ncContentTypeAlias"]);

            foreach (var alias in aliases)
            {
                if (item.ContainsKey(alias))
                {
                    item[alias] = ResetKeys(item[alias]);
                }
            }
        });

        return JsonConvert.SerializeObject(nestedItems);
    }

    return input;
}
Enter fullscreen mode Exit fullscreen mode

So, in here, I first deserialize the input string as the expected Dictionary<string, string> that Nested Content stores. I then loop through each item, resetting the key property on each. And because the content type of the item, can have its own Nested Content properties, I get the possible aliases, and reset the keys once again for the values in there.

In the end, the items are serialized back to a string and returned to the original ResetKeys method.

Only save or published if you need to

Because I don't want to mess more than necessary with my content, I check if the content node now is dirty before I save or save and publish it.

    // from ResetKeys(IContent node) further up

    if (node.IsDirty())
    {
        if (node.Published)
        {
            Services.ContentService.SaveAndPublish(node);
        }
        else
        {
            Services.ContentService.Save(node);
        }
    }
Enter fullscreen mode Exit fullscreen mode

And after running this script - I no longer had any problems with content suddenly duplicating here and there on the site. This is going in the toolbelt for future migrations.

If you are interested in the complete controller, I've added it in a gist here.

Discussion (0)