Converting IContent to IPublishedContent in Umbraco v8+

12 min read

Within Umbraco, content can be accessed in two possible ways, either via the ContentService which returns IContent or via the ContentCache which returns IPublishedContent. The main difference with these is that IContent comes straight from the database, and all of it's properties are stored in a raw format, where as IPublishedContent comes from a cache layer with all it's properties stored in a more accessible and strongly typed manor.

Generally speaking, when working with content in Umbraco, if you are reading / rendering content, you'll want to use IPublishedContent from the ContentCache, and if you are creating / updating content you'll want to use IContent and the ContentService.

TL;DR: Just show me the solution!

The Problem

There are times however when content may not yet be available in the ContentCache, such as in a content Saving/Saved event handler, and so it would be great if, as a last resort, we could fallback to fetching the IContent version from the database and use that instead.

The issue with this however is that IContent and IPublishedContent are very different interfaces especially around content property access, where with IContent you are generally dealing with a raw format, often JSON instead of the friendlier strongly typed access you get from IPublishedContent.

In these situations you need to do one of two things, either create code that works in two different ways depending on the model you are working with, but this often requires you to have insider knowledge as to how the raw properties are formatted, or, we can implement some kind of wrapper to convert the IContent interface into IPublishedContent and have all the contents properties converted to their strongly typed implementation in the same way that Umbraco does internally. The benefit of this approach is that we only have to write code that deals with one particular interface and we don't have to worry about the property raw data formats.

This then is the approach I decided to take.

In an ideal world, it would be great if Umbraco had an API that could do this for you out of the box, after all, it must be doing this already when it converts the IContent to IPublishedContent in order to add it to the ContentCache. Unfortunately however, this is not the case (not publicly anyway), and so we must go about implementing a solution ourselves.

The Research

Strike 1

Before taking a stab at this problem myself I thought I'd do a quick search to see if the problem had already been solved.

The first promising result I came across was a forum post on the Umbraco Developer Portal (Our), where the accepted answer points to a Gist by Jeroen Breuer in which he creates an IContent extension method that wraps the IContent instance in a custom IPublishedContent implementation that does all the relevant conversions.

PERFECT!

Not so fast!

Under closer inspection it appeared that it only seemed to work for Umbraco v7 installs and I needed a solution for Umbraco v8+ so unfortunately it wasn't a goer.

Strike 2

As my search continued, I noticed that David Peck had recently tweeted asking the very same question.

Umbraco Twitter: does anyone have any idea how I can convert IContent in to IPublishedContent in ContentService.Saving? I want to trigger all the PropertyValueConverters.

Could this be the answer?

Nope again!

In the thread, David was basically discouraged from doing this, and really I can understand why as the whole point of the caching layer is to ensure the site stays performant and using IContent instead of IPublishedContent is one sure fire way of bringing a site to a halt if used improperly.

Regardless, if used properly, I still think being able convert IContent to IPublishedContent is a useful feature in certain circumstances, so I continued with my quest.

At this point I started to poke around the Umbraco source code trying to find where it does it's own conversion, and that's when I came across the PublishedContentHashtableConverter and specifically it's PagePublishedContent implementation

https://github.com/umbraco/Umbraco-CMS/blob/0bd4dced0b3e9205660114406b7e814f817179c7/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs#L181

The more I looked at it, the more I thought "This looks pretty close to Jeroen's code. Could I just update what Jeroen had and make it work for Umbraco v8? 🤔"

And so, I DID 🎉

The Solution

What you'll find below then is essentially a cross between the code from Jeroen's original Gist and elements from the PagePublishedContent in order to bring it inline with the Umbraco v8 API.

It may look like a lot of code, but ultimately all it's doing is taking an IContent element, and wrapping it in an IPublishedContent custom implementation that intercepts all of it's field / method requests and runs the same code Umbraco does during a conversion including property value conversion.

It does this in a Lazy way too meaning it should only run the expensive code if you ask for the given property, and once it's run, it remembers the result so it won't need to run the same code again.

using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Strings;
using Umbraco.Web.Composing;

namespace MyNamespace.Web
{
   public static class ContentExtensions
    {
        public static IPublishedContent ToPublishedContent(this IContent content, bool isPreview = false)
        {
            return new PublishedContentWrapper(content, isPreview);
        }

        #region PublishedContentWrapper

        private class PublishedContentWrapper : IPublishedContent
        {
            private static readonly IReadOnlyDictionary<string, PublishedCultureInfo> NoCultureInfos = new Dictionary<string, PublishedCultureInfo>();

            private readonly IContent _inner;
            private readonly bool _isPreviewing;

            private readonly Lazy<string> _creatorName;
            private readonly Lazy<string> _writerName;
            private readonly Lazy<IPublishedContentType> _contentType;
            private readonly Lazy<IPublishedProperty[]> _properties;
            private readonly Lazy<IPublishedContent> _parent;
            private readonly Lazy<IEnumerable<IPublishedContent>> _children;
            private readonly Lazy<IReadOnlyDictionary<string, PublishedCultureInfo>> _cultureInfos;

            public PublishedContentWrapper(IContent inner, bool isPreviewing)
            {
                _inner = inner ?? throw new NullReferenceException("inner");
                _isPreviewing = isPreviewing;

                _creatorName = new Lazy<string>(() => _inner.GetCreatorProfile()?.Name);
                _writerName = new Lazy<string>(() => _inner.GetWriterProfile()?.Name);

                _contentType = new Lazy<IPublishedContentType>(() =>
                {
                    var ct = Current.Services.ContentTypeBaseServices.GetContentTypeOf(_inner);
                    return Current.PublishedContentTypeFactory.CreateContentType(ct);
                });

                _properties = new Lazy<IPublishedProperty[]>(() => ContentType.PropertyTypes
                    .Select(x =>
                    {
                        var p = _inner.Properties.SingleOrDefault(xx => xx.Alias == x.Alias);
                        return new PublishedPropertyWrapper(this, x, p, _isPreviewing);
                    })
                    .Cast<IPublishedProperty>()
                    .ToArray());

                _parent = new Lazy<IPublishedContent>(() =>
                {
                    return Current.Services.ContentService.GetById(_inner.ParentId)?.ToPublishedContent(_isPreviewing);
                });

                _children = new Lazy<IEnumerable<IPublishedContent>>(() =>
                {
                    var c = Current.Services.ContentService.GetPagedChildren(_inner.Id, 0, 2000000000, out var totalRecords);
                    return c.Select(x => x.ToPublishedContent(_isPreviewing)).OrderBy(x => x.SortOrder);
                });

                _cultureInfos = new Lazy<IReadOnlyDictionary<string, PublishedCultureInfo>>(() =>
                {
                    if (!_inner.ContentType.VariesByCulture())
                        return NoCultureInfos;

                    return _inner.PublishCultureInfos.Values
                        .ToDictionary(x => x.Culture, x => new PublishedCultureInfo(x.Culture,
                            x.Name,
                            GetUrlSegment(x.Culture),
                            x.Date));
                });
            }

            public IPublishedContentType ContentType 
                => _contentType.Value;

            public int Id 
                => _inner.Id;

            public Guid Key 
                => _inner.Key;

            public int? TemplateId 
                => _inner.TemplateId;

            public int SortOrder 
                => _inner.SortOrder;

            public string Name 
                => _inner.Name;

            public IReadOnlyDictionary<string, PublishedCultureInfo> Cultures 
                => _cultureInfos.Value;

            public string UrlSegment 
                => GetUrlSegment();

            public string WriterName 
                => _writerName.Value;

            public string CreatorName 
                => _creatorName.Value;

            public int WriterId 
                => _inner.WriterId;

            public int CreatorId 
                => _inner.CreatorId;

            public string Path 
                => _inner.Path;

            public DateTime CreateDate 
                => _inner.CreateDate;

            public DateTime UpdateDate 
                => _inner.UpdateDate;

            public int Level 
                => _inner.Level;

            public string Url 
                => null; // TODO: Implement?

            public PublishedItemType ItemType 
                => PublishedItemType.Content;

            public bool IsDraft(string culture = null)
                => !IsPublished(culture);

            public bool IsPublished(string culture = null)
                => _inner.IsCulturePublished(culture);

            public IPublishedContent Parent 
                => _parent.Value;

            public IEnumerable<IPublishedContent> Children 
                => _children.Value; // TODO: Filter by current culture?

            public IEnumerable<IPublishedContent> ChildrenForAllCultures 
                => _children.Value;

            public IEnumerable<IPublishedProperty> Properties 
                => _properties.Value;

            public IPublishedProperty GetProperty(string alias)
                => _properties.Value.FirstOrDefault(x => x.Alias.InvariantEquals(alias));

            private string GetUrlSegment(string culture = null)
            {
                var urlSegmentProviders = Current.UrlSegmentProviders;
                var url = urlSegmentProviders.Select(p => p.GetUrlSegment(_inner, culture)).FirstOrDefault(u => u != null);
                url = url ?? new DefaultUrlSegmentProvider().GetUrlSegment(_inner, culture); // be safe
                return url;
            }
        }

        private class PublishedPropertyWrapper : IPublishedProperty
        {
            private readonly object _sourceValue;
            private readonly IPublishedContent _content;
            private readonly bool _isPreviewing;

            public PublishedPropertyWrapper(IPublishedContent content, IPublishedPropertyType propertyType, Property property, bool isPreviewing)
                : this(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored
            {
                _sourceValue = property?.GetValue();
                _content = content;
                _isPreviewing = isPreviewing;
            }

            protected PublishedPropertyWrapper(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel)
            {
                PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
                ReferenceCacheLevel = referenceCacheLevel;
            }

            public IPublishedPropertyType PropertyType { get; }
            public PropertyCacheLevel ReferenceCacheLevel { get; }
            public string Alias => PropertyType.Alias;

            public bool HasValue(string culture = null, string segment = null)
            {
                return _sourceValue != null && ((_sourceValue is string) == false || string.IsNullOrWhiteSpace((string)_sourceValue) == false);
            }

            public object GetSourceValue(string culture = null, string segment = null)
            {
                return _sourceValue;
            }

            public object GetValue(string culture = null, string segment = null)
            {
                var source = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing);

                return PropertyType.ConvertInterToObject(_content, PropertyCacheLevel.Unknown, source, _isPreviewing);
            }

            public object GetXPathValue(string culture = null, string segment = null)
            {
                var source = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing);

                return PropertyType.ConvertInterToXPath(_content, PropertyCacheLevel.Unknown, source, _isPreviewing);
            }
        }


        #endregion
    }
}

For me this is exactly what I needed and given I knew at least one person was looking for an answer to the same problem, I thought I might as well throw it up on a blog post, and so here it is.

I hope this helps

🚨 DISCLAIMER 🚨

I mentioned it a little previously, but working with IContent is not the most performant way to work with Umbraco content, so if you do use this method, be sure to know what you are doing.

I won't be held responsible for any problems you introduce into your solution by using this. Use this code at your own risk.