Start Debugging

Solución: EF Core no pudo traducir `Contains` sobre un `IList<T>` o `ISet<T>` static readonly

EF Core 8, 9 y 10 no logran traducir Contains cuando la lista es un campo static readonly de tipo IList, ICollection, ISet o IReadOnlySet. Llama a Enumerable.Contains de forma explícita o actualiza a EF Core 11.

Si Where(x => AllowedCodes.Contains(x.Code)) lanza The LINQ expression ... could not be translated con Translation of method 'System.Linq.Enumerable.Contains' failed, revisa cómo está declarado AllowedCodes. Casi seguro es un campo static readonly de tipo IList<T>, ICollection<T>, ISet<T>, IReadOnlySet<T>, IImmutableSet<T> o FrozenSet<T>. La solución más rápida, que mantiene el mismo SQL, es llamar al operador LINQ de forma explícita: Enumerable.Contains(AllowedCodes, x.Code). La solución real es EF Core 11, donde dotnet/efcore#36757 corrigió la comprobación de query root que rechaza estas formas. Medí cada variante a continuación con Microsoft.EntityFrameworkCore.Sqlite 8.0.21, 9.0.19, 10.0.12 y 11.0.0-rc.1.26425.128. Las tres primeras fallan igual. EF Core 11 RC 1 las traduce todas.

El error en contexto

Esta es la excepción de EF Core 10.0.12 en .NET 10 para un static readonly IList<string>:

System.InvalidOperationException: The LINQ expression 'DbSet<Order>()
    .Where(o => (IList<string>)List<string> { "Open", "Pending" }
        .Contains(o.Status))' could not be translated. Additional information: Translation of method 'System.Linq.Enumerable.Contains' failed. If this method can be mapped to your custom function, see https://go.microsoft.com/fwlink/?linkid=2132413 for more information. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutorExpression[TResult](Expression query)

Ese mensaje tiene dos pistas. La primera es la conversión de tipo (cast). La colección aparece como (IList<string>)List<string> { "Open", "Pending" }, lo que significa que EF ya evaluó tu campo a su valor, una constante, y lo envolvió en un cast al tipo declarado. La segunda es el nombre del método. Escribiste IList<T>.Contains, un método de instancia, pero el mensaje nombra Enumerable.Contains. EF reescribe las llamadas a ICollection<T>.Contains al operador LINQ antes de traducirlas. Así que el método no es el problema. El problema es el argumento que EF le pasa.

Cuando el campo es de tipo IReadOnlySet<T> o IImmutableSet<T>, el nombre del método en el mensaje cambia a System.Collections.Generic.IReadOnlySet<string>.Contains o System.Collections.Immutable.IImmutableSet<string>.Contains. Esas interfaces no heredan de ICollection<T>, así que EF nunca reescribe la llamada. Es el mismo error con otro mensaje.

Por qué falla un campo static readonly y una variable local no

Esto ocurre en dos pasos, y el error solo aparece cuando suceden ambos.

Paso 1: EF incrusta los campos static readonly como constantes. Antes de traducir, el funcletizer de EF recorre la consulta y evalúa todo lo que no depende de la base de datos. Las variables locales capturadas, los campos de instancia y las propiedades estáticas se convierten en parámetros de la consulta. Un campo estático que es readonly (FieldInfo.IsInitOnly) se trata como un valor que no puede cambiar, así que EF lo evalúa una vez y lo incrusta como constante. En EF Core 10 puedes verlo en ExpressionTreeFuncletizer.VisitMember: el miembro estático se marca como variable capturada “unless the captured variable is init-only”. Cuando EF construye esa constante, le asigna el tipo de runtime del valor (List<string>) y luego agrega un nodo Convert de vuelta al tipo declarado (IList<string>) siempre que ambos difieran.

Paso 2: la comprobación de query root solo elimina un tipo de cast. Para traducir Contains sobre una colección en memoria, EF convierte la colección en un query root en línea y luego en IN (...). En EF Core 8, 9 y 10, QueryRootProcessor.VisitQueryRootCandidate desenvuelve un Convert solo cuando el tipo destino es exactamente IEnumerable<T>:

// EF Core 10.0.x, src/EFCore/Query/QueryRootProcessor.cs
if (expression is UnaryExpression { NodeType: ExpressionType.Convert } convertExpression
    && convertExpression.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
    candidateExpression = convertExpression.Operand;
}

Un Convert a IList<string> no coincide, así que la colección nunca se reconoce como query root y el Contains cae en “could not be translated”.

Con esto en mente, el patrón de éxito y fallo tiene sentido:

Esto no es una regresión. El reporte original de la variante con IReadOnlySet<T> se remonta a EF Core 7, y dotnet/efcore#38839 lo reproduce en 7.0.20 hasta 10.0.11.

Reproducción mínima

// .NET 10, C# 14, Microsoft.EntityFrameworkCore.Sqlite 10.0.12
using Microsoft.EntityFrameworkCore;

using var db = new ShopContext();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
db.Orders.AddRange(
    new Order { Status = "Open" },
    new Order { Status = "Pending" },
    new Order { Status = "Shipped" });
db.SaveChanges();

// Throws InvalidOperationException on EF Core 8, 9 and 10
var active = db.Orders
    .Where(o => OrderRules.ActiveStatuses.Contains(o.Status))
    .ToList();

Console.WriteLine(active.Count);

public static class OrderRules
{
    public static readonly IList<string> ActiveStatuses = new List<string> { "Open", "Pending" };
}

public class Order
{
    public int Id { get; set; }
    public string Status { get; set; } = "";
}

public class ShopContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=shop.db");
}

Cambia IList<string> por List<string>, quita readonly o convierte el campo en una propiedad { get; }, y la misma consulta funciona. Así suele descubrirse el error: una revisión de código sugiere “expón la interfaz, no el tipo concreto” o “haz ese campo readonly”, y una consulta que funcionó durante meses se rompe.

Qué hace cada declaración en EF Core 8, 9, 10 y 11

Ejecuté una prueba por cada forma contra SQLite en cada versión de EF. Los paquetes de EF Core 8 y 9 se ejecutaron en el runtime de .NET 10. EF Core 11 RC 1 se ejecutó en .NET 11 RC 1. “Constante” significa que EF incrustó los valores en el SQL. “Parámetro” significa que los envió como parámetros.

DeclaraciónEF 8.0.21EF 9.0.19EF 10.0.12EF 11 RC 1
static readonly IList<T>fallafallafallaconstante
static readonly ICollection<T>fallafallafallaconstante
static readonly ISet<T>fallafallafallaconstante
static readonly IReadOnlySet<T>fallafallafallaconstante
static readonly IImmutableSet<T>fallafallafallaconstante
static readonly FrozenSet<T>fallafallafallaconstante
static readonly IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>constanteconstanteconstanteconstante
static readonly List<T>, HashSet<T>, T[]constanteconstanteconstanteconstante
static IList<T> (no readonly) o propiedad estáticaparámetroparámetroparámetroparámetro
IList<T> local capturadaparámetroparámetroparámetroparámetro
EF.Constant(localIList).Contains(...)fallaconstanteconstanteconstante

La última fila es un caso parecido que vale la pena conocer. En EF Core 8, forzar un IList<T> local a constante con EF.Constant cae en el mismo error. Desde EF Core 9, EF.Constant pasa por otra ruta y funciona.

Las soluciones, en orden de preferencia

1. Actualizar a EF Core 11

La corrección es dotnet/efcore#36757, integrada en main el 2025-09-24 y publicada en EF Core 11. Cambia la comprobación para desenvolver cualquier Convert cuyo destino sea asignable a IEnumerable, y lo hace de forma recursiva:

// EF Core 11.0, src/EFCore/Query/QueryRootProcessor.cs
if (expression is UnaryExpression { NodeType: ExpressionType.Convert } convertExpression
    && convertExpression.Type.IsAssignableTo(typeof(IEnumerable)))
{
    return VisitQueryRootCandidate(convertExpression.Operand, elementClrType);
}

No se hizo backport. La rama release/10.0 todavía tiene la comparación con typeof(IEnumerable<>), y 10.0.12 sigue fallando. dotnet/efcore#35024 (el reporte de IList/ICollection) tiene el hito 11.0.0, y #38839 se cerró como duplicado de este. Si estás en EF Core 10 LTS, planea usar una de las reescrituras siguientes hasta que pases a 11.

2. Llamar a Enumerable.Contains de forma explícita

Esta es la solución de una línea que recomiendo en EF Core 8, 9 y 10, porque mantiene exactamente el SQL que obtendrías en EF Core 11:

// .NET 10, Microsoft.EntityFrameworkCore.Sqlite 10.0.12
var active = db.Orders
    .Where(o => Enumerable.Contains(OrderRules.ActiveStatuses, o.Status))
    .ToList();

// WHERE "o"."Status" IN ('Open', 'Pending')

Al llamar directamente al método estático, el compilador convierte el campo a IEnumerable<string>, así que la constante de EF llega con cast a IEnumerable<T> y pasa la comprobación antigua. Funcionó en las seis formas que fallaban en mis pruebas, incluidas IReadOnlySet<T> e IImmutableSet<T>. OrderRules.ActiveStatuses.AsEnumerable().Contains(o.Status) hace lo mismo si prefieres la sintaxis de método. OrderRules.ActiveStatuses.Any(s => s == o.Status) también se traduce a la misma lista IN, pero se lee peor y no lo usaría solo para sortear esto.

Una contrapartida: en un ISet<T> o FrozenSet<T>, Enumerable.Contains en LINQ to Objects normal se saltaría la búsqueda por hash. Dentro de una consulta de EF eso no importa, porque la llamada nunca se ejecuta en .NET. Solo describe el SQL.

3. Cambiar el tipo declarado

Si la colección solo se usa en consultas, decláralo como IReadOnlyCollection<T>, IReadOnlyList<T> o un arreglo. Los tres son de solo lectura y los tres se traducen en todas las versiones:

// .NET 10, Microsoft.EntityFrameworkCore 10.0.12
public static class OrderRules
{
    public static readonly IReadOnlyList<string> ActiveStatuses = ["Open", "Pending"];
}

No cambies a FrozenSet<T> para obtener inmutabilidad “real”. En EF Core 8 a 10 falla por la razón descrita arriba.

4. Dejar que EF parametrice los valores

Copiar el campo a una variable local, o convertirlo en una propiedad static, hace que EF envíe los valores como parámetros en lugar de constantes:

// .NET 10, Microsoft.EntityFrameworkCore.Sqlite 10.0.12
var statuses = OrderRules.ActiveStatuses;
var active = db.Orders.Where(o => statuses.Contains(o.Status)).ToList();

// EF Core 10: WHERE "o"."Status" IN (@statuses1, @statuses2)
// EF Core 8/9 on SQLite: WHERE "o"."Status" IN (SELECT "s"."value" FROM json_each(@__statuses_0) AS "s")

Esto funciona, pero cambia el SQL. Recuerda por qué existe la ruta de constantes: los valores nunca cambian, así que incrustarlos le da a la base de datos una lista fija de literales, que es la mejor forma para el uso de índices y el almacenamiento en caché de planes. Para una lista corta de códigos de estado, las constantes dan mejor SQL. Usa esta opción cuando la lista realmente pueda cambiar en runtime, no solo para esquivar el error de traducción. Si quieres ver qué genera EF en cada caso, registra el SQL que genera EF Core antes y después del cambio.

Variantes que llegan a esta página por error

Relacionado

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver