EPiServer Find UnifiedSearch conventions causing ReflectionTypeLoadException to be thrown

At my current client’s we are building a large platform serving several EPiServer multisite installations with common code. Consolidating how we work with EPiServer Find, we ran into a bit of trouble. My collegue Svante Seleborg did all of the heavy lifting on this one (I’m just taking the credit).

While attempting to add simple UnifiedSearchRegistry conventions, as the kind shown in below code snippet, we ran into a wall in form of the ReflectionTypeLoadException.

SearchClient.Instance.Conventions.UnifiedSearchRegistry
  .ForInstanceOf<IIndexableObject>()
  .ProjectUrlFrom<IIndexableObject>(x => x.Url);

IIndexableObject is an interface implemented by custom classes that we want to be able to index with EPiServer Find.

The ReflectionTypeLoadException thrown when ForInstanceOf<T>

The trouble begins when the EPiServer Find code attempts to scan the assemblies in order to find all concrete instances of our interface IIndexableObject. Since the GetTypes method requires that all of the transitive dependencies are loadable, there will be ReflectionTypeLoadExceptions when they are not (in other words, whenever you have an assembly loaded that contains types referring to other assemblies that are not available).

There are plenty of real world examples of this. We happened to run into it due to an interface (System.IdentityMode.Tokens.ISecurityTokenValidator) missing from System.IdentityModel.Tokens.Jwt assembly (which was not supposed to be there in the first place, but that’s another article).

As my collegue Svante pointed out, other examples are several assemblies in the Roslyn directory, as well as if your project has dead NuGet references that are never actually used (but in turn contains such types).

Workaround until the issue gets addressed

Even though it is difficult, it is desirable to avoid these kinds of exceptions here. The workaround for this issue, while waiting for a fix, is to only allow types that are actually available.

public static class WorkaroundExtensions
{
  public static UnifiedSearchTypeRulesBuilder<T> ForInstanceOfWorkaroundReflectionTypeLoadException<T>(this IUnifiedSearchRegistry registry)
  {
    Type type = typeof(T);
    foreach (Type item in (from x in AppDomain.CurrentDomain.GetAssemblies()
      where !x.IsDynamicAssembly()
      select x).SelectMany((Assembly x) => GetTypes(x)).Where(delegate (Type x)
      {
        if (type.IsAssignableFrom(x))
        {
          return !registry.Contains(x);
        }
        return false;
      }))
    {
      registry.Add(item);
    }
    return new UnifiedSearchTypeRulesBuilder<T>(registry);
  }

  private static Type[] GetTypes(Assembly a)
  {
    Type[] types;
    try
    {
      types = a.GetTypes();
    }
    catch (ReflectionTypeLoadException e)
    {
      types = e.Types;
    }
    return types;
  }
}

As you can see in the above code, we let the exception be thrown but handles it by not going any deeper into the recursion.