PAN.DapperLambdaSQL
1.3.0
dotnet add package PAN.DapperLambdaSQL --version 1.3.0
NuGet\Install-Package PAN.DapperLambdaSQL -Version 1.3.0
<PackageReference Include="PAN.DapperLambdaSQL" Version="1.3.0" />
<PackageVersion Include="PAN.DapperLambdaSQL" Version="1.3.0" />
<PackageReference Include="PAN.DapperLambdaSQL" />
paket add PAN.DapperLambdaSQL --version 1.3.0
#r "nuget: PAN.DapperLambdaSQL, 1.3.0"
#:package PAN.DapperLambdaSQL@1.3.0
#addin nuget:?package=PAN.DapperLambdaSQL&version=1.3.0
#tool nuget:?package=PAN.DapperLambdaSQL&version=1.3.0
PAN.DapperLambdaToSql
PAN.DapperLambdaToSql es una librería ligera que extiende Dapper y Dapper.Contrib, permitiendo realizar operaciones genéricas como UPDATE y EXIST utilizando expresiones lambda (Expression<Func<T, bool>>), al estilo de Entity Framework.
⚠️ Importante: Esta librería está diseñada para usarse exclusivamente con Dapper y Dapper.Contrib. Nota: Esta librería no es compatible con Entity Framework ni con Entity Framework Core. Está enfocada en simplificar el uso de Dapper para operaciones comunes sin necesidad de escribir SQL manualmente.
✨ Características
- ✅ Actualización de entidades genéricas con
UpdateAsync - ✅ Verificación de existencia con
ExistAsync - ✅ Consulta de filas que cumplen un predicado con
QueryAsync - ✅ Predicados con
==,!=,<,<=,>,>=,&&,||,Contains(LIKE) y agrupamiento con paréntesis - ✅ Orden y paginado genéricos en memoria con
OrderByProperty/ToPagedResult, con soporte de desempate por varias claves (OrderByProperties) yQueryPagedAsynccomo atajo - ✅ Compatible con
DapperyDapper.Contrib: respeta[Table],[Key],[ExplicitKey],[Write(false)]y[Computed] - ✅ Varias convenciones de clave primaria:
Id,IDyClase+Id - ✅ Funciona con SQL Server y MySQL / MariaDB, delimitando los identificadores según el motor
- ✅ Sin boilerplate: elimina la necesidad de escribir SQL manual para cada entidad
💡 Instalación
dotnet add package PAN.DapperLambdaToSql
🚀 Uso
📌 Actualizar cualquier entidad con UpdateAsync
Este método permite actualizar cualquier entidad genérica sin escribir SQL manualmente. Solo necesitas asegurarte de que la entidad tenga una clave primaria reconocible —ver Clave primaria: nombres soportados— y que las propiedades que deseas actualizar no sean null.
// En tu servicio o repositorio genérico
public async Task<bool> UpdateAsync(T entity)
{
return await _context.UpdateAsync(entity);
}
👉 Internamente genera y ejecuta dinámicamente una consulta SQL como esta:
UPDATE NombreTabla SET Columna1 = @Columna1, Columna2 = @Columna2 WHERE Id = @Id
🗄️ Motores soportados
El motor se detecta solo, a partir del tipo de conexión que le pasas. No hay nada que configurar:
| Motor | Se activa con | Identificadores |
|---|---|---|
| SQL Server | SqlConnection (System.Data.SqlClient o Microsoft.Data.SqlClient) |
[Nombre] |
| MySQL / MariaDB | MySqlConnection (MySql.Data o MySqlConnector) |
`Nombre` |
| Otros | cualquier otra conexión | sin delimitar |
// La misma llamada, el SQL correcto para cada motor
using var cn = new SqlConnection(cs); // UPDATE [Roles] SET [Key] = @Key WHERE [Id] = @Id
using var cn = new MySqlConnection(cs); // UPDATE `Roles` SET `Key` = @Key WHERE `Id` = @Id
await cn.UpdateAsync(role);
Delimitar los identificadores permite usar columnas cuyo nombre es palabra reservada —Key, Order, Group, Status—, que sin comillas provocan un error de sintaxis.
Si usas un proveedor que envuelve la conexión real (perfilado, tracing) y por eso no se reconoce, puedes forzar el dialecto:
DapperHelper.Dialect = SqlDialects.SqlServer; // o SqlDialects.MySql
⚠️ Es una configuración global. Si tu aplicación habla con dos motores a la vez, déjala en
nully confía en la detección automática.
PostgreSQL no está soportado. Postgres convierte a minúsculas todo identificador que no vaya entre comillas, así que la decisión de delimitar condiciona cómo debe crearse el esquema; se prefirió no adivinarla.
🔑 Clave primaria: nombres soportados
UpdateAsync resuelve la clave primaria recorriendo esta lista en orden y deteniéndose en la primera coincidencia:
| # | Qué se busca | Ejemplo |
|---|---|---|
| 1 | Propiedad con [Key] o [ExplicitKey] de Dapper.Contrib |
[ExplicitKey] public Guid Codigo { get; set; } |
| 2 | Una propiedad llamada Id, coincidencia exacta |
public int Id { get; set; } |
| 3 | Una propiedad llamada Id sin distinguir mayúsculas |
public int ID { get; set; } |
| 4 | Una propiedad llamada <NombreDeLaClase>Id |
public int UserId { get; set; } en la clase User |
Si ninguna coincide, se lanza una InvalidOperationException indicando la entidad y los nombres que se buscaron.
Los atributos siempre ganan a la convención, así que cualquier caso ambiguo se resuelve anotando la propiedad correcta con [ExplicitKey].
⚠️ Las llaves foráneas nunca se confunden con la primaria
El paso 4 busca un nombre concreto —el de la clase más Id—, nunca "cualquier propiedad que termine en Id". Es una distinción importante, porque las llaves foráneas siguen exactamente ese mismo patrón de nombres:
public class User
{
public int Id { get; set; } // ← clave primaria (resuelta en el paso 2)
public int? GymId { get; set; } // ← foránea: se actualiza como cualquier columna
public int RoleId { get; set; } // ← foránea: se actualiza como cualquier columna
}
En User el nombre buscado por el paso 4 sería UserId, así que ni GymId ni RoleId son candidatas. Y como la resolución se detiene en el paso 2, en este ejemplo el paso 4 ni siquiera llega a evaluarse.
Solo la clave primaria resuelta se excluye del SET. Todas las demás columnas, foráneas incluidas, se siguen actualizando con normalidad.
La clave da nombre al WHERE
La columna del WHERE y el parámetro toman el nombre real de la propiedad resuelta:
public class Producto
{
[ExplicitKey] public string Sku { get; set; }
public string Nombre { get; set; }
}
UPDATE Productos SET Nombre = @Nombre WHERE Sku = @Sku
📌 Actualizar cualquier entidad con UpdateAsync (actualización parcial)
Este método de extensión permite actualizar dinámicamente cualquier entidad genérica sin escribir SQL manualmente.
Solo actualizará las propiedades no nulas que no sean la clave primaria, por lo que es ideal para escenarios de actualización parcial (PATCH).
- ✅ Ventaja: No se sobreescriben columnas con
null,0oDateTime.MinValuesi no las envías.
Ejemplo de entidad:
public class Gym
{
public int Id { get; set; }
public int? Code { get; set; } // Campo que no se actualiza si no se envía
public string Name { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
public DateTime? CreatedAt { get; set; } // Tampoco se actualiza si no se envía
}
Ejemplo de uso en un servicio o handler:
var gym = new Gym
{
Id = 1,
Name = "New Name",
Phone = "999-888-777"
// No enviamos Code ni CreatedAt => no se actualizan
};
bool actualizado = await gymRepository.UpdateGymAsync(gym);
if (actualizado)
{
Console.WriteLine("Actualización parcial exitosa 🚀");
}
else
{
Console.WriteLine("No se actualizó ningún registro.");
}
Ejemplo de uso en un repositorio
public async Task<bool> UpdateGymAsync(Gym gym)
{
return await _dbConnection.UpdateAsync(gym);
}
💡 Notas:
- La clave primaria se usa exclusivamente para el
WHEREen elUPDATE; nunca se incluye en elSET. - Las propiedades con
nullse ignoran y no se incluyen en la sentencia SQL. - Las propiedades marcadas con
[Write(false)]o[Computed]de Dapper.Contrib se excluyen delUPDATE. Es lo que necesitas para las propiedades de navegación que cargas con unJOINy que no existen como columna en la tabla, y para las columnas cuyo valor genera la base de datos (columnas calculadas,DEFAULT,rowversion, triggers). - Funciona con cualquier entidad cuya clave primaria siga alguna de las convenciones soportadas.
📌 Verificar existencia con ExistAsync
Este método permite consultar si existe una entidad que cumpla con una condición específica, usando expresiones lambda al estilo de Entity Framework.
// En tu servicio o repositorio genérico
public async Task<bool> ExistAsync(Expression<Func<T, bool>> predicate)
{
return await _context.ExistAsync(predicate);
}
Ejemplo:
bool existe = await _context.ExistAsync<User>(x => x.Email == "test@example.com");
📌 Consultar filas con QueryAsync
Este método devuelve las filas que cumplen el predicado, al estilo de un Where() de Entity Framework — a diferencia de ExistAsync, que solo devuelve bool.
// En tu servicio o repositorio genérico
public async Task<IEnumerable<T>> QueryAsync(Expression<Func<T, bool>> predicate)
{
return await _context.QueryAsync(predicate);
}
Ejemplo:
IEnumerable<User> usuarios = await _context.QueryAsync<User>(u => u.RoleId == 2 && u.Name.Contains("Pedro"));
🔎 Operadores soportados en los predicados
Tanto ExistAsync como QueryAsync reciben un Expression<Func<T, bool>> y lo traducen a SQL. Están soportados:
| Expresión C# | SQL generado |
|---|---|
== |
= |
!= |
<> |
<, <=, >, >= |
<, <=, >, >= |
&& |
AND |
\|\| |
OR |
x.Prop.Contains("valor") |
LIKE '%valor%' |
Se pueden combinar libremente, incluso con agrupamiento entre paréntesis — se agregan los paréntesis mínimos necesarios para que el SQL se lea igual que la expresión de C#:
await _context.QueryAsync<User>(u => u.Name == "Pedro" && (u.RoleId == 2 || u.RoleId == 3));
// WHERE Name = @param0 AND (RoleId = @param1 OR RoleId = @param2)
⚠️
Containsno escapa%ni_en el valor; si tu dato los trae, se interpretan como comodines deLIKE. Cualquier otro método (StartsWith,Containssobre una lista, etc.) lanzaNotSupportedException.
📄 Orden y paginado en memoria con PagingExtensions
Dapper no tiene una capa IQueryable diferida como Entity Framework, así que no hay forma de traducir un "ordená por este nombre de columna" hasta el SQL sin reimplementar esa capa. OrderByProperty y ToPagedResult resuelven eso en memoria, sobre una secuencia ya traída (por ejemplo, el resultado de QueryAsync):
var usuarios = await _context.QueryAsync<User>(u => u.GymId == 1);
PagedResult<User> pagina = usuarios.ToPagedResult(page: 2, pageSize: 20, orderBy: "Name");
// pagina.Items, pagina.Page, pagina.PageSize, pagina.TotalCount, pagina.TotalPages
orderBy recibe el nombre de una propiedad pública de T (no distingue mayúsculas/minúsculas); si no existe, lanza ArgumentException. No soporta rutas anidadas ("Cliente.Nombre").
Desempate con varias claves de orden
Para ordenar por más de una propiedad, OrderByProperties y el overload correspondiente de ToPagedResult reciben params (string Property, bool Descending)[]:
var usuarios = await _context.QueryAsync<User>(u => u.GymId == 1);
// Ordena por RoleId y, dentro de cada rol, por Name
PagedResult<User> pagina = usuarios.ToPagedResult(page: 1, pageSize: 20,
(nameof(User.RoleId), false), (nameof(User.Name), false));
La primera clave se aplica con OrderBy/OrderByDescending y el resto encadena ThenBy/ThenByDescending, así que cada clave puede tener su propio sentido (ascendente o descendente). OrderByProperties exige al menos una clave (lanza ArgumentException con un array vacío); el overload de ToPagedResult con sortKeys, en cambio, trata un array vacío como "sin orden" y preserva el orden de origen.
QueryPagedAsync: QueryAsync + ToPagedResult en una sola llamada
Para no repetir esos dos pasos en cada repositorio:
PagedResult<User> pagina = await _context.QueryPagedAsync<User>(
u => u.GymId == 1,
page: 1, pageSize: 20,
(nameof(User.RoleId), false), (nameof(User.Name), false));
Es pura conveniencia: internamente ejecuta QueryAsync<T>(predicate) y aplica ToPagedResult sobre el resultado, en memoria.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
1.3.0
- Nuevo OrderByProperties/ToPagedResult(params (string, bool)[] sortKeys): orden con desempate
por varias claves, ademas de la clave unica ya soportada.
- Nuevo QueryPagedAsync<T>: une QueryAsync<T>(predicate) + ToPagedResult(...) en una sola
llamada, para no repetir ambos pasos en cada repositorio.
1.2.0
- Nuevo QueryAsync<T>: devuelve las filas que cumplen un predicado (SELECT filtrado),
a diferencia de ExistAsync, que solo confirma si existe alguna.
- El traductor de expresiones ahora soporta !=, <, <=, >, >=, || y agrupamiento
con parentesis, ademas de == y &&.
- Contains(...) sobre propiedades string se traduce a LIKE.
- Nuevo OrderByProperty/ToPagedResult (PagingExtensions): orden y paginado genericos en
memoria por nombre de columna, ya que Dapper no expone una capa IQueryable diferida.
1.1.0
- Compatibilidad con SQL Server y MySQL: los identificadores se delimitan segun el motor,
detectado a partir del tipo de conexion. Permite columnas con nombre reservado (Key, Order...).
- Se respetan [Write(false)] y [Computed] de Dapper.Contrib: las propiedades de navegacion
y las columnas calculadas dejan de incluirse en el UPDATE.
- La clave primaria se resuelve por [Key]/[ExplicitKey] o por convencion (Id, ID, Clase+Id),
en lugar de estar fijada al nombre literal "Id".
- La cache de nombres de tabla pasa a ser segura para concurrencia.