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()oFromSqlInterpolated(), 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 elOnModelCreating - 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 | ⚠️ Parcial | Baja |
| Filtro en C# post-query | ❌ No | ❌ No | Alta |
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: