Different subsets of content types in different EPiServer multi sites with all types in one assembly

As I explained in my previous article Different subsets of properties in different EPiServer multi sites using same content type model my current client is building a common platform for 7 websites spread over 3 EPiServer installations.

As the platform grew the need for restructuring grew as well. It was no longer viable to keep all content types in separate assemblies in order to add only subsets of them in each website. We needed a way to keep all content types in a single assembly, while at the same time exclude or include them in the different websites.

To address this issue it is possible to extend the functionality mentioned in the article referenced above. The code below shows what we ended up with. The IncludeInSite attribute works the same way for all content types.

[ContentType(
  DisplayName = "Some test page",
  GUID = "9ACBC03A-0838-433A-B9DD-BB9BCFD53D09")]
[IncludeInSite(MultiSiteId.FirstSite | MultiSiteId.ThirdSite)]
public class TestPage : PageData
{

Prevent EPiServer from adding pagetypes and blocktypes to the database

The EPiServer content model scanner uses a type scanner lookup in order to determine which content types it should add to the database. While the interface for this, ITypeScannerLookup is public, the implementation that EPiServer uses is marked as internal. Normally I would just extend the original implementation and replace it in the container with our own, but now we need to do it slightly different.

We implement EPiServer’s public interface just like the original implementation does, and register it in the StructureMap container.

For<ITypeScannerLookup>().Use<CustomTypeScannerLookup>();

Since we cannot access internal classes, constructor injection is out of the question. We create a property called Inner to represent the base implementation in our own class.

In order to get hold of EPiServer’s implementation, we will use the ServiceLocator to retrieve all instances of the public interface form the StructureMap container. Then it’s just a matter of selecting the one that we want.

CustomTypeScannerLookup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using EPiServer.Framework.TypeScanner;
using EPiServer.ServiceLocation;

namespace Platform.Core.Infrastructure.ContentManagement.Scanning
{
  public class CustomTypeScannerLookup : ITypeScannerLookup
  {
    private ITypeScannerLookup _inner;
    private ITypeScannerLookup Inner
    {
      get
      {
        if (_inner == null)
        {
          // Unable to constructor inject due to EPiServer's TypeScannerLookup being internal.
          // This will allow for additions earlier in the chain, but not after our own scanner.
          var implementations = ServiceLocator.Current.GetAllInstances<ITypeScannerLookup>();
          _inner = implementations.Last(i => i.GetType() != typeof(CustomTypeScannerLookup));
        }
        return _inner;
      }
    }

    public IEnumerable<Type> AllTypes
    {
      get
      {
        var types = Inner.AllTypes;
        var siteId = (MultiSiteId) Properties.Settings.Default.MultiSiteId;

        foreach (var type in types)
        {
          if (ShouldIgnoreType(type, siteId))
          {
            continue;
          }
          yield return type;
        }
      }
    }

    private static bool ShouldIgnoreType(Type type, MultiSiteId siteId)
    {
      var attribute = type.GetCustomAttribute<IncludeInSiteAttribute>(inherit: true);
      return attribute != null && !attribute.ShouldIncludeIn(siteId);
    }
  }
}

The AllTypes property in our own implementation of ITypeScannerLookup will first get all types from the base (Inner.AllTypes) and then apply the attribute filter on them. This will filter out any unwanted types.

StructureMap’s Use<> will automatically set the default implementation of the interface to our implementation. This happens because we make sure that our own StructureMap setup happens after EPiServer’s container initialization. The default implementation will be the one that was registered last.

StructureMapInitializer.cs

[InitializableModule]
[ModuleDependency(typeof(ServiceContainerInitialization), ...)]
public class StructureMapInitializer : IConfigurableModule
{

The rest of the setup is the same as in the article mentioned at the top. Note that it will not be possible to add extentions after this one in the chain. If that is needed, you will have to change the way that you locate the base implementation.