Extend your Episerver content types

Mattias Olsson 2017-02-17 00:29:04

I spent some hours researching this topic, mostly fiddling around in Episerver assemblies. I looked at ContentScannerExtension, ContentDataInterceptorHandler and IContentDataActivator to try to find ways to "fool" Episerver to use the extended content type. I have to say that they have made it hard to create own implementations of specifically IContentDataActivator. Anyway, at the end, I came up with a very simple solution that I will go through now.

First of all, I created an Attribute that I could decorate my extended content types with. This attribute is very simple and just inherits Episerver's ContentTypeAttribute. I'll explain why later.

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class ExtendedContentTypeAttribute : ContentTypeAttribute
{
}

Ok, let's say you have a content type called ArticlePage in a different assembly than your web project assembly, which looks something like this:

[ContentType(DisplayName = "Article page", GUID = "76158075-8240-4b6d-95bd-abb484ba1e6e")]
public class ArticlePage : PageData
{
    [UIHint(UIHint.Image)]
    [Required]
    public virtual ContentReference MainImage { get; set; }

    [Display(Name = "Main intro")]
    [CultureSpecific]
    public virtual string MainIntro { get; set; }

    [Display(Name = "Main body")]
    [CultureSpecific]
    public virtual XhtmlString MainBody { get; set; }

    public virtual string Author { get; set; }
}

In my web project I want to add a property to ArticlePage and also make the Author property culture specific. Let's create an extended content type!

[ExtendedContentType(GUID = "76158075-8240-4b6d-95bd-abb484ba1e6e", DisplayName = "My extended article")]
public class ExtendedArticlePage : ArticlePage
{
    [Display(Name = "My extended string property")]
    [CultureSpecific]
    public virtual string MyExtendedProperty { get; set; }

    [CultureSpecific]
    public override string Author { get; set; }
}

Two important things for this to work properly is that I assign the same Guid to the extended content type and inherit from the content type that I want to extend.

Ok, let's get back to the reason why I inherit Episerver's ContentTypeAttribute. The reason is that I want Episerver's model sync magic to register this as any other normal content type and sync it to the database, without having to create complex logic.

What we need to handle though is that Episerver throws an exception if two or more content types share the same Guid. I only want my extended content type to be synced and the base type should be ignored. I do this by creating a slightly modified implementation of IContentTypeModelScanner. Here's the code for that.

[ServiceConfiguration(typeof(IContentTypeModelScanner))]
[ServiceConfiguration(typeof(ContentTypeModelScanner))]
public class ExtendedContentTypeModelScanner : ContentTypeModelScanner
{
    private IEnumerable _extendedContentTypeBaseTypes;

    protected readonly ITypeScannerLookup TypeScannerLookup;
    protected IEnumerable<Type> ExtendedContentTypeBaseTypes
    {
        get 
        { 
            return _extendedContentTypeBaseTypes 
               ?? (_extendedContentTypeBaseTypes = GetExtendedContentTypeBaseTypes());
        }
    }

    public ExtendedContentTypeModelScanner(ITypeScannerLookup typeScannerLookup, ...)
    {
        TypeScannerLookup = typeScannerLookup;
    }

    protected virtual IEnumerable<Type> GetExtendedContentTypeBaseTypes()
    {
        var extendedContentTypeBaseTypes = TypeScannerLookup.AllTypes
            .Where(t => t.GetCustomAttributes<ExtendedContentTypeAttribute>().Any())
            .Select(t => t.BaseType)
            .Distinct();

        return extendedContentTypeBaseTypes;
    }

    public override IEnumerable<Type> IgnoredTypes 
    {
        get { return base.IgnoredTypes.Union(ExtendedContentTypeBaseTypes); }
    }
}

So, what I'm doing is basically to scan all types that has my ExtendedContentTypeAttribute and select their base types (Type.BaseType). One feature of the default IContentTypeModelScanner is that you can tell it to ignore certain types through the public and virtual IgnoredTypes property. In this example, ArticlePage is appended to the list of ignored types already defined in the base class. As a result, ExtendedArticlePage is being synced to the database instead. Sweet.

Now all article pages in my solution will be instances of ExtendedArticlePage and I can change the model in my view:

@model ExtendedArticlePage

@Html.PropertyFor(m => m.PageName)
@Html.PropertyFor(m => m.MainIntro)
@Html.PropertyFor(m => m.MainBody)
@Html.PropertyFor(m => m.Author)
@Html.PropertyFor(m => m.MyExtendedProperty)