Tenemos el requisito de buscar un término determinado dentro de una cadena separada por comas. La consulta está construida para que ignore los posibles espacios iniciales y finales en la cadena separada por comas. Se me ocurrió la siguiente consulta que funciona bien con EF 6.0

var trimmedTags = tags.Select(t => t.Trim()); // List of tags we need to look for    
return products.Where(p => trimmedTags.Any(t => ("," + p.Categories + ",").Contains("," + t + ",")) ||
                               trimmedTags.Any(t => ("," + p.Categories + ",").Contains(", " + t + ",")) ||
                               trimmedTags.Any(t => ("," + p.Categories + ",").Contains("," + t + " ,")) ||
                               trimmedTags.Any(t => ("," + p.Categories + ",").Contains(", " + t + " ,")));

Esta consulta ya no se ejecuta en EF Core 3.1 y genera el siguiente error:

System.InvalidOperationException: 'The LINQ expression 'DbSet<Product>
    .Where(p => __trimmedTags_1
        .Any(t => ("," + p.Categories + ",").Contains("," + t + ",")) || __trimmedTags_1
        .Any(t => ("," + p.Categories + ",").Contains(", " + t + ",")) || __trimmedTags_1
        .Any(t => ("," + p.Categories + ",").Contains("," + t + " ,")) || __trimmedTags_1
        .Any(t => ("," + p.Categories + ",").Contains(", " + t + " ,")))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'

Mi tabla de destino tiene millones de filas, por lo que, lamentablemente, la evaluación del cliente no es una opción. El equipo de EF Core afirma que string.Contains es compatible, pero no puedo entender por qué mi consulta falla repentinamente en EF Core.

2
GETah 14 mar. 2021 a las 01:40

1 respuesta

La mejor respuesta

A menudo aparecen variaciones diferentes de esta pregunta en SO, y el problema es siempre el mismo, incluso en la versión más actual (5.x) EF Core no admite operadores en colecciones en memoria que no sean simples {{X0} } con valor primitivo (o Any que se puede convertir en Contains como x => memValues.Any(v => v == SomeExpr(x)), siendo el operador == el esencial).

La solución alternativa es también una y la misma: crear una expresión dinámicamente: || (o) basada en Any y && (y) basada en All.

Este caso requiere || y es similar a Cómo simplificar la condición OR repetitiva en Where (e => e.prop1.contains () || e.prop2.contains () || ...) pero con valores y roles de campo intercambiados, por lo que el siguiente es el método auxiliar que usaría:

public static partial class QueryableExtensions
{
    public static IQueryable<T> WhereAnyMatch<T, V>(this IQueryable<T> source, IEnumerable<V> values, Expression<Func<T, V, bool>> match)
    {
        var parameter = match.Parameters[0];
        var body = values
            // the easiest way to let EF Core use parameter in the SQL query rather than literal value
            .Select(value => ((Expression<Func<V>>)(() => value)).Body)
            .Select(value => Expression.Invoke(match, parameter, value))
            .Aggregate<Expression>(Expression.OrElse);
        var predicate = Expression.Lambda<Func<T, bool>>(body, parameter);
        return source.Where(predicate);
    }
}

Tenga en cuenta que esto solo funciona para expresiones de consulta de nivel superior. Si necesita algo como esto para algo que es parte de un árbol de expresión de consulta (como la propiedad de navegación de la colección), necesitará un tipo diferente de función auxiliar o alguna biblioteca que permita la inyección de expresiones.

Afortunadamente, ese no es el caso aquí, por lo que el método auxiliar anterior se puede usar directamente pasando trimmedTags y la condición para cada valor de etiqueta, p.

return products.WhereAnyMatch(trimmedTags, (p, t) => 
    ("," + p.Categories + ",").Contains("," + t + ",") ||
    ("," + p.Categories + ",").Contains(", " + t + ",") ||
    ("," + p.Categories + ",").Contains("," + t + " ,") ||
    ("," + p.Categories + ",").Contains(", " + t + " ,"));
2
Ivan Stoev 14 mar. 2021 a las 12:37