Different subsets of properties in different EPiServer multi sites using same content type model

For my current client we are building 7 websites spread over (at least) 3 EPiServer multi site installations. Rather than seeing each of them as separate entities, they all consume the same base code from a common platform (such as infrastructure, EPiServer content types, and so on).

A code platform with three websites using its common code.

An issue that you’re facing when using the same block or page type on several EPiServer installations is that you would need to override it in the website’s code if you wanted to add or remove properties. This would result in a new content type and feels like bad design.

Adding or removing EPiServer properties without creating a new content type class

The headline above both is and is not true. While all the properties will exist in the code on the shared content type, they will not be added to all EPiServer databases.

Consider that you have three EPiServer multi site installations as in the image at the top of this article. Your content type class lives in the code Platform. You want to add your property Preamble to the websites FirstSite and ThirdSite, but not to the one called SecondSite. All you need to do is decorate your property with the IncludeInSiteAttribute as shown below.

[Display(Name = "Preamble")]
[UIHint(UIHint.Textarea)]
[IncludeInSite(MultiSiteId.FirstSite | MultiSiteId.ThirdSite)]
public virtual string Preamble { get; set; }

This will cause our EPiServer installations to only insert the property into the database if it has a matching multi site ID. Of course, properties decorated with EPiServer’s own IgnoreAttribute will still be ignored, and properties not decorated with either one will be inserted as usual.

How to conditionally prevent EPiServer from adding properties to the database

So how does this work? First off, we will need the MultiSiteId enum decorated with the FlagsAttribute from the System assembly.

MultiSiteId.cs

[Flags]
public enum MultiSiteId
{
  None = 0,
  FirstSite = 1,
  SecondSite = 2,
  ThirdSite = 4,
}

Next, the attribute itself. There really isn’t anything special about it, just an attribute taking flags in the constructor together with a small method.

IncludeInSiteAttribute.cs

[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public class IncludeInSiteAttribute : Attribute
{
  private readonly MultiSiteId _siteIds;

  /// <summary>
  /// Used to include this EPiServer property only in a subset of websites.
  /// </summary>
  /// <param name="siteIds">IDs of multisites that should have this property.</param>
  public IncludeInSiteAttribute(MultiSiteId siteIds)
  {
    _siteIds = siteIds;
  }

  public bool ShouldIncludeIn(MultiSiteId siteId)
  {
    return _siteIds.HasFlag(siteId);
  }
}

When EPiServer scans through all the content type models that it finds in your source code, it loops through each property using reflection. This scanner uses a series of filters to determine which of each model’s properties that are to be inserted into the EPiServer database (the commonly used IgnoreAttribute is one of these filters.

There is a class in the DataAbstraction namespace called ContentScannerExtension. The EPiServer model scanner will look for all implementations inheriting from this class, and then execute the ShouldIgnoreProperty method for each one.

In short, all we need to do is override this method to add our own filter logic, see below. An easy way to register this in the StructureMap container is by decorating the extension class with the ServiceConfiguration attribute.

using System.Reflection;
using EPiServer.DataAbstraction.RuntimeModel;
using EPiServer.ServiceLocation;

namespace Platform.Core.Infrastructure.PropertyManagement.Scanning
{
  [ServiceConfiguration(typeof(ContentScannerExtension))]
  public class CustomContentScannerExtension : ContentScannerExtension
  {
    public override bool ShouldIgnoreProperty(ContentTypeModel contentTypeModel, PropertyInfo property)
    {
      var siteId = (MultiSiteId) Properties.Settings.Default.MultiSiteId;
      var attribute = property.GetCustomAttribute<IncludeInSiteAttribute>(inherit: true);
      return attribute != null && !attribute.ShouldIncludeIn(siteId);
    }
  }
}

If you disassemble EPiServer’s DataAbstraction assembly you will find that this class also has a lot of other things that you may fiddle with, none of which are needed for this functionality however.

SecondSite’s web.config

  <applicationSettings>
    <Platform.Core.Properties.Settings>
      <setting name="MultiSiteId" serializeAs="String">
        <value>2</value>
      </setting>
    </Platform.Core.Properties.Settings>
  </applicationSettings>

The last thing that you need to do is add a property setting in the platform, and then use the configuration to tell the filter about the current website.