Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 19 de noviembre de 2019
Entity Framework Core Hace algunos meses hablábamos del peligro que suponía la la evaluación en cliente de Entity Framework Core, una característica que, por defecto, venía activada de serie. En el post explicábamos los problemas que podían dar, cómo podíamos detectar su uso e incluso impedirlo para evitar males mayores.

Pues bien, esto ha cambiado a partir de la versión 3.0, en mi opinión, hacia una dirección bastante más correcta. Veamos en qué consisten estos cambios.

El comportamiento antes de EF Core 3.0

Como sabemos, en versiones anteriores a la 3.0 de Entity Framework Core, una consulta como la siguiente funcionaba perfectamente:
// Consulta:
public List<Friend> GetAdults()
{
    var adults = ctx.Friends.Where(f => IsAdult(f));
    return adults.ToList();
}

private static bool IsAdult(Friend f) => f.Birthdate < DateTime.Now.AddYears(-18);
El problema que tenía ese código es que, dado que la expresión IsAdult() no tenía sentido para el proveedor de datos, ésta era evaluada en cliente para todas las filas obtenidas en la consulta, que en
este caso, dado que no hay otros criterios de filtrado, serían absolutamente todas.

El framework ejecutaría la consulta, recorrería todas las filas de la tabla, materializaría las entidades y descartaría las que no cumplieran las condiciones, pero el destrozo a nivel de rendimiento ya se habría hecho.

El comportamiento a partir de EF Core 3.0

A partir de Entity Framework Core 3.0, la ejecución de una consulta como la mostrada anteriormente resultará en una excepción InvalidOperationException como la mostrada en la siguiente captura de pantalla:

Excepción lanzada
System.InvalidOperationException: 'The LINQ expression 'Where<Friend>( source: DbSet<Friend>, predicate: (f) => Program.IsAdult(f))' 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.'
El error indica que la expresión IsAdult() no puede ser traducida al "idioma" del almacén de datos (por ejemplo, no existe algo como IsAdult() en SQL), y nos invita a replantear la consulta de forma que el motor pueda entenderla, o bien mover esa condición al lado cliente para que pueda ser evaluada con éxito.

Hay muchos escenarios en los que no será posible aplicar la primera solución, aunque en nuestro caso podría ser muy sencillo porque podemos convertirlo a operaciones soportadas por el almacén de datos:
// Sí, no es muy exacto, pero ya me entendéis... ;)
var adults = ctx.Friends.Where(f => DateTime.Today.Year - f.Birthdate.Year >= 18);
O también podríamos utilizar las extensiones DbFunctions como se muestra a continuación:
var adults = ctx.Friends.Where(f =>
    EF.Functions.DateDiffYear(f.Birthdate, DateTime.Now) >= 18
);
La segunda sugerencia es más violenta, pues implica la lectura de todas las entidades y su posterior cribado. Vaya, algo similar a lo que se hacía con versiones anterior a EF Core 3.0, lo que ocurre es que seríamos nosotros los que explícitamente se lo estaríamos indicando:
var adults = ctx.Friends
                .AsEnumerable()              // La consulta es ejecutada aquí
                .Where(f => IsAdult(f));  // El filtro se aplica sobre las entidades materializadas
Entonces, ¿esto significa que en las consultas no podemos incluir ningún tipo de expresiones evaluadas en cliente? Pues no. Hay un escenario en el que sí pueden ser incluidas, y puede ser bastante útil: las expresiones en proyecciones finales.

Por ejemplo, imaginad que queremos obtener el nombre de nuestros amigos que cumplan una condición determinada, junto con un booleano indicando si es mayor de edad. La consulta podría ser como la siguiente:
public class FriendServices
{
    public void ShowSomeFriends()
    {
        using var ctx = new FriendsContext();
        var query = ctx.Friends
            .Where(f => f.Name.StartsWith("J"))
            .Select(f=> new { Name=f.Name, IsAdult = IsAdult(f) });

        foreach (var friend in query)
        {
            Console.WriteLine(friend.Name + " " + friend.IsAdult);
        }
    }

    private static bool IsAdult(Friend f) => f.Birthdate < DateTime.Now.AddYears(-18);
}
Aquí son importantes dos detalles que conviene remarcar.

En primer lugar, la expresión en cliente sólo podrá ser utilizada si se trata de la proyección final. Tiene bastante sentido: esta proyección se realiza en el momento de la materialización a objetos del CLR, por lo que ya nos encontramos en el lado cliente, y ya podemos utilizar expresiones .NET.

Sin embargo, dado que las proyecciones intermedias son resueltas en el almacén de datos, no sería posible utilizar expresiones evaluables únicamente en cliente. Por esta razón, el siguiente ejemplo fallaría en tiempo de ejecución:
var query = ctx.Friends
    .Select(f => new {f.Name, f.Birthdate, IsAdult = IsAdult(f)})
    .Where(f=>f.IsAdult)
    ...
Otro aspecto importante a tener en cuenta es que el árbol de expresión introducirá referencias hacia los objetos o métodos incluidos en la expresión cliente, y esto puede conducir a sutiles memory leaks. Por ejemplo, si en el caso anterior el método IsAdult() fuera de instancia, el árbol de expresión contendría una referencia a dicho método y, por tanto, a la instancia de FriendServices que lo aloja.

De hecho, EF Core detectará esta circunstancia y hará que la consulta rompa en ejecución por este motivo:

Excepción InvalidOperationException por posible memory leak
System.InvalidOperationException: 'Client projection contains reference to constant expression of type: MyConsoleApp.FriendServices. This could potentially cause memory leak.'
Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!