blipson89 / Synthesis

Synthesis is a universal object mapper for Sitecore
MIT License
75 stars 25 forks source link

ContainsOr with an ItemReferenceListField doesn't work #89

Open sitecorepm opened 4 years ago

sitecorepm commented 4 years ago

Do you want to request a feature or report a bug? Bug

What is the current behavior? When attempting to use the "ContainsOr" extension method with an ItemReferenceListField fieldtype an exception is thrown:

public IQueryable<SearchResultItem> ShapeQuery1(IProviderSearchContext context, IQueryable<SearchResultItem> query, IBaseSearchPageConfigurationItem config)
{
    var cfg = config as IPersonListingItem;
    var q = context.GetQueryable<IBasePersonItem>();

    if (cfg.PersonTypeFilter.HasValue)
        // where k.PersonType is an IItemReferenceListField field
        q = q.ContainsOr(k => k.PersonType, cfg.PersonTypeFilter.TargetIds.ToArray());

    return q.Cast<SearchResultItem>().AsQueryable();
}

The exception is occurs in the ContentSearchQueryExtensions.cs class line 103:

System.InvalidOperationException: 'No method 'Contains' on type 'System.Linq.Enumerable' is compatible with the supplied arguments.'

From what I can tell, this occurs because the initial attempt to resolve the 'Contains' method on IItemReferenceListField fails to find it (line 70). Even though IItemReferenceListField has the base interface ICollection, it does not find it because it is not explicitly defined on IItemReferenceListField.

In order to resolve this issue, I came up with 2 solutions.

  1. The simple hack was to just add the 'Contains' method to the IItemReferenceListField interface using the 'new' keyword to hide the underlying 'Contains' method from ICollection
public interface IItemReferenceListField : ICollection<ID>, IFieldType
{
    //
    // ...
    //

    // hides 'Contains' from ICollection<ID>
    new bool Contains(ID item);
}

This worked, but is somewhat of a hack.. and it is specific to just IItemReferenceListField.

  1. I added another else-if block to the ContentSearchQueryExtensions.cs class which checks the base interfaces of a type as a secondary fallback before resorting to using the 'Contains' method from IEnumerable.
if (typeOfTKey.GetMethods().Any(m => m.Name.Equals(methodName)))
{
    var method = typeOfTKey.GenericTypeArguments.Any() ? typeOfTKey.GetMethod(methodName, typeOfTKey.GenericTypeArguments) : typeOfTKey.GetMethod(methodName);

    // Useful Large Comment...
    expressions = constants.Select(constant => Expression.Call(keySelector.Body, method, constant));
}
else if (typeOfTKey.GetInterfaces().Any(i => i.GetMethods().Any(m => m.Name.Equals(methodName))))
{
    // Same as above except, check base interfaces of the typeOfTKey for the "Contains" method
    var baseType = typeOfTKey.GetInterfaces().First(i => i.GetMethods().Any(m => m.Name.Equals(methodName)));
    var method = baseType.GenericTypeArguments.Any() ? baseType.GetMethod(methodName, baseType.GenericTypeArguments) : baseType.GetMethod(methodName);
    expressions = constants.Select(constant => Expression.Call(keySelector.Body, method, constant));
}
else
{
    // Useful Large Comment...
    var typeArgs = typeOfTKey.IsArray ? new[] { typeOfTKey.GetElementType() } : typeOfTKey.GenericTypeArguments;

    expressions = constants.Select(constant => Expression.Call(typeof(Enumerable), methodName, typeArgs, keySelector.Body, constant));
}

If this looks okay, I can send in a PR.

If the current behavior is a bug, please provide the steps to reproduce.

  1. Setup a synthesis item with a ItemReferenceListField
  2. Run a content search like the snippet above

What is the expected behavior?

I expected it to work with the ItemReferenceListField field type.

Please mention your Sitecore version and Synthesis version. Sitecore version 9.1 Synthesis version 9.1.0.2-beta1