I finally had some time to fix a long standing bug in JOS.ContentSerializer.

The bug

If you passed in a custom IContentSerializerSettings to the Serialize or GetStructuredData method on IContentSerializer, it was simply ignored... :)

The default implementation of IPropertyManager did not pass on the IContentSerializerSettings to the IPropertyHandlers. Instead, the default settings registered in DI was used.

PropertyManager.cs

public Dictionary<string, object> GetStructuredData(
    IContentData contentData,
    IContentSerializerSettings settings) // As you can see, settings is not used at all...
{
    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;
        }

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

That's not weird though, because the Handle method didn't have a IContentSerializerSetting parameter.
IPropertyHandler<>

public interface IPropertyHandler<in T>
{
    object Handle(T value, PropertyInfo property, IContentData contentData);
}

The fix

IPropertyHandler<>

public interface IPropertyHandler<in T>
{
    object Handle(T value, PropertyInfo property, IContentData contentData);
    object Handle(T value, PropertyInfo property, IContentData contentData, IContentSerializerSettings settings);
}

PropertyManager.cs

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;
        }

        // I've now refactored this so that I cache the methods as well
        var method = GetMethodInfo(propertyHandler);
        var key = this._propertyNameStrategy.GetPropertyName(property);
        var value = property.GetValue(contentData);
        // We are now using the new overload on IPropertyHandler that takes a IContentSerializerSettings
        var result = method.Invoke(propertyHandler, new[] { value, property, contentData, settings });
        structuredData.Add(key, result);
    }
    return structuredData;
}

private static MethodInfo GetMethodInfo(object propertyHandler)
{
    var type = propertyHandler.GetType();
    if (CachedHandleMethodInfos.ContainsKey(type))
    {
        CachedHandleMethodInfos.TryGetValue(type, out var cachedMethod);
        return cachedMethod;
    }

    var method = propertyHandler.GetType().GetMethods()
        .Where(x => x.Name.Equals(nameof(IPropertyHandler<object>.Handle)))
        .OrderByDescending(x => x.GetParameters().Length)
        .First();

    CachedHandleMethodInfos.TryAdd(type, method);
    return method;
}

The breaking change

Since I've introduced a new method on the IPropertyHandler<> interface this is a breaking change. If you have any custom property handlers you will need to implement the new method as well. Also, remember that the default implementation of IPropertyManager will only call the new method.

Here's an example of how I fixed the default implementation for the Url property.

Before

public class UrlPropertyHandler : IPropertyHandler<Url>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private const string MailTo = "mailto";

    public UrlPropertyHandler(IUrlHelper urlHelper, IContentSerializerSettings contentSerializerSettings)
    {
        _urlHelper = urlHelper ?? throw new ArgumentNullException(nameof(urlHelper));
        _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
    }

    public object Handle(Url url, PropertyInfo propertyInfo, IContentData contentData)
    {
        if (url == null)
        {
            return null;
        }

        if (url.Scheme == MailTo) return url.OriginalString;

        if (url.IsAbsoluteUri)
        {
            if (this._contentSerializerSettings.UrlSettings.UseAbsoluteUrls)
            {
                return url.OriginalString;
            }

            return url.PathAndQuery;
        }

        return this._urlHelper.ContentUrl(url, this._contentSerializerSettings.UrlSettings);
    }
}

After
As you can see, I'm just calling the new method from the old one and passing along the IContentSerializerSettings from DI to the new method.

public class UrlPropertyHandler : IPropertyHandler<Url>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private const string MailTo = "mailto";

    public UrlPropertyHandler(IUrlHelper urlHelper, IContentSerializerSettings contentSerializerSettings)
    {
        _urlHelper = urlHelper ?? throw new ArgumentNullException(nameof(urlHelper));
        _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
    }

    public object Handle(
        Url url,
        PropertyInfo propertyInfo,
        IContentData contentData)
    {
        return Handle(url, propertyInfo, contentData, _contentSerializerSettings);
    }

    public object Handle(
        Url url,
        PropertyInfo property,
        IContentData contentData,
        IContentSerializerSettings settings)
    {
        if (url == null)
        {
            return null;
        }

        if (url.Scheme == MailTo) return url.OriginalString;

        if (url.IsAbsoluteUri)
        {
            if (settings.UrlSettings.UseAbsoluteUrls)
            {
                return url.OriginalString;
            }

            return url.PathAndQuery;
        }

        return this._urlHelper.ContentUrl(url, settings.UrlSettings);
    }
}