Synthesis icon indicating copy to clipboard operation
Synthesis copied to clipboard

ContainsOr with an ItemReferenceListField doesn't work

Open sitecorepm opened this issue 4 years ago • 0 comments

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<ID>, 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<ID>
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

sitecorepm avatar Jun 25 '20 17:06 sitecorepm