PAN.DapperLambdaSQL 1.3.0

dotnet add package PAN.DapperLambdaSQL --version 1.3.0
                    
NuGet\Install-Package PAN.DapperLambdaSQL -Version 1.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PAN.DapperLambdaSQL" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PAN.DapperLambdaSQL" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="PAN.DapperLambdaSQL" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PAN.DapperLambdaSQL --version 1.3.0
                    
#r "nuget: PAN.DapperLambdaSQL, 1.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PAN.DapperLambdaSQL@1.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PAN.DapperLambdaSQL&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=PAN.DapperLambdaSQL&version=1.3.0
                    
Install as a Cake Tool

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) y QueryPagedAsync como atajo
  • ✅ Compatible con Dapper y Dapper.Contrib: respeta [Table], [Key], [ExplicitKey], [Write(false)] y [Computed]
  • ✅ Varias convenciones de clave primaria: Id, ID y Clase+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 reservadaKey, 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 null y 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, 0 o DateTime.MinValue si 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 WHERE en el UPDATE; nunca se incluye en el SET.
  • Las propiedades con null se ignoran y no se incluyen en la sentencia SQL.
  • Las propiedades marcadas con [Write(false)] o [Computed] de Dapper.Contrib se excluyen del UPDATE. Es lo que necesitas para las propiedades de navegación que cargas con un JOIN y 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)

⚠️ Contains no escapa % ni _ en el valor; si tu dato los trae, se interpretan como comodines de LIKE. Cualquier otro método (StartsWith, Contains sobre una lista, etc.) lanza NotSupportedException.

📄 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .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.

Version Downloads Last Updated
1.3.0 174 8/1/2026
1.2.0 103 8/1/2026
1.1.0 126 7/28/2026
1.0.2 313 8/6/2025
1.0.1 277 8/6/2025
1.0.0 185 7/16/2025

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.