← maurobernal.com.ar

EF.Functions: llamá a funciones nativas de SQL desde LINQ en EF Core

Uno de los avances más prácticos en las versiones recientes de Entity Framework Core es la posibilidad de llamar a funciones nativas de la base de datos directamente desde LINQ, sin necesidad de librerías externas, interpolación de SQL crudo ni workarounds costosos. Todo a través de EF.Functions.

El problema de antes

Antes de que EF Core expusiera estas funciones de forma nativa, llamar a algo como LIKE, SOUNDEX, DATEDIFF o CONTAINS implicaba una de estas opciones:

  • Usar FromSqlRaw() o FromSqlInterpolated(), rompiendo la fluidez del LINQ y volviendo al SQL crudo
  • Instalar librerías de terceros como LinqKit o EntityFramework.Functions
  • Mapear funciones escalares manualmente via HasDbFunction() en el OnModelCreating
  • Traer más datos de los necesarios a memoria y filtrar en C# (el clásico antipatrón de performance)

Nada de eso es malo por definición, pero generaba fricción y código difícil de mantener.

¿Qué es EF.Functions?

EF.Functions es una propiedad estática de la clase EF (namespace Microsoft.EntityFrameworkCore) que expone métodos CLR que se traducen directamente a funciones de base de datos cuando se usan en consultas LINQ to Entities. Si los llamás fuera de ese contexto (por ejemplo en LINQ to Objects), lanzan NotSupportedException — son puros trampolines de traducción, no implementaciones reales en C#.

// EF.Functions es de tipo DbFunctions
// Accedés a sus métodos vía EF.Functions.XYZ(...)
var result = context.Products
    .Where(p => EF.Functions.Like(p.Name, "%laptop%"))
    .ToList();

El SQL generado es exactamente lo que esperás:

SELECT * FROM Products WHERE Name LIKE '%laptop%'

Funciones disponibles (con ejemplos)

1. EF.Functions.Like — búsqueda por patrón

La función más usada. Equivale al operador LIKE de SQL con soporte para wildcards (%, _).

// Buscar productos cuyo nombre contenga "core"
var productos = await context.Productos
    .Where(p => EF.Functions.Like(p.Nombre, "%core%"))
    .ToListAsync();

// Con escape character (para buscar literalmente el %)
var especiales = await context.Productos
    .Where(p => EF.Functions.Like(p.Codigo, @"50\%", @"\"))
    .ToListAsync();

Antes de tener esto nativo, mucha gente hacía p.Nombre.Contains("core") que en algunos providers no genera un LIKE limpio, o directamente traía todo a memoria y filtraba en C#.

2. EF.Functions.DateDiffDay / DateDiffMonth / etc. (SQL Server)

Para calcular diferencias entre fechas usando la función DATEDIFF de SQL Server, sin hacer la resta en C# después de traer los datos.

// Pedidos creados en los últimos 30 días
var recientes = await context.Pedidos
    .Where(p => EF.Functions.DateDiffDay(p.FechaCreacion, DateTime.Now) <= 30)
    .ToListAsync();

// Clientes que llevan más de 12 meses activos
var veteranos = await context.Clientes
    .Where(c => EF.Functions.DateDiffMonth(c.FechaAlta, DateTime.Now) >= 12)
    .ToListAsync();

SQL generado:

SELECT * FROM Pedidos WHERE DATEDIFF(day, FechaCreacion, GETDATE()) <= 30

3. EF.Functions.Contains — Full-Text Search

Cuando tenés un índice Full-Text configurado en SQL Server, podés aprovecharlo directamente desde LINQ:

// Full-text search sobre la columna Descripcion
var resultados = await context.Articulos
    .Where(a => EF.Functions.Contains(a.Descripcion, "\"inteligencia artificial\""))
    .ToListAsync();

// Búsqueda de múltiples términos con NEAR
var proximidad = await context.Articulos
    .Where(a => EF.Functions.Contains(a.Contenido, "NEAR((machine, learning), 5)"))
    .ToListAsync();

Sin esto, había que caer en FromSqlRaw o una stored procedure para aprovechar el FTS.

4. EF.Functions.FreeText — Full-Text Search semántico

// Búsqueda por significado, no solo por texto exacto
var relacionados = await context.Articulos
    .Where(a => EF.Functions.FreeText(a.Descripcion, "programación orientada a objetos"))
    .ToListAsync();

5. EF.Functions.Collate — comparación con collation específica

Muy útil para búsquedas case-sensitive o accent-sensitive sin cambiar la collation de toda la columna:

// Búsqueda case-sensitive sin tocar el esquema
var exacto = await context.Usuarios
    .Where(u => EF.Functions.Collate(u.Username, "SQL_Latin1_General_CP1_CS_AS") == "Admin")
    .ToListAsync();

6. EF.Functions.IsNumeric (SQL Server)

// Filtrar solo las filas donde un campo de texto es numérico
var soloNumericos = await context.Registros
    .Where(r => EF.Functions.IsNumeric(r.CodigoExterno) == 1)
    .ToListAsync();

7. Funciones de distancia geoespacial (con provider específico)

Con el provider de SQL Server y NetTopologySuite, EF.Functions se extiende para soportar funciones espaciales:

// Sucursales dentro de 10km del usuario
var cercanas = await context.Sucursales
    .Where(s => s.Ubicacion.Distance(puntoUsuario) <= 10000)
    .OrderBy(s => s.Ubicacion.Distance(puntoUsuario))
    .ToListAsync();

Funciones propias: HasDbFunction

Si tu base de datos tiene funciones escalares propias, podés exponerlas en LINQ con una declaración en el modelo y sin salir de la fluidez del query:

// En el DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasDbFunction(
        typeof(MiDbContext).GetMethod(nameof(CalcularDescuento))!
    );
}

// Método estático que actúa de "proxy"
public static decimal CalcularDescuento(int clienteId, decimal monto)
    => throw new NotSupportedException(); // nunca se ejecuta en C#

// Uso en LINQ
var pedidos = await context.Pedidos
    .Select(p => new {
        p.Id,
        Descuento = MiDbContext.CalcularDescuento(p.ClienteId, p.Total)
    })
    .ToListAsync();

EF Core traduce la llamada a dbo.CalcularDescuento(ClienteId, Total) en el SQL generado.

¿Por qué importa esto para la performance?

El beneficio no es solo sintáctico. Cuando el filtrado o cálculo ocurre en la base de datos en lugar de en memoria, el motor puede usar índices, paralelismo y el plan de ejecución óptimo. Traer miles de filas a C# para luego filtrarlas es uno de los problemas de performance más comunes en proyectos con ORM.

Enfoque¿Filtra en DB?¿Usa índices?Fricción
EF.Functions.Like()✅ Sí✅ SíMínima
p.Nombre.Contains()⚠️ Depende del provider⚠️ ParcialBaja
Filtro en C# post-query❌ No❌ NoAlta
FromSqlRaw()✅ Sí✅ SíAlta (SQL manual)

Conclusión

EF.Functions es una de esas features que, una vez que la conocés, no podés creer haber vivido sin ella. Reduce la necesidad de bajar a SQL crudo para casos que antes no tenían salida limpia, mantiene la expresividad del LINQ, y sobre todo empuja el trabajo al motor de base de datos donde tiene que estar.

📖 Referencias oficiales: