Passing custom IContentSerializerSettings to JOS Content Serializer

We use the JOS.ContentSerializer to provide JSON for React in a platform project at my current client (7 Episerver websites spread over 5 installations). For this we needed to provide the JOS.ContentSerializer with custom settings via the IContentSerializerSettings interface to use in our custom property handlers. However, we encountered some difficulties doing this and here is how we (temporarily) solved it. I will make the changes into a PR as soon as I get the time, and if it gets accepted into the project we can remove all this.

This solution was developed together with Svante Seleborg.

Not getting IContentSerializerSettings to IPropertyHandler<T> implementation

In our code, we use the method GetStructuredData found in the IPropertyManager interface directly as we need to do other things with the structure before turning it into JSON. Our custom IPropertyHandler implementations also need custom site specific data in order to function correctly.

 public interface IPropertyManager
 {
    Dictionary<string, object> GetStructuredData(
      IContentData contentData,
      IContentSerializerSettings contentSerializerSettings);
 }

In order to achieve this we implemented our own version of the IContentSerializerSettings interface.

public interface IMyContentSerializerSettings : IContentSerializerSettings
{
  MyObject MyCustomData { get; set; }
}

public class MyContentSerializerSettings : IMyContentSerializerSettings
{
  public MyObject MyCustomData { get; set; }

  public bool WrapContentAreaItems { get; set; } = true;

  public IUrlSettings UrlSettings { get; set; } = new UrlSettings();

  public string BlockTypePropertyName { get; set; } = "__type__";
}

In our implementation we added the custom data that we needed and passed an instance of it into the JOS.ContentSerializer. The default values are copied from the original implementation and are expected by internal code to not be null.

In the custom implementation of the IPropertyHandler interface we need to get hold of this custom data. This is where the problem arises as we cannot inject the IContentSerializerSettings via dependency injection (you would get a settings object, but it would not be the correct one).

public class MyXhtmlStringPropertyHandler : IPropertyHandler<XhtmlString>
{
  public object Handle(XhtmlString value, PropertyInfo property, IContentData contentData)
  {
    // We cannot get our implementation of IContentSerializerSettings in this class.
  }

Getting your IContentSerializerSettings implementation into a property handler

The solution is to create a second method in the handler implementation called Handler2 (see Microsofts General Naming Conventions). Let this method accept a IContentSerializerSettings implementation and use it for your custom code.

The original Handle method will no longer be used and should let you know if someone tries to use it.

public class MyXhtmlStringPropertyHandler : IPropertyHandler<XhtmlString>
{
  public object Handle(XhtmlString value, PropertyInfo property, IContentData contentData)
  {
    throw new NotImplementedException("This implementation should never be used.");
  }

  public object Handle2(ContentArea contentArea, PropertyInfo propertyInfo, IContentData contentData, IContentSerializerSettings contentSerializerSettings)
  {
    IMyContentSerializerSettings settings = (IMyContentSerializerSettings) contentSerializerSettings;

In order to get the JOS.ContentSerializer to pass your custom IContentSerializerSettings implementation through to your property handler we need to do some alterations to internal code. This is where it becomes a little bit hacky.

Below we copied the code for the PropertyManager implementation from the JOS.ContentSerializer GitHub repository and made a few changes. One should be aware that doing this may cause problems if the product changes. I will paste the whole file here to make it easier to follow.

private class MyPropertyManager : IPropertyManager
{
  private readonly IPropertyResolver _propertyResolver;
  private readonly IPropertyNameStrategy _propertyNameStrategy;
  private readonly IPropertyHandlerService _propertyHandlerService;

  public MyPropertyManager(
    IPropertyNameStrategy propertyNameStrategy,
    IPropertyResolver propertyResolver,
    IPropertyHandlerService propertyHandlerService)
  {
    _propertyNameStrategy = propertyNameStrategy ?? throw new ArgumentNullException(nameof(propertyNameStrategy));
    _propertyResolver = propertyResolver ?? throw new ArgumentNullException(nameof(propertyResolver));
    _propertyHandlerService = propertyHandlerService;
}

  public Dictionary<string, object> GetStructuredData(
    IContentData contentData,
    IContentSerializerSettings settings)
  {
    var properties = this._propertyResolver.GetProperties(contentData);
    var structuredData = new Dictionary<string, object>();

    foreach (var property in properties)
    {
      var propertyHandler = this._propertyHandlerService.GetPropertyHandler(property);
      if (propertyHandler == null)
      {
        Trace.WriteLine($"No PropertyHandler was found for type '{property.PropertyType}'");
        continue;
      }

      // Changes below

      var key = this._propertyNameStrategy.GetPropertyName(property);
      var value = property.GetValue(contentData);
      var methodEx = propertyHandler.GetType().GetMethod("Handle2");
      if (methodEx != null)
      {
        var result = methodEx.Invoke(propertyHandler, new[] { value, property, contentData, settings });
        structuredData.Add(key, result);
        continue;
      }

      var method = propertyHandler.GetType().GetMethod(nameof(IPropertyHandler<object>.Handle));
      if (method != null)
      {
        var result = method.Invoke(propertyHandler, new[] { value, property, contentData });
        structuredData.Add(key, result);
      }
    }
    return structuredData;
  }
}

As you can see, we’ve added an if-statement looking for a Handle2 method to the for-loop, before the original Handle method check. This is because we want our own Handle2 to take priority over the one we know will throw an exception. The only difference between the if-statements is the parameter list. The new one also passes the settings object.

After this it’s just a matter of making StructureMap inject our new implementation instead of the original one.

context.Services.AddSingleton<IPropertyManager, MyPropertyManager>();