Gesgocom.MicroOrmGesg 1.2.15
MicroOrmGesg
Micro ORM para PostgreSQL basado en Dapper y Npgsql. Proporciona CRUD genérico, queries directas, ejecución de funciones, migraciones idempotentes y logging integrado.
Tabla de contenidos
🚀 Inicio rápido
📚 Casos de uso
- ¿Cuándo usar qué? - Guía de decisión
- CRUD simple con repositorio genérico
- Queries SQL personalizadas
- Transacciones y operaciones complejas
- Ejecutar funciones PostgreSQL
- Trabajo con JSONB
- Migraciones de base de datos
🔧 Referencia completa
- DbSession: Conexiones y transacciones
- IDataMicroOrm: Repositorio genérico
- IDirectQuery: Queries directas con Dapper
- IDataFunctions: Funciones PostgreSQL
- Sistema de migraciones
- Logging y diagnóstico
- Atributos de mapeo
- Paginación y filtrado
- SqlLiteralFormatter: literales SQL seguros
📖 Recursos adicionales
🚀 Inicio rápido
Instalación
dotnet add package Gesgocom.MicroOrmGesg
O compilar localmente:
dotnet build --configuration Release
Setup inicial
1. Instalar paquete NuGet:
dotnet add package Gesgocom.MicroOrmGesg
# Npgsql, Dapper y Newtonsoft.Json se instalan automáticamente como dependencias
2. Registrar servicios en Program.cs:
using MicroOrmGesg.Interfaces;
using MicroOrmGesg.Repository;
using Npgsql;
var builder = WebApplication.CreateBuilder(args);
// 1. NpgsqlDataSource (pool de conexiones)
builder.Services.AddSingleton(sp =>
{
var connectionString = builder.Configuration.GetConnectionString("PostgreSQL")!;
var dsBuilder = new NpgsqlDataSourceBuilder(connectionString);
// dsBuilder.UseNodaTime(); // Descomentar si usas NodaTime
return dsBuilder.Build();
});
// 2. DbSession para conexiones y transacciones
builder.Services.AddScoped<IDbSession, DbSession>();
// 3. Repositorio genérico
builder.Services.AddScoped(typeof(IDataMicroOrm<>), typeof(DataMicroOrmRepository<>));
// 4. Queries directas con Dapper (habilita snake_case → PascalCase automáticamente)
builder.Services.AddScoped<IDirectQuery, DirectQuery>();
// 5. Ejecutor de funciones PostgreSQL
builder.Services.AddScoped<IDataFunctions, DataFunctionsRepository>();
// 6. Compatibilidad de tipos (opcional, según tus modelos)
// NpgsqlDateTimeCompatibility.EnableDateTimeCompatibility(); // Si usas DateTime con date/time
// NodaTimeCompatibility.EnableDateTimeCompatibility(); // Si usas DateTime con UseNodaTime()
// 7. Logging (opcional pero recomendado)
builder.Logging.AddConsole();
builder.Logging.AddFilter("MicroOrmGesg", LogLevel.Information);
var app = builder.Build();
app.Run();
3. Configurar connection string en appsettings.json:
{
"ConnectionStrings": {
"PostgreSQL": "Host=localhost;Database=mydb;Username=user;Password=pass"
}
}
Tu primer CRUD
1. Define tu entidad:
using MicroOrmGesg;
// PostgreSQL: CREATE TYPE rol_enum AS ENUM ('Admin','Editor','Lector');
public enum Rol { Admin, Editor, Lector }
[Table("usuarios")]
public class Usuario
{
[Key]
public int Id { get; set; }
public string Nombre { get; set; } = null!;
public string Email { get; set; } = null!;
[PgEnum] // Enum PostgreSQL → se envía como texto
public Rol Rol { get; set; }
[SoftDelete]
public bool Eliminado { get; set; }
}
2. Crea un servicio:
public class UsuarioService
{
private readonly IDbSession _db;
private readonly IDataMicroOrm<Usuario> _repo;
public UsuarioService(IDbSession db, IDataMicroOrm<Usuario> repo)
{
_db = db;
_repo = repo;
}
public async Task<Usuario?> ObtenerAsync(int id, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _repo.GetByIdAsync(_db, id, ct);
}
public async Task<int> CrearAsync(Usuario usuario, CancellationToken ct)
{
await _db.OpenAsync(ct);
await _db.BeginTransactionAsync(ct: ct);
try
{
var id = await _repo.InsertAsyncReturnId(_db, usuario, ct);
await _db.CommitAsync(ct);
return (int)id!;
}
catch
{
await _db.RollbackAsync(ct);
throw;
}
}
}
3. Úsalo en un controlador:
[ApiController]
[Route("api/[controller]")]
public class UsuariosController : ControllerBase
{
private readonly UsuarioService _service;
public UsuariosController(UsuarioService service) => _service = service;
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id, CancellationToken ct)
{
var usuario = await _service.ObtenerAsync(id, ct);
return usuario is null ? NotFound() : Ok(usuario);
}
[HttpPost]
public async Task<IActionResult> Post(Usuario usuario, CancellationToken ct)
{
var id = await _service.CrearAsync(usuario, ct);
return CreatedAtAction(nameof(Get), new { id }, usuario);
}
}
✅ ¡Listo! Ya tienes CRUD funcional con transacciones, soft delete y logging.
📚 Casos de uso
¿Cuándo usar qué?
| Necesitas... | Usa | Ejemplo |
|---|---|---|
| CRUD simple de una tabla | IDataMicroOrm<T> |
await _repo.GetByIdAsync(...) |
| Query con JOINs o subconsultas | IDirectQuery |
await _query.QueryAsync<Dto>("SELECT...") |
| Llamar función/stored procedure | IDataFunctions |
await _funcs.CallFunctionAsync(...) |
| Operación con múltiples tablas | IDirectQuery + Transacción |
Ver Caso 3 |
| Paginación y filtros simples | IDataMicroOrm<T> |
await _repo.PageAsync(...) |
| Migrar esquema de base de datos | IPgMigrator |
Ver Caso 6 |
Caso 1: CRUD simple
Problema: Necesito operaciones básicas (crear, leer, actualizar, eliminar) en una tabla.
Solución: Usa IDataMicroOrm<T> (repositorio genérico)
public class ProductoService
{
private readonly IDbSession _db;
private readonly IDataMicroOrm<Producto> _repo;
// Leer
public async Task<Producto?> ObtenerAsync(int id, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _repo.GetByIdAsync(_db, id, ct);
}
// Listar con paginación
public async Task<Page<Producto>> ListarAsync(int page, int size, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _repo.PageAsync(_db, page, size, ct: ct);
}
// Crear
public async Task<int> CrearAsync(Producto producto, CancellationToken ct)
{
await _db.OpenAsync(ct);
var id = await _repo.InsertAsyncReturnId(_db, producto, ct);
return (int)id!;
}
// Actualizar
public async Task<bool> ActualizarAsync(Producto producto, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _repo.UpdateAsync(_db, producto, ct);
}
// Actualización parcial
public async Task<bool> ActualizarPrecioAsync(int id, decimal nuevoPrecio, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _repo.UpdateSetAsync(_db, id, new { precio = nuevoPrecio }, ct);
}
// Eliminar (soft delete si la entidad lo tiene configurado)
public async Task<bool> EliminarAsync(int id, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _repo.DeleteAsync(_db, id, ct);
}
}
Ventajas:
- No escribes SQL
- Convenciones automáticas (snake_case)
- Soft delete automático
- Type-safe
Ver más: Referencia IDataMicroOrm
Caso 2: Queries personalizadas
Problema: Necesito hacer un JOIN, una subconsulta o SQL específico.
Solución: Usa IDirectQuery para SQL directo con Dapper
public class ReporteService
{
private readonly IDbSession _db;
private readonly IDirectQuery _query;
// Query con JOIN
public async Task<List<UsuarioConPedidosDto>> ObtenerUsuariosConPedidosAsync(CancellationToken ct)
{
await _db.OpenAsync(ct);
const string sql = @"
SELECT
u.id,
u.nombre,
u.email,
COUNT(p.id) as total_pedidos,
SUM(p.total) as total_gastado
FROM usuarios u
LEFT JOIN pedidos p ON p.usuario_id = u.id
WHERE u.eliminado = false
GROUP BY u.id, u.nombre, u.email
HAVING COUNT(p.id) > 0
ORDER BY total_gastado DESC
LIMIT 100";
return (await _query.QueryAsync<UsuarioConPedidosDto>(_db, sql, ct: ct)).ToList();
}
// Query con parámetros
public async Task<PedidoDetalleDto?> ObtenerDetallePedidoAsync(int pedidoId, CancellationToken ct)
{
await _db.OpenAsync(ct);
const string sql = @"
SELECT
p.id,
p.fecha,
p.total,
u.nombre as nombre_usuario,
json_agg(json_build_object(
'producto', prod.nombre,
'cantidad', dp.cantidad,
'precio', dp.precio_unitario
)) as items
FROM pedidos p
INNER JOIN usuarios u ON u.id = p.usuario_id
LEFT JOIN detalle_pedido dp ON dp.pedido_id = p.id
LEFT JOIN productos prod ON prod.id = dp.producto_id
WHERE p.id = @pedidoId
GROUP BY p.id, p.fecha, p.total, u.nombre";
return await _query.QuerySingleOrDefaultAsync<PedidoDetalleDto>(
_db, sql, new { pedidoId }, ct);
}
// Múltiples result sets
public async Task<DashboardDto> ObtenerDashboardAsync(int usuarioId, CancellationToken ct)
{
await _db.OpenAsync(ct);
const string sql = @"
-- Result set 1: Usuario
SELECT id, nombre, email FROM usuarios WHERE id = @usuarioId;
-- Result set 2: Pedidos recientes
SELECT id, fecha, total FROM pedidos
WHERE usuario_id = @usuarioId
ORDER BY fecha DESC LIMIT 5;
-- Result set 3: Estadísticas
SELECT COUNT(*) as total_pedidos, SUM(total) as total_gastado
FROM pedidos WHERE usuario_id = @usuarioId";
await using var multi = await _query.QueryMultipleAsync(_db, sql, new { usuarioId }, ct);
var usuario = await multi.ReadSingleAsync<UsuarioDto>();
var pedidos = (await multi.ReadAsync<PedidoDto>()).ToList();
var stats = await multi.ReadSingleAsync<StatsDto>();
return new DashboardDto(usuario, pedidos, stats);
}
}
Ventajas:
- Flexibilidad total (cualquier SQL)
- JOINs, CTEs, window functions
- Comparte conexión/transacción con el repositorio
Ver más: Referencia IDirectQuery
Caso 3: Transacciones
Problema: Necesito que varias operaciones se ejecuten atómicamente (todo o nada).
Solución: Usa IDbSession.BeginTransactionAsync() + Commit/Rollback
public class PedidoService
{
private readonly IDbSession _db;
private readonly IDataMicroOrm<Pedido> _pedidoRepo;
private readonly IDirectQuery _query;
public async Task<int> CrearPedidoAsync(CrearPedidoDto dto, CancellationToken ct)
{
await _db.OpenAsync(ct);
await _db.BeginTransactionAsync(ct: ct);
try
{
// 1. Crear pedido
var pedido = new Pedido
{
UsuarioId = dto.UsuarioId,
Fecha = DateTime.UtcNow,
Total = dto.Items.Sum(i => i.Precio * i.Cantidad)
};
var pedidoId = (int)(await _pedidoRepo.InsertAsyncReturnId(_db, pedido, ct))!;
// 2. Insertar items del pedido
foreach (var item in dto.Items)
{
const string insertItem = @"
INSERT INTO detalle_pedido (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (@pedidoId, @productoId, @cantidad, @precio)";
await _query.ExecuteAsync(_db, insertItem, new
{
pedidoId,
productoId = item.ProductoId,
cantidad = item.Cantidad,
precio = item.Precio
}, ct);
}
// 3. Actualizar stock de productos
foreach (var item in dto.Items)
{
const string updateStock = @"
UPDATE productos
SET stock = stock - @cantidad
WHERE id = @productoId AND stock >= @cantidad";
var rowsAffected = await _query.ExecuteAsync(_db, updateStock, new
{
productoId = item.ProductoId,
cantidad = item.Cantidad
}, ct);
if (rowsAffected == 0)
throw new InvalidOperationException($"Stock insuficiente para producto {item.ProductoId}");
}
// 4. Confirmar transacción
await _db.CommitAsync(ct);
return pedidoId;
}
catch
{
// Rollback automático en caso de error
await _db.RollbackAsync(ct);
throw;
}
}
}
Ventajas:
- Atomicidad garantizada
- Rollback automático en excepciones
- Puedes mezclar repositorio + queries directas
Ver más: Referencia DbSession
Caso 4: Funciones PostgreSQL
Problema: Tengo lógica compleja en funciones/stored procedures de PostgreSQL.
Solución: Usa IDataFunctions para invocarlas
public class AuthService
{
private readonly IDbSession _db;
private readonly IDataFunctions _funcs;
// Función que devuelve un escalar
public async Task<string?> GenerarTokenRecuperacionAsync(int usuarioId, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _funcs.CallFunctionAsync<string>(
_db,
"generar_token_recuperacion",
new { p_usuario_id = usuarioId, p_duracion_horas = 24 },
schema: "auth",
ct);
}
// Función que devuelve una tabla (SETOF o TABLE)
public async Task<List<ValidacionDto>> ValidarTokenAsync(string token, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _funcs.CallFunctionListAsync<ValidacionDto>(
_db,
"validar_token_recuperacion",
new { p_token = token },
schema: "auth",
ct);
}
// Función void (sin retorno)
public async Task IncrementarIntentoAsync(string token, CancellationToken ct)
{
await _db.OpenAsync(ct);
await _funcs.CallVoidFunctionAsync(
_db,
"incrementar_intento_token",
new { p_token = token },
schema: "auth",
ct);
}
}
Ejemplo de función PostgreSQL:
CREATE OR REPLACE FUNCTION auth.generar_token_recuperacion(
p_usuario_id int,
p_duracion_horas int DEFAULT 24
)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_token text;
BEGIN
v_token := encode(gen_random_bytes(32), 'hex');
INSERT INTO auth.tokens_recuperacion (usuario_id, token, expira_en)
VALUES (p_usuario_id, v_token, now() + (p_duracion_horas || ' hours')::interval);
RETURN v_token;
END;
$$;
Ver más: Referencia IDataFunctions
Caso 5: JSONB
Problema: Necesito almacenar/consultar datos en formato JSON.
Solución: Usa JObject de Newtonsoft.Json, Dictionary<,> o cualquier tipo complejo con el atributo [Jsonb].
MicroOrmGesg detecta automáticamente como JSONB:
JObject,JArray,JToken(siempre)Dictionary<TKey, TValue>(siempre, desde v1.2.0)- Cualquier propiedad marcada con
[Jsonb]
1. Registrar TypeHandlers (una vez en Program.cs):
using Dapper;
using MicroOrmGesg.Utils;
SqlMapper.AddTypeHandler(new JObjectTypeHandler());
SqlMapper.AddTypeHandler(new JArrayTypeHandler());
SqlMapper.AddTypeHandler(new JTokenTypeHandler());
2. Define tu entidad con JSONB:
using Newtonsoft.Json.Linq;
using MicroOrmGesg;
[Table("usuarios")]
public class Usuario
{
[Key]
public int Id { get; set; }
public string Nombre { get; set; } = null!;
[Jsonb]
public JObject? Preferencias { get; set; }
[Jsonb]
public JObject? Metadatos { get; set; }
// Dictionary se detecta como JSONB automáticamente (sin atributo)
public Dictionary<string, string> Tags { get; set; } = new();
}
3. Úsalo normalmente:
// Crear con JSONB
var usuario = new Usuario
{
Nombre = "Juan",
Preferencias = JObject.FromObject(new
{
tema = "dark",
idioma = "es",
notificaciones = true
}),
Metadatos = JObject.FromObject(new
{
ip_registro = "192.168.1.1",
navegador = "Chrome"
})
};
await _db.OpenAsync(ct);
var id = await _repo.InsertAsyncReturnId(_db, usuario, ct);
// Leer
var usuarioLeido = await _repo.GetByIdAsync(_db, (int)id!, ct);
var tema = usuarioLeido?.Preferencias?["tema"]?.ToString(); // "dark"
// Actualizar parcialmente el JSONB
var nuevoJson = JObject.FromObject(new { tema = "light", idioma = "en" });
await _repo.UpdateSetAsync(_db, (int)id!, new { preferencias = nuevoJson }, ct);
4. Consultas con operadores JSONB:
const string sql = @"
SELECT * FROM usuarios
WHERE preferencias->>'tema' = @tema
AND preferencias->'notificaciones' = 'true'::jsonb";
var usuarios = await _query.QueryAsync<Usuario>(
_db, sql, new { tema = "dark" }, ct);
Ver más: Extensiones JSON | Filtros JSONB | GetJsonPartAsync
Caso 6: Migraciones
Problema: Necesito gestionar cambios en el esquema de base de datos de forma controlada.
Solución: Usa el sistema de migraciones integrado
1. Configura migraciones en Program.cs:
using MicroOrmGesg.Migrations.Extensions;
using MicroOrmGesg.Migrations.Models;
builder.Services.AddPgMigrations(options =>
{
options.AdvisoryLockKey = "myapp:migrations";
options.DriftPolicy = DriftPolicy.WarnAndSkip;
options.StopOnError = true;
});
2. Crea tu archivo de migraciones (scripts/schema.sql):
-- @step id:001 name:create.usuarios
CREATE TABLE IF NOT EXISTS usuarios(
id serial PRIMARY KEY,
nombre text NOT NULL,
email text NOT NULL UNIQUE,
eliminado boolean NOT NULL DEFAULT false
);
-- @step id:002 name:index.usuarios.email
CREATE INDEX IF NOT EXISTS idx_usuarios_email ON usuarios(email) WHERE eliminado = false;
-- @step id:003 name:create.pedidos
CREATE TABLE IF NOT EXISTS pedidos(
id serial PRIMARY KEY,
usuario_id int NOT NULL REFERENCES usuarios(id),
fecha timestamptz NOT NULL DEFAULT now(),
total decimal(10,2) NOT NULL
);
-- @step id:004 name:alter.usuarios.add_telefono
-- @check SELECT EXISTS(
-- SELECT 1 FROM information_schema.columns
-- WHERE table_name='usuarios' AND column_name='telefono'
-- );
ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS telefono text;
3. Ejecuta migraciones al inicio:
using MicroOrmGesg.Migrations;
var app = builder.Build();
// Ejecutar migraciones antes de iniciar
using (var scope = app.Services.CreateScope())
{
var migrator = scope.ServiceProvider.GetRequiredService<IPgMigrator>();
var source = new FileMigrationSource("./scripts/schema.sql");
var result = await migrator.RunAsync(source);
if (!result.IsSuccess)
{
Console.WriteLine($"Migraciones fallidas: {result.StepsFailed} pasos");
Environment.Exit(1);
}
}
app.Run();
Ventajas:
- Idempotentes (puedes ejecutarlas múltiples veces)
- Detección de drift (cambios no autorizados)
- Advisory locks (seguro en multi-instancia)
- Transacciones por paso
Ver más: Referencia completa de migraciones
🔧 Referencia completa
DbSession: Conexiones y transacciones
IDbSession gestiona una conexión y opcionalmente una transacción por scope (típicamente por request HTTP).
Métodos principales
public interface IDbSession
{
NpgsqlConnection? Connection { get; }
NpgsqlTransaction? Transaction { get; }
Task<NpgsqlConnection> OpenAsync(CancellationToken ct = default);
Task BeginTransactionAsync(IsolationLevel isolation = IsolationLevel.ReadCommitted, CancellationToken ct = default);
Task CommitAsync(CancellationToken ct = default);
Task RollbackAsync(CancellationToken ct = default);
Task ResetParaReintentoAsync(); // v1.2.2+
void Close();
}
Ciclo de vida típico
await _db.OpenAsync(ct); // 1. Abrir conexión
await _db.BeginTransactionAsync(ct: ct); // 2. Iniciar transacción (opcional)
try
{
// ... operaciones de base de datos ...
await _db.CommitAsync(ct); // 3. Confirmar
}
catch
{
await _db.RollbackAsync(ct); // 4. Revertir en caso de error
throw;
}
// 5. Dispose automático al finalizar el scope
Niveles de aislamiento
// Por defecto: ReadCommitted
await _db.BeginTransactionAsync(ct: ct);
// Serializable (más estricto)
await _db.BeginTransactionAsync(IsolationLevel.Serializable, ct);
// RepeatableRead
await _db.BeginTransactionAsync(IsolationLevel.RepeatableRead, ct);
ResetParaReintentoAsync (v1.2.2+)
Permite reutilizar una IDbSession tras un fallo transitorio de conexión, limpiando la transacción y conexión rotas sin marcar el objeto como disposed. Diseñado para integrarse con Polly u otros frameworks de resiliencia.
// Ejemplo con Polly v8
var pipeline = pipelineProvider.GetPipeline("db-retry");
await pipeline.ExecuteAsync(async token =>
{
await _db.ResetParaReintentoAsync(); // Limpia estado previo (no-op en primer intento)
await _db.OpenAsync(token);
await _db.BeginTransactionAsync(ct: token);
try
{
// ... operaciones de BD ...
await _db.CommitAsync(token);
}
catch
{
try { await _db.RollbackAsync(token); } catch { }
throw;
}
}, ct);
Logs generados
Con LogLevel.Debug:
[DBG] Abriendo nueva conexión a la base de datos desde el pool
[DBG] Iniciando transacción con nivel de aislamiento ReadCommitted
[DBG] Confirmando transacción (COMMIT)
IDataMicroOrm: Repositorio genérico
Repositorio tipado que proporciona CRUD completo sin escribir SQL.
Métodos disponibles
public interface IDataMicroOrm<T> where T : class
{
// Lectura
Task<T?> GetByIdAsync(IDbSession session, object id, CancellationToken ct = default);
Task<List<T>> GetAllAsync(IDbSession session, bool includeSoftDeleted = false, string? orderBy = null, ...);
Task<int> CountAsync(IDbSession session, bool includeSoftDeleted = false, ...);
Task<Page<T>> PageAsync(IDbSession session, int page, int size, ...);
// Escritura
Task<int> InsertAsync(IDbSession session, T data, CancellationToken ct = default);
Task<object?> InsertAsyncReturnId(IDbSession session, T data, CancellationToken ct = default);
Task<bool> UpdateAsync(IDbSession session, T data, CancellationToken ct = default);
Task<bool> UpdateSetAsync(IDbSession session, object id, object patch, CancellationToken ct = default);
Task<bool> DeleteAsync(IDbSession session, object id, CancellationToken ct = default);
}
Convenciones automáticas
| Convención | Ejemplo C# | SQL generado |
|---|---|---|
| Nombre de clase → tabla | Usuario |
usuarios (snake_case) |
| Nombre de propiedad → columna | NombreCompleto |
nombre_completo |
[Table("...")] |
[Table("users")] |
users (literal) |
[Column("...")] |
[Column("full_name")] |
full_name (literal) |
[Key] |
public int Id |
PRIMARY KEY autoincrement |
[SoftDelete] |
public bool Eliminado |
DELETE → UPDATE eliminado=true |
Paginación y filtros
var page = await _repo.PageAsync(
_db,
page: 2, // Página 2
size: 10, // 10 elementos por página
includeSoftDeleted: false, // Excluir eliminados
orderBy: "FechaCreacion", // Ordenar por propiedad C#
dir: SortDirection.Desc, // Descendente
filterField: "Nombre", // Filtrar por columna
filterValue: "Juan", // Valor a buscar
stringMode: StringFilterMode.Contains, // Coincidencia parcial
forceLowerCase: true, // Case-insensitive
ct: ct
);
Console.WriteLine($"Total: {page.Total}");
Console.WriteLine($"Página {page.PageNumber} de {Math.Ceiling(page.Total / (double)page.Size)}");
foreach (var item in page.Items)
{
// ...
}
UpdateSetAsync (PATCH parcial)
// Actualizar solo el email
await _repo.UpdateSetAsync(_db, usuarioId, new { email = "nuevo@example.com" }, ct);
// Actualizar múltiples campos
await _repo.UpdateSetAsync(_db, usuarioId, new
{
email = "nuevo@example.com",
nombre = "Nuevo Nombre",
telefono = "123456789"
}, ct);
// También acepta snake_case
await _repo.UpdateSetAsync(_db, usuarioId, new { password_hash = "xxx" }, ct);
Importante: UpdateSetAsync ignora automáticamente:
- La primary key (no se puede cambiar)
- Columnas de soft delete
- Columnas marcadas con
[Computed]o[Write(Include = false)]
InsertBulkAsync (inserción masiva)
Para insertar grandes volúmenes de datos, usa InsertBulkAsync que utiliza el protocolo binario COPY de PostgreSQL. Es órdenes de magnitud más rápido que múltiples llamadas a InsertAsync.
// Generar 10,000 productos de ejemplo
var productos = Enumerable.Range(1, 10000)
.Select(i => new Producto
{
Nombre = $"Producto {i}",
Precio = 9.99m + (i % 100),
Stock = i % 500
})
.ToList();
await _db.OpenAsync(ct);
await _db.BeginTransactionAsync(ct: ct); // Opcional pero recomendado
try
{
var insertados = await _repo.InsertBulkAsync(_db, productos, ct);
Console.WriteLine($"Insertados: {insertados} registros");
await _db.CommitAsync(ct);
}
catch
{
await _db.RollbackAsync(ct);
throw;
}
Características:
- Usa
COPY ... FROM STDIN (FORMAT BINARY)de PostgreSQL - Tipos .NET:
int,long,short,byte,decimal,double,float,string,bool,DateTime,DateTimeOffset,DateOnly,TimeOnly,TimeSpan,Guid,byte[], enums - Tipos NodaTime:
Instant,LocalDateTime,LocalDate,LocalTime,OffsetDateTime,ZonedDateTime,Period,Duration - JSONB: propiedades con
[Jsonb],JObject,JArray,JToken,Dictionary<,>(auto-detectado) - Enums PostgreSQL: propiedades con
[PgEnum]se envían como texto automáticamente - Arrays y listas:
int[],string[],Guid[],decimal[],double[],bool[],List<T>,IList<T> - Excluye automáticamente columnas
[Key],[Computed],[Write(Include=false)] - Retorna el número de registros insertados (
ulong)
Cuándo usar:
| Escenario | Método recomendado |
|-----------|-------------------|
| 1-10 registros | InsertAsync / InsertAsyncReturnId |
| 10-100 registros | Depende del caso (considera bulk) |
| 100+ registros | InsertBulkAsync |
| Importación masiva (CSV, logs) | InsertBulkAsync |
Nota: InsertBulkAsync NO retorna los IDs generados. Si necesitas los IDs, usa InsertAsyncReturnId en un bucle o considera una estrategia de IDs explícitos ([ExplicitKey] con GUIDs).
Extensiones JSON (JsonExtensions)
Utilidades para trabajar con datos JSONB de forma fluida y segura.
GetPath<T> - Acceso por ruta (JsonPath fluido)
Navega por JSON usando rutas separadas por puntos, con soporte para arrays:
using MicroOrmGesg.Utils;
var config = JObject.Parse(@"{
""meta"": {""version"": ""1.0""},
""views"": {""detail"": {""rows"": [{""slots"": [{""label"": ""Fecha""}]}]}}
}");
// Acceso simple
var version = config.GetPath<string>("meta.version"); // "1.0"
// Acceso a arrays con índices
var label = config.GetPath<string>("views.detail.rows[0].slots[0].label"); // "Fecha"
// Valor por defecto si no existe
var missing = config.GetPath<int>("missing.path", -1); // -1
// Verificar si existe la ruta
if (config.HasPath("meta.version")) { ... }
// Establecer valor (crea nodos intermedios)
config.SetPath("settings.theme", "dark");
GetTranslation - Soporte multi-idioma
Busca traducciones en arrays con formato [{"etiqueta": "Texto", "ididioma": 1}, ...]:
var labels = JArray.Parse(@"[
{""etiqueta"": ""Fecha"", ""ididioma"": 1},
{""etiqueta"": ""Date"", ""ididioma"": 2}
]");
// Obtener traducción por idioma
var spanish = labels.GetTranslation("etiqueta", languageId: 1); // "Fecha"
var english = labels.GetTranslation("etiqueta", languageId: 2); // "Date"
// Con fallback a otro idioma
var text = labels.GetTranslationWithFallback("etiqueta", 3, fallbackLanguageId: 1); // "Fecha"
// Obtener todas las traducciones
var all = labels.GetAllTranslations("etiqueta"); // {1: "Fecha", 2: "Date"}
// Buscar en ruta anidada
var title = config.GetTranslationAt("labels.title", "etiqueta", 1);
Filtros JSONB en consultas
Filtra por campos dentro de columnas JSONB usando operadores PostgreSQL:
// Filtrar por valor dentro de JSONB
// SQL: WHERE preferencias->'meta'->>'version' ILIKE '%1.0%'
var results = await _repo.GetAllAsync(
_db,
filterField: "preferencias->'meta'->>'version'",
filterValue: "1.0",
stringMode: StringFilterMode.Contains
);
// En PageAsync
var page = await _repo.PageAsync(
_db,
page: 1,
size: 10,
filterField: "config->>'status'",
filterValue: "active"
);
Nota: Cuando filterField contiene -> o ->>, se valida con regex estricto (previene inyección SQL) y se usa como expresión JSONB en el WHERE.
GetJsonPartAsync - Proyección parcial de JSONB
Recupera solo una parte de un campo JSONB grande:
// Obtener solo los filtros de una configuración grande
var filters = await _repo.GetJsonPartAsync<List<FilterDto>>(
_db,
configId,
jsonColumn: "configuracion",
path: "filters",
ct
);
// Obtener metadata anidada
var meta = await _repo.GetJsonPartAsync<MetaInfo>(
_db,
id,
jsonColumn: "datos",
path: "meta.info",
ct
);
// SQL generado: SELECT "configuracion"->>'filters' FROM tabla WHERE id = @id
// Para rutas anidadas: SELECT "datos"->'meta'->>'info' FROM tabla WHERE id = @id
Uso ideal: Cuando tienes configuraciones JSONB muy grandes y solo necesitas un fragmento específico.
IDirectQuery: Queries directas con Dapper
Ejecuta SQL personalizado compartiendo la misma conexión y transacción de IDbSession.
DirectQuery habilita automáticamente el mapeo snake_case → PascalCase de Dapper (DefaultTypeMap.MatchNamesWithUnderscores). Esto significa que puedes mapear directamente columnas PostgreSQL a DTOs en PascalCase sin alias ni configuración:
// PostgreSQL devuelve: fecha_creacion, nombre_completo, total_pedidos
// C# mapea automáticamente a: FechaCreacion, NombreCompleto, TotalPedidos
var resultados = await _query.QueryAsync<ReporteDto>(_db,
"SELECT fecha_creacion, nombre_completo, total_pedidos FROM vista_reportes", ct: ct);
Métodos disponibles
public interface IDirectQuery
{
Task<IEnumerable<T>> QueryAsync<T>(...);
Task<T> QuerySingleAsync<T>(...);
Task<T?> QuerySingleOrDefaultAsync<T>(...);
Task<T> QueryFirstAsync<T>(...);
Task<T?> QueryFirstOrDefaultAsync<T>(...);
Task<int> ExecuteAsync(...);
Task<T?> ExecuteScalarAsync<T>(...);
Task<SqlMapper.GridReader> QueryMultipleAsync(...);
}
Cuándo usar cada método
| Método | Uso | Ejemplo |
|---|---|---|
QueryAsync<T> |
Múltiples filas | SELECT * FROM usuarios |
QuerySingleAsync<T> |
Exactamente 1 fila (error si 0 o >1) | SELECT * FROM usuarios WHERE id = @id |
QuerySingleOrDefaultAsync<T> |
0 o 1 fila (error si >1) | Lo mismo, pero retorna null si no existe |
QueryFirstAsync<T> |
Al menos 1 fila (toma la primera) | SELECT * FROM usuarios LIMIT 1 |
QueryFirstOrDefaultAsync<T> |
0 o más filas (toma la primera o null) | Lo mismo, pero retorna null si vacío |
ExecuteAsync |
INSERT/UPDATE/DELETE | Retorna filas afectadas |
ExecuteScalarAsync<T> |
Un solo valor | SELECT COUNT(*) ... |
QueryMultipleAsync |
Múltiples result sets | Varios SELECT en una llamada |
Ejemplos rápidos
// SELECT múltiple
var usuarios = await _query.QueryAsync<Usuario>(
_db, "SELECT * FROM usuarios WHERE activo = @activo",
new { activo = true }, ct);
// INSERT con RETURNING
var nuevoId = await _query.ExecuteScalarAsync<int>(
_db, "INSERT INTO logs(mensaje) VALUES(@msg) RETURNING id",
new { msg = "Log entry" }, ct);
// UPDATE
var rowsAffected = await _query.ExecuteAsync(
_db, "UPDATE productos SET stock = stock - @qty WHERE id = @id",
new { qty = 5, id = 10 }, ct);
// COUNT
var total = await _query.ExecuteScalarAsync<int>(
_db, "SELECT COUNT(*) FROM pedidos WHERE fecha > @fecha",
new { fecha = DateTime.Today.AddDays(-30) }, ct);
Logs generados
[DBG] Ejecutando QueryAsync<Usuario>: SELECT * FROM usuarios WHERE activo = @activo
[DBG] QueryAsync<Usuario> ejecutado exitosamente
[DBG] Ejecutando ExecuteAsync (comando): UPDATE productos SET stock = ...
[DBG] ExecuteAsync completado: 1 fila(s) afectada(s)
IDataFunctions: Funciones PostgreSQL
Invoca funciones almacenadas en PostgreSQL sin escribir SQL manualmente.
Métodos
public interface IDataFunctions
{
// Función que devuelve un escalar
Task<TResult?> CallFunctionAsync<TResult>(
IDbSession session, string functionName, object? args = null,
string? schema = null, CancellationToken ct = default);
// Función que devuelve tabla (SETOF/TABLE)
Task<List<TResult>> CallFunctionListAsync<TResult>(
IDbSession session, string functionName, object? args = null,
string? schema = null, CancellationToken ct = default);
// Función void (sin retorno)
Task CallVoidFunctionAsync(
IDbSession session, string functionName, object? args = null,
string? schema = null, CancellationToken ct = default);
}
Parámetros
Puedes pasar argumentos de dos formas:
1. Objeto anónimo:
await _funcs.CallFunctionAsync<string>(
_db, "generar_token",
new { p_usuario_id = 123, p_duracion = 24 },
ct: ct);
2. Diccionario:
var args = new Dictionary<string, object?>
{
["p_usuario_id"] = 123,
["p_duracion"] = 24
};
await _funcs.CallFunctionAsync<string>(_db, "generar_token", args, ct: ct);
El prefijo @ se elimina automáticamente si lo incluyes.
SQL generado
// CallFunctionAsync (escalar)
await _funcs.CallFunctionAsync<int>(_db, "sumar", new { a = 5, b = 3 });
// SQL: SELECT sumar(@a, @b)
// CallFunctionListAsync (tabla)
await _funcs.CallFunctionListAsync<Usuario>(_db, "obtener_usuarios_activos");
// SQL: SELECT * FROM obtener_usuarios_activos()
// Con schema
await _funcs.CallFunctionAsync<string>(_db, "generar_hash", args, schema: "auth");
// SQL: SELECT auth.generar_hash(@password)
Sistema de migraciones
Sistema completo de migraciones idempotentes con checksums SHA-256, advisory locks y detección de drift.
Configuración
builder.Services.AddPgMigrations(options =>
{
options.AdvisoryLockKey = "myapp:migrations"; // Lock único por app
options.CommandTimeoutSeconds = 120; // Timeout de comandos
options.DriftPolicy = DriftPolicy.WarnAndSkip; // Fail, WarnAndSkip, Reapply
options.StopOnError = true; // Detener al primer error
options.JournalTableName = "__micro_orm_migrations"; // Tabla de historial
options.JournalSchema = null; // Schema (null = public)
});
Formato de script SQL
Directiva @step:
-- @step id:001 name:create.usuarios
id: Identificador único (001, 002, 003... o 001-create-users)name: Descripción (usa puntos: create.tabla, alter.tabla.columna)
Directiva @check (opcional):
-- @check SELECT to_regclass('public.usuarios') IS NOT NULL;
- SQL que devuelve boolean
true= ya aplicado,false= necesita aplicarse- Solo usar cuando no tienes
IF NOT EXISTS
Cuándo usar @check
| SQL | ¿Necesitas @check? | Razón |
|---|---|---|
CREATE TABLE IF NOT EXISTS |
❌ NO | Ya es idempotente |
CREATE INDEX IF NOT EXISTS |
❌ NO | Ya es idempotente |
CREATE OR REPLACE FUNCTION |
❌ NO | Ya es idempotente |
ALTER TABLE ADD COLUMN IF NOT EXISTS |
❌ NO | Ya es idempotente |
ALTER TABLE ALTER COLUMN TYPE |
✅ SÍ | No tiene IF NOT EXISTS |
ALTER TABLE ADD CONSTRAINT |
✅ SÍ | PostgreSQL < 16 no tiene IF NOT EXISTS |
INSERT INTO ... |
✅ SÍ | Para evitar duplicados |
| Migrar a BD existente | ✅ SÍ | Para adoptar objetos pre-existentes |
Ejemplo completo
-- Tabla con IF NOT EXISTS (sin @check)
-- @step id:001 name:create.usuarios
CREATE TABLE IF NOT EXISTS usuarios(
id serial PRIMARY KEY,
email text NOT NULL UNIQUE
);
-- Índice con IF NOT EXISTS (sin @check)
-- @step id:002 name:index.usuarios.email
CREATE INDEX IF NOT EXISTS idx_usuarios_email ON usuarios(email);
-- ALTER COLUMN sin IF NOT EXISTS (CON @check)
-- @step id:003 name:alter.usuarios.email_varchar
-- @check SELECT EXISTS(
-- SELECT 1 FROM information_schema.columns
-- WHERE table_name='usuarios' AND column_name='email'
-- AND data_type='character varying' AND character_maximum_length=255
-- );
ALTER TABLE usuarios ALTER COLUMN email TYPE varchar(255);
-- Función con CREATE OR REPLACE (sin @check)
-- @step id:004 name:function.update_timestamp
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Datos iniciales (CON @check)
-- @step id:005 name:insert.admin
-- @check SELECT EXISTS(SELECT 1 FROM usuarios WHERE email = 'admin@example.com');
INSERT INTO usuarios(email) VALUES('admin@example.com');
Ejecución
Opción 1: Manual en Program.cs
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var migrator = scope.ServiceProvider.GetRequiredService<IPgMigrator>();
var source = new FileMigrationSource("./scripts/schema.sql");
var result = await migrator.RunAsync(source);
if (!result.IsSuccess)
Environment.Exit(1);
}
app.Run();
Opción 2: IHostedService
builder.Services.AddHostedService<MigrationHostedService>();
Políticas de drift
Cuando un paso ya aplicado tiene un checksum diferente:
| Política | Comportamiento |
|---|---|
Fail |
Lanza excepción (fuerza corrección manual) |
WarnAndSkip |
Registra warning y omite (default) |
Reapply |
Re-ejecuta el paso (útil para funciones/vistas) |
Generar migraciones con LLM
Puedes usar este prompt con Claude/GPT para generar automáticamente las directivas:
Tengo las siguientes sentencias SQL de PostgreSQL y necesito generar
las directivas @step y @check para el sistema de migraciones de MicroOrmGesg.
REGLAS:
1. Si tiene IF NOT EXISTS, CREATE OR REPLACE, o ADD COLUMN IF NOT EXISTS → NO generar @check
2. Para CREATE TABLE sin IF NOT EXISTS → usar to_regclass('schema.tabla')
3. Para ALTER TABLE ALTER COLUMN TYPE → usar information_schema.columns
4. Para INSERT INTO → usar EXISTS(SELECT 1 FROM tabla WHERE condicion_unica)
FORMATO:
-- @step id:XXX name:descripcion.del.paso
-- @check SELECT ...; (solo si es necesario)
[SQL original]
SENTENCIAS SQL:
[Pega aquí tu SQL]
Logging y diagnóstico
Todos los componentes incluyen logging con ILogger<T>.
Componentes con logging
| Componente | Qué registra |
|---|---|
DbSession |
Conexiones, transacciones, commits, rollbacks |
DirectQuery |
Queries ejecutadas, tipo de resultado, filas afectadas |
DataFunctionsRepository |
Funciones invocadas, esquema, resultados |
PgMigrator |
Ejecución completa de migraciones |
Niveles usados
| Nivel | Cuándo |
|---|---|
Debug |
Operaciones normales (conexión, query, commit) |
Information |
Eventos importantes (migraciones aplicadas) |
Warning |
Situaciones anómalas (rollback, drift) |
Error |
Excepciones y errores |
Configuración
// Desarrollo: Todo en Debug
if (builder.Environment.IsDevelopment())
{
builder.Logging.AddFilter("MicroOrmGesg", LogLevel.Debug);
}
else
{
// Producción: Solo Information y superiores
builder.Logging.AddFilter("MicroOrmGesg", LogLevel.Information);
}
// Filtro selectivo por componente
builder.Logging.AddFilter("MicroOrmGesg.Repository.DbSession", LogLevel.Debug);
builder.Logging.AddFilter("MicroOrmGesg.Migrations", LogLevel.Information);
Ejemplo de logs
[10:15:32 DBG] Abriendo nueva conexión a la base de datos desde el pool
[10:15:32 DBG] Iniciando transacción con nivel de aislamiento ReadCommitted
[10:15:32 DBG] Ejecutando QueryAsync<Usuario>: SELECT * FROM usuarios WHERE id = @id
[10:15:32 DBG] QueryAsync<Usuario> ejecutado exitosamente
[10:15:32 DBG] Confirmando transacción (COMMIT)
[10:15:32 ERR] Error ejecutando QueryAsync<Producto>: SELECT * FROM productos...
[10:17:21 WRN] Revirtiendo transacción (ROLLBACK)
Importante: Los logs incluyen SQL pero NO los valores de los parámetros (seguridad).
Atributos de mapeo
| Atributo | Uso | Ejemplo |
|---|---|---|
[Table("nombre")] |
Nombre de tabla | [Table("users")] |
[Column("nombre")] |
Nombre de columna | [Column("full_name")] |
[Key] |
Primary key autoincrement | [Key] public int Id |
[ExplicitKey] |
Primary key manual (no autoincrement) | [ExplicitKey] public string Code |
[SoftDelete] |
Columna de soft delete | [SoftDelete] public bool Eliminado |
[Computed] |
Solo lectura (no incluir en INSERT/UPDATE) | [Computed] public DateTime CreatedAt |
[Write(Include = false)] |
Excluir de escritura | [Write(Include = false)] |
[Jsonb] |
Columna JSONB | [Jsonb] public JObject Data |
[PgEnum] |
Enum PostgreSQL (envía como texto) | [PgEnum] public MiEnum Estado |
Detección automática JSONB (v1.2.0): Las propiedades
Dictionary<,>,JObject,JArrayyJTokense detectan como JSONB automáticamente sin necesidad de[Jsonb]. El atributo sigue siendo útil para tipos personalizados que quieras serializar como JSON.
Ejemplo completo
// PostgreSQL: CREATE TYPE rol_enum AS ENUM ('Admin','Editor','Lector');
public enum Rol { Admin, Editor, Lector }
[Table("usuarios", Schema = "public")]
public class Usuario
{
[Key]
public int Id { get; set; }
public string Nombre { get; set; } = null!; // → nombre (snake_case automático)
[Column("email_address")]
public string Email { get; set; } = null!; // → email_address (literal)
[Column("password_hash")]
public string PasswordHash { get; set; } = null!;
[PgEnum]
public Rol Rol { get; set; } // Enum PG → se envía como texto "Admin"
[Computed]
public DateTime CreatedAt { get; set; } // Solo lectura
[SoftDelete]
public bool Eliminado { get; set; } // DELETE → UPDATE eliminado=true
[Jsonb]
public JObject? Preferencias { get; set; } // JSONB explícito
public Dictionary<string, object> Config { get; set; } = new(); // JSONB auto-detectado
[Write(Include = false)]
public string TempPassword { get; set; } = null!; // No se incluye en INSERT/UPDATE
}
Paginación y filtrado
PageAsync
public async Task<Page<Usuario>> ListarAsync(int page, int size, string? busqueda, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _repo.PageAsync(
_db,
page: page, // Número de página (1-based)
size: size, // Elementos por página
includeSoftDeleted: false, // Excluir eliminados
orderBy: "FechaCreacion", // Columna para ordenar
dir: SortDirection.Desc, // Ascendente/Descendente
filterField: "Nombre", // Filtrar por campo
filterValue: busqueda, // Valor a buscar
stringMode: StringFilterMode.Contains, // Equals, Contains, StartsWith, EndsWith
forceLowerCase: true, // Case-insensitive
ct: ct
);
}
Resultado Page
public class Page<T>
{
public List<T> Items { get; init; } // Elementos de la página actual
public int Total { get; init; } // Total de registros
public int PageNumber { get; init; } // Página actual
public int Size { get; init; } // Tamaño de página
}
StringFilterMode
| Modo | SQL generado | Ejemplo |
|---|---|---|
Equals |
columna = @valor |
"Juan" |
Contains |
columna ILIKE '%' \|\| @valor \|\| '%' |
Buscar "uan" encuentra "Juan" |
StartsWith |
columna ILIKE @valor \|\| '%' |
"Ju" encuentra "Juan" |
EndsWith |
columna ILIKE '%' \|\| @valor |
"an" encuentra "Juan" |
SqlLiteralFormatter: literales SQL seguros
SqlLiteralFormatter.Format(object? valor) convierte cualquier valor CLR a un literal SQL seguro para PostgreSQL. Pensado para generar scripts SQL planos (exportaciones, dumps, migraciones) donde no se pueden usar parámetros Dapper.
Regla de oro: En queries normales usa SIEMPRE parámetros (
@param). Este helper solo para scripts donde el SQL tiene que ser texto final (se escribe a fichero, se reimporta, se envía a un cliente que no soporta parámetros, etc).
¿Por qué existe?
Cuando NpgsqlDataSourceBuilder.UseNodaTime() está habilitado y haces Dapper.QueryAsync sin tipo genérico (devuelve DapperRow/dynamic), los TypeHandlers de Dapper no se aplican — Npgsql entrega tipos NodaTime crudos (Instant, LocalDateTime, LocalDate, LocalTime).
Si formateas esos valores con .ToString() heredas la cultura del proceso. En un contenedor con locale es-ES una fecha se convierte en "13/4/2026 18:32:50" — formato que PostgreSQL rechaza al reimportar (22008: date/time field value out of range).
SqlLiteralFormatter resuelve esto serializando siempre en ISO 8601 con CultureInfo.InvariantCulture, independiente del locale del proceso.
Tipos soportados
| Categoría | Tipos | Formato producido |
|---|---|---|
| Nulos | null, DBNull.Value |
NULL |
| Booleanos | bool |
true / false |
| Enteros | int, long, short, byte, sbyte, uint, ulong, ushort |
literal numérico |
| Decimales | decimal, float, double |
literal con . como separador (InvariantCulture) |
| Enums | Enum |
valor numérico subyacente |
| Identidades | Guid |
'xxxxxxxx-xxxx-...' |
| Binario | byte[] |
'\xHEX' (bytea de PostgreSQL) |
| BCL fechas | DateTime, DateOnly, TimeOnly, TimeSpan, DateTimeOffset |
ISO 8601 |
| NodaTime | Instant, LocalDateTime, LocalDate, LocalTime, ZonedDateTime, OffsetDateTime, Duration |
ISO 8601 |
| JSON | JObject, JArray, JToken, IDictionary<string, object?> |
E'{...}'::jsonb |
| Texto | string y fallback |
E'texto escapado' (E-string con \\ y '' escapados) |
DateTime preserva la semántica de su Kind:
Utc→'2026-04-13T18:32:50.0000000Z'Local→'2026-04-13T18:32:50.0000000+01:00'Unspecified→'2026-04-13T18:32:50.0000000'
Ejemplo básico
using MicroOrmGesg.Utils;
SqlLiteralFormatter.Format(null); // NULL
SqlLiteralFormatter.Format(true); // true
SqlLiteralFormatter.Format(42); // 42
SqlLiteralFormatter.Format(3.14m); // 3.14
SqlLiteralFormatter.Format("it's"); // E'it''s'
SqlLiteralFormatter.Format(Guid.NewGuid()); // '6f9619ff-8b86-d011-b42d-00cf4fc964ff'
SqlLiteralFormatter.Format(new byte[] { 0x48, 0x69 }); // '\x4869'
// Fechas — siempre ISO 8601 independiente de la cultura del proceso
SqlLiteralFormatter.Format(new DateTime(2026, 4, 13, 18, 32, 50, DateTimeKind.Utc));
// → '2026-04-13T18:32:50.0000000Z'
SqlLiteralFormatter.Format(Instant.FromUtc(2026, 4, 13, 18, 32, 50));
// → '2026-04-13T18:32:50Z'
// JSONB con cast explícito
SqlLiteralFormatter.Format(new JObject { ["a"] = 1, ["b"] = "hola" });
// → E'{"a":1,"b":"hola"}'::jsonb
Uso típico: generar un script INSERT
using MicroOrmGesg.Utils;
var filas = await connection.QueryAsync("SELECT id, nombre, fecha_creacion, metadatos FROM tabla");
var sb = new StringBuilder();
sb.AppendLine("BEGIN;");
foreach (var fila in filas)
{
var dict = (IDictionary<string, object?>)fila;
var valores = dict.Values.Select(SqlLiteralFormatter.Format);
sb.AppendLine(
$"INSERT INTO tabla (id, nombre, fecha_creacion, metadatos) " +
$"VALUES ({string.Join(", ", valores)}) ON CONFLICT (id) DO NOTHING;");
}
sb.AppendLine("COMMIT;");
File.WriteAllText("export.sql", sb.ToString());
Escape manual de texto
Si solo necesitas escapar un string como E-string de PostgreSQL sin pasar por el switch completo:
SqlLiteralFormatter.EscaparTexto("O'Brien"); // E'O''Brien'
SqlLiteralFormatter.EscaparTexto("ruta\\path"); // E'ruta\\path'
SqlLiteralFormatter.EscaparTexto("sin especiales"); // 'sin especiales' (fast-path sin prefijo E)
Los null bytes (\0) se eliminan porque PostgreSQL no admite U+0000 dentro de strings.
Limitaciones
- No parametriza: si construyes SQL dinámico con input de usuario, parametriza con Dapper. Este helper asume que los valores vienen de fuentes de confianza (columnas de tu propia base de datos, configuración, etc.).
- Fechas sin zona horaria:
DateTimeconKind=Unspecifiedse serializa sin offset. Si la columna destino estimestamptz, PostgreSQL interpretará la fecha en la zona horaria del servidor. Si necesitas garantizar UTC, usaDateTime.SpecifyKind(dt, DateTimeKind.Utc)antes de llamar al helper, o pasa unInstantde NodaTime. - JSONB: se usa
Newtonsoft.Jsonpara serializar. Si tu modelo está tipado conSystem.Text.Json, conviértelo antes aJObjectoIDictionary<string, object?>.
📖 Recursos adicionales
Mejores prácticas
1. Siempre propaga CancellationToken
// ✅ BIEN
public async Task<Usuario?> ObtenerAsync(int id, CancellationToken ct)
{
await _db.OpenAsync(ct);
return await _repo.GetByIdAsync(_db, id, ct);
}
// ❌ MAL
public async Task<Usuario?> ObtenerAsync(int id)
{
await _db.OpenAsync(); // Sin ct
return await _repo.GetByIdAsync(_db, id); // Sin ct
}
2. Usa transacciones para operaciones múltiples
// ✅ BIEN - Atómico (todo o nada)
await _db.BeginTransactionAsync(ct: ct);
try
{
await _repo.InsertAsync(_db, pedido, ct);
await _query.ExecuteAsync(_db, "UPDATE stock...", ct);
await _db.CommitAsync(ct);
}
catch
{
await _db.RollbackAsync(ct);
throw;
}
// ❌ MAL - No atómico, puede quedar inconsistente
await _repo.InsertAsync(_db, pedido, ct);
await _query.ExecuteAsync(_db, "UPDATE stock...", ct);
3. Registra TypeHandlers de JSONB una sola vez
// En Program.cs, ANTES de builder.Build()
SqlMapper.AddTypeHandler(new JObjectTypeHandler());
SqlMapper.AddTypeHandler(new JArrayTypeHandler());
SqlMapper.AddTypeHandler(new JTokenTypeHandler());
4. Usa logging apropiado por entorno
if (builder.Environment.IsDevelopment())
{
builder.Logging.AddFilter("MicroOrmGesg", LogLevel.Debug); // Verbose
}
else
{
builder.Logging.AddFilter("MicroOrmGesg", LogLevel.Warning); // Solo advertencias
}
5. Configura timeout adecuado
// Para migraciones pesadas
services.AddPgMigrations(options =>
{
options.CommandTimeoutSeconds = 300; // 5 minutos
});
6. Nunca modifiques migraciones ya aplicadas
-- ❌ MAL: Modificar paso existente
-- @step id:001 name:create.usuarios
CREATE TABLE usuarios(
id serial PRIMARY KEY,
email text,
telefono text -- ← Agregado después
);
-- ✅ BIEN: Crear nuevo paso
-- @step id:002 name:alter.usuarios.add_telefono
ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS telefono text;
Troubleshooting
"La sesión está cerrada. Llama a OpenAsync() primero"
Causa: Intentaste usar IDbSession sin llamar a OpenAsync().
Solución:
await _db.OpenAsync(ct); // ← Agregar esta línea
await _repo.GetByIdAsync(_db, id, ct);
"Ya existe una transacción activa"
Causa: Llamaste a BeginTransactionAsync() dos veces sin hacer commit/rollback.
Solución:
// Asegúrate de commit o rollback antes de iniciar otra
await _db.CommitAsync(ct);
// Ahora puedes iniciar otra transacción
await _db.BeginTransactionAsync(ct: ct);
"El modelo X no tiene definida una clave primaria"
Causa: Tu entidad no tiene [Key] o [ExplicitKey].
Solución:
public class Usuario
{
[Key] // ← Agregar esto
public int Id { get; set; }
// ...
}
"No hay columnas válidas para actualizar con el patch proporcionado"
Causa: En UpdateSetAsync, todos los campos eran PK, soft delete o no existen.
Solución:
// Asegúrate de usar nombres correctos (C#, snake_case o [Column])
await _repo.UpdateSetAsync(_db, id, new { Nombre = "Juan" }, ct); // C#
await _repo.UpdateSetAsync(_db, id, new { nombre = "Juan" }, ct); // snake_case
Tip (v1.2.5+): Habilita
System.Diagnostics.Tracepara ver qué campos ignoraUpdateSetAsyncy por qué (campo no reconocido, clave primaria, soft delete).
"Drift detectado en paso XXX"
Causa: El SQL de un paso ya aplicado cambió (diferente checksum).
Solución:
- Revertir el cambio en el SQL (si fue un error)
- Crear un nuevo paso con el cambio deseado
- Cambiar política a
DriftPolicy.Reapplysi es seguro (funciones/vistas)
"Migration file not found"
Causa: La ruta al archivo SQL es incorrecta o el archivo no se copia al output.
Solución:
<!-- En tu .csproj -->
<ItemGroup>
<None Update="scripts\schema.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Queries JSONB no funcionan
Causa: No registraste los TypeHandlers de JSONB.
Solución:
// En Program.cs
using Dapper;
using MicroOrmGesg.Utils;
SqlMapper.AddTypeHandler(new JObjectTypeHandler());
SqlMapper.AddTypeHandler(new JArrayTypeHandler());
SqlMapper.AddTypeHandler(new JTokenTypeHandler());
Compatibilidad DateTime / TimeSpan con Npgsql 6.0+ y NodaTime
A partir de Npgsql 6.0, los tipos que devuelve PostgreSQL cambiaron. Si tus modelos C# usan DateTime o TimeSpan, necesitas habilitar compatibilidad. Hay dos escenarios mutuamente excluyentes — elige solo uno:
| Tu configuración | Error típico | Método a llamar |
|---|---|---|
Npgsql 6.0+ sin UseNodaTime() |
Error parsing column X (DateOnly) |
NpgsqlDateTimeCompatibility.EnableDateTimeCompatibility() |
Npgsql 6.0+ con UseNodaTime() |
Error parsing column X (Instant) |
NodaTimeCompatibility.EnableDateTimeCompatibility() |
Escenario 1 — Sin NodaTime (solo el cambio DateOnly/TimeOnly de Npgsql 6.0+):
// En Program.cs, ANTES de cualquier operación de base de datos
using MicroOrmGesg.Utils;
NpgsqlDateTimeCompatibility.EnableDateTimeCompatibility();
Convierte automáticamente al leer:
DateOnly→DateTime(columnasdate)TimeOnly→TimeSpan(columnastime)
La escritura pasa DateTime/TimeSpan tal cual a Npgsql, que se encarga de la conversión al tipo de columna destino.
Parámetros
DateOnly/TimeOnlyenIDirectQuery(v1.2.12+): Si pasas unDateOnlyoTimeOnly(incluyendo nullables) como parámetro de Dapper en SQL crudo, los handlers fuerzanNpgsqlDbType.Date/NpgsqlDbType.Timeautomáticamente. Antes, la API genéricaIDataMicroOrm<T>ya lo hacía (porque construye losNpgsqlParameterella misma), pero las queries directas pasaban por Dapper sin tipo concreto y Npgsql fallaba la inferencia. Ahora ambas rutas se comportan igual. Aplica también aNodaTimeCompatibility.Lectura
DateOnly/TimeOnlyconUseNodaTime()(v1.2.13+): CuandoUseNodaTime()está activo y un modelo tiene propiedadesDateOnly/TimeOnly(o nullables) leídas desde columnasdate/time, Npgsql devuelveLocalDate/LocalTimeen vez del tipo nativo. ElParsede los handlers ahora cubreLocalDate → DateOnly,LocalDateTime → DateOnly(truncando la hora) yLocalTime → TimeOnly, evitando elInvalidCastExceptionque se producía en v1.2.12.
Escenario 2 — Con UseNodaTime() (Npgsql devuelve tipos NodaTime):
// En Program.cs, DESPUÉS de configurar UseNodaTime()
using MicroOrmGesg.Utils;
NodaTimeCompatibility.EnableDateTimeCompatibility();
Convierte automáticamente:
| Dirección | Conversión | Columna PostgreSQL |
|---|---|---|
| Lectura | Instant → DateTime |
timestamptz |
| Lectura | LocalDateTime → DateTime |
timestamp |
| Lectura | LocalDate → DateTime |
date |
| Lectura | LocalTime → TimeSpan |
time |
| Escritura | DateTime → Instant |
timestamptz |
| Escritura | TimeSpan → LocalTime |
time |
Limitación de escritura: El handler de
DateTimeconvierte siempre aInstant(asumetimestamptz). Para columnasdate, usaDateOnlyoLocalDateen tu modelo en lugar deDateTime.
NO llames a ambos métodos. Ambos registran TypeHandlers de Dapper para DateTime y TimeSpan — el segundo sobreescribe al primero.
Ambas llamadas son thread-safe e idempotentes. Comprueba el estado con .IsEnabled.
Alternativa (recomendada para proyectos nuevos): Usa directamente los tipos nativos en tus modelos: DateOnly, TimeOnly, Instant, LocalDate, etc.
Enums de PostgreSQL rechazan valores enteros
Causa: Por defecto, C# envía enums como integer, pero PostgreSQL espera el nombre de texto del tipo ENUM personalizado.
Solución: Marca la propiedad con [PgEnum]:
// PostgreSQL: CREATE TYPE estado_ticket_enum AS ENUM ('Abierto','Cerrado','Pendiente');
public enum EstadoTicket { Abierto, Cerrado, Pendiente }
[Table("tickets")]
public class Ticket
{
[Key]
public int Id { get; set; }
[PgEnum]
public EstadoTicket Estado { get; set; } // Se envía como "Abierto", no como 0
}
[PgEnum] envía el valor .ToString() del enum con NpgsqlDbType.Unknown, permitiendo que PostgreSQL aplique el cast implícito text → enum. Funciona en todos los métodos de escritura: InsertAsync, UpdateAsync, UpdateSetAsync e InsertBulkAsync.
Importante: Los valores del enum C# deben coincidir exactamente (case-sensitive) con los valores definidos en el tipo ENUM de PostgreSQL.
Dictionary<string,T> no se guarda como JSONB
Causa: En versiones anteriores solo se detectaban como JSONB las propiedades con [Jsonb], JToken/JObject/JArray.
Solución (v1.2.0): Dictionary<,> se detecta automáticamente como JSONB sin necesidad de atributos:
[Table("configuraciones")]
public class Config
{
[Key]
public int Id { get; set; }
// Se detecta automáticamente como JSONB
public Dictionary<string, string> Parametros { get; set; } = new();
public Dictionary<int, List<string>> Agrupaciones { get; set; } = new();
}
Si prefieres ser explícito, puedes seguir usando [Jsonb].
Historial de cambios
v1.2.13: Lectura
DateOnly/TimeOnlydesde tipos NodaTime- 🐛 Bugfix: Los handlers añadidos en v1.2.12 (
DateOnlyParameterHandler,NullableDateOnlyParameterHandler,TimeOnlyParameterHandler,NullableTimeOnlyParameterHandler) cubrían la escritura, pero suParseno contemplaba que conUseNodaTime()activo PostgreSQL devuelveLocalDate/LocalTimeen columnasdate/time. Si un modelo tenía propiedadesDateOnly/TimeOnlyleídas en ese escenario, lanzabaInvalidCastException. - ✅ Fix:
Parseahora reconoceLocalDate → DateOnly,LocalDateTime → DateOnly(truncando la hora) yLocalTime → TimeOnly, alineando el comportamiento conInstantToDateTimeHandlerdeNodaTimeCompatibility.
- 🐛 Bugfix: Los handlers añadidos en v1.2.12 (
v1.2.12: Handlers de parámetros
DateOnly/TimeOnlyparaIDirectQuery- 🐛 Bugfix: Pasar
DateOnly/TimeOnly(o sus nullable) como parámetro a Dapper en queries crudas (IDirectQuery) fallaba la inferencia de tipo de Npgsql. La API genéricaIDataMicroOrm<T>no estaba afectada porque construye losNpgsqlParameterdirectamente conNpgsqlDbType.Date/NpgsqlDbType.Time. - ✅ Fix: Nuevos
SqlMapper.TypeHandler<DateOnly>,TypeHandler<DateOnly?>,TypeHandler<TimeOnly>,TypeHandler<TimeOnly?>que enSetValuefuerzanNpgsqlDbType.Date/NpgsqlDbType.Time(con fallback aDbType.Date/DbType.Timesi el parámetro no esNpgsqlParameter). Los nullable mantienen el tipo aun cuando el valor esnull, para que PostgreSQL no falle por tipo desconocido enWHERE col = @param. - ✅ Mejora: Registro automático tanto en
NpgsqlDateTimeCompatibility.EnableDateTimeCompatibility()como enNodaTimeCompatibility.EnableDateTimeCompatibility(). Los consumidores no necesitan cambios — la siguiente vez que arranquen quedan cubiertos.
- 🐛 Bugfix: Pasar
v1.2.11:
UpsertBulkAsynccon claves de conflicto compuestas- ✅ Mejora:
UpsertBulkAsyncadmite múltiples columnas como clave de conflicto (ON CONFLICT (col1, col2) DO UPDATE)
- ✅ Mejora:
v1.2.8:
UpsertBulkAsync— upserts masivos con COPY + ON CONFLICT- ✅ Nueva funcionalidad:
UpsertBulkAsynccombina la performance de Binary Import con la semántica deINSERT ... ON CONFLICT DO UPDATE. Internamente carga los datos a una tabla temporal con COPY y luego ejecuta el upsert.
- ✅ Nueva funcionalidad:
v1.2.9: Nuevo
SqlLiteralFormatterpara generar literales SQL sin depender de cultura- ✅ Nueva funcionalidad:
MicroOrmGesg.Utils.SqlLiteralFormatter.Format(object?)convierte cualquier valor CLR a literal SQL seguro para PostgreSQL, siempre conCultureInfo.InvariantCulturey formato ISO 8601- Soporte completo BCL:
DateTime(con semántica deKind),DateOnly,TimeOnly,TimeSpan,DateTimeOffset,Guid,byte[](bytea hex), enteros, decimales, enums - Soporte completo NodaTime:
Instant,LocalDateTime,LocalDate,LocalTime,ZonedDateTime,OffsetDateTime,Duration - JSONB:
JObject,JArray,JToken,IDictionary<string, object?>con cast explícito...::jsonb - Strings con E-strings de PostgreSQL (
\\y''escapados; null bytes eliminados)
- Soporte completo BCL:
- ✅ Nueva utilidad pública:
SqlLiteralFormatter.EscaparTexto(string)para escape manual como E-string - 🐛 Motivación (bugfix aguas arriba): Cuando
UseNodaTime()está habilitado y se haceQueryAsyncsin tipo genérico (devuelveDapperRow), Dapper no aplicaTypeHandlers y llegan tipos NodaTime crudos. Formatearlos con.ToString()heredaba la cultura del proceso (p. ej.es-ESen contenedores) produciendo fechas"13/4/2026 18:32:50"que PostgreSQL rechaza con22008: date/time field value out of range. Este helper centraliza la serialización y elimina la clase entera de bug en cualquier consumidor - 📖 Ver sección SqlLiteralFormatter: literales SQL seguros
- ✅ Nueva funcionalidad:
v1.2.5: Consistencia en validaciones y trazas de diagnóstico
- 🐛 Bugfix:
GetByIdAsyncno verificabasession.Connectionantes de usarla (todos los demás métodos CRUD sí lo hacían). Ahora lanzaInvalidOperationExceptioncon mensaje descriptivo si la sesión está cerrada. - ✅ Mejora:
UpdateSetAsyncemite trazas (Trace.TraceInformation) cuando ignora campos del patch, indicando el motivo: campo no reconocido, clave primaria, o columna de soft delete. Facilita diagnosticar por qué un patch "no hace nada". - ✅ Mejora:
GetJsonPartAsyncregistraTrace.TraceWarningcuando falla la deserialización del fragmento JSON, incluyendo columna, ruta y mensaje de error. Antes devolvíanullsilenciosamente.
- 🐛 Bugfix:
v1.2.4: Separación clara de handlers DateTime/NodaTime y correcciones internas
- ✅ Mejora:
NodaTimeCompatibilityahora maneja tambiénTimeSpan↔LocalTime(columnastime) - ✅ Mejora:
NpgsqlDateTimeCompatibility.SetValueya no fuerzaDateOnly— pasaDateTimetal cual para no romper columnastimestamptz - 🐛 Bugfix:
EntityMap— la columna de soft delete no se excluía deWritableProps(error de orden en constructor) - 🐛 Bugfix: Regex en
IsJsonbExpressionyBuildJsonPathExpressionse recompilaba en cada llamada (ahora esstatic readonly) - 🐛 Bugfix:
[PgEnum]usabaNpgsqlDbType.TextenInsertBulkAsyncvsNpgsqlDbType.Unknownen INSERT/UPDATE (unificado aUnknown) - ✅ Mejora:
ResetParaReintentoAsyncahora loguea excepciones conLogWarningen vez de tragarlas silenciosamente
- ✅ Mejora:
v1.2.2: Soporte para reintentos con Polly en escenarios de resiliencia
- ✅ Nueva funcionalidad:
IDbSession.ResetParaReintentoAsync()para limpiar el estado interno de la sesión tras un fallo transitorio- Libera la transacción y la conexión actuales (si existen) sin marcar el objeto como disposed
- Permite volver a llamar a
OpenAsync()y continuar operando en el siguiente intento - Diseñado para integrarse con políticas de reintentos (Polly, Microsoft.Extensions.Resilience)
- Caso de uso: Cuando una conexión TCP muere a mitad de una transacción,
RollbackAsyncpuede fallar y dejar_tx != null. Sin este método,BeginTransactionAsynclanzaríaInvalidOperationException("Ya existe una transacción activa")en el reintento
- ✅ Nueva funcionalidad:
v1.2.1: Corrección de doble serialización JSONB y compatibilidad con columnas
date- 🐛 Bugfix crítico: Propiedades
stringcon[Jsonb]se serializaban dos veces en INSERT/UPDATEBuildWriteParametersllamabaJsonConvert.SerializeObject(value)incluso cuando el valor ya era un string JSON- Esto producía un literal JSON string (
"{ ... }") en vez de un objeto JSON ({ ... }) - Afectaba a
InsertAsync,InsertAsyncReturnId,UpdateAsync,UpdateSetAsynceInsertBulkAsync - Síntoma: triggers de validación JSONB que usan el operador
?no encontraban las claves esperadas porque el valor era un string JSONB, no un objeto - Fix: Si el valor ya es
string, se usa directamente sin re-serializar
- 🐛 Bugfix:
NodaTimeCompatibilityno manejaba columnasdatenitimestamp(sin zona horaria)- Con
UseNodaTime(), PostgreSQLdatedevuelveLocalDateytimestampdevuelveLocalDateTime, pero los handlers solo manejabanInstant(paratimestamptz) - Modelos con
DateTime/DateTime?para columnasdatelanzabanInvalidCastExceptional leer - Fix: Añadidos
LocalDate,LocalDateTimeyDateOnlyaInstantToDateTimeHandleryNullableInstantToDateTimeHandler
- Con
- 🐛 Bugfix crítico: Propiedades
v1.2.0: Compatibilidad NodaTime, enums PostgreSQL, Dictionary JSONB y snake_case automático
- ✅ Nueva funcionalidad:
NodaTimeCompatibility.EnableDateTimeCompatibility()para modelos conDateTimecuandoUseNodaTime()está habilitadoInstantToDateTimeHandler: convierteInstant/LocalDate/LocalDateTime/DateOnly→DateTime(lectura) yDateTime→Instant(escritura)NullableInstantToDateTimeHandler: mismo comportamiento paraDateTime?- Cubre columnas
timestamptz,timestamp,dateyDateOnly(Npgsql 6.0+) - Thread-safe e idempotente (patrón double-check locking)
- ✅ Nuevo atributo:
[PgEnum]para propiedades enum que mapean a tipos ENUM personalizados de PostgreSQL- Envía el valor como texto (
NpgsqlDbType.Unknown) en INSERT/UPDATE/BulkInsert - Compatible con cast implícito
text → enumde PostgreSQL - Soportado en
BuildWriteParameters,UpdateSetAsyncyGetNpgsqlTypeAndValue
- Envía el valor como texto (
- ✅ Mejora: Auto-detección de
Dictionary<,>como JSONB- Propiedades
Dictionary<string, T>,Dictionary<int, T>, etc. se serializan automáticamente como JSONB - No requiere
[Jsonb]explícito (aunque sigue siendo compatible) - Soportado en INSERT, UPDATE, UpdateSet y BulkInsert
- Propiedades
- ✅ Mejora:
DirectQueryhabilitaMatchNamesWithUnderscoresautomáticamente- Mapea columnas
snake_case→ propiedadesPascalCasesin configuración adicional - Se activa via constructor estático (una sola vez al cargar la clase)
- Mapea columnas
- ✅ Nueva funcionalidad:
v1.1.2: Mejoras en manejo de conexiones y depuración
- Optimización en construcción de cadenas de conexión
- Trazas detalladas en
JsonExtensionspara facilitar depuración
v1.1.1: Utilidades JSONB y TypeHandlers
- ✅ Nueva funcionalidad:
JsonValueConverterpara manejo de valores JSONB - Mejoras en los TypeHandlers de Dapper para mayor compatibilidad con PostgreSQL
- ✅ Nueva funcionalidad:
v1.1.0: Actualización a .NET 10, Npgsql 10.0.1, JSONB y seguridad
- ⚠️ Breaking changes de Npgsql 10.0:
- PostgreSQL
dateahora mapea aDateOnly(antesDateTime) - PostgreSQL
timeahora mapea aTimeOnly(antesTimeSpan) - PostgreSQL
intervalcon meses/años no puede leerse comoTimeSpan— usar NodaTimePeriod cidrahora mapea aIPNetwork(antesNpgsqlCidr)
- PostgreSQL
- ✅ Nueva funcionalidad:
NpgsqlDateTimeCompatibility.EnableDateTimeCompatibility()para modelos existentes conDateTime/TimeSpan- Valida rango TimeSpan 0-24h para columnas
timecon error descriptivo
- Valida rango TimeSpan 0-24h para columnas
- ✅ Nueva funcionalidad:
InsertBulkAsync()— inserción masiva con Binary Import (COPY protocol)- Soporta tipos NodaTime,
List<T>,IList<T>, arrays adicionales - Validación de entidades sin propiedades escribibles
- Soporta tipos NodaTime,
- ✅ Nueva funcionalidad:
JsonExtensions— utilidades para trabajar con JSONBGetPath<T>(),GetTranslation(),GetTranslationWithFallback(),GetAllTranslations()SetPath(),HasPath(),Flatten(),Merge()
- ✅ Nueva funcionalidad: Filtros JSONB en
GetAllAsync/PageAsync/CountAsync- Soporte para expresiones con operadores
->y->> - Validación de seguridad para prevenir SQL injection en expresiones JSONB
- Soporte para expresiones con operadores
- ✅ Nueva funcionalidad:
GetJsonPartAsync<T>()— proyección parcial de campos JSONB - 🔒 Seguridad: Validación robusta en
IsJsonbExpression()con regex estricto - 🐛 Bugfix:
FlattenRecursivemaneja correctamenteJNull,JRaw,JUndefined
- ⚠️ Breaking changes de Npgsql 10.0:
v1.0.2: IDirectQuery para queries directas con Dapper compartiendo IDbSession
v1.0.1: Sistema de migraciones completo con checksums SHA-256, advisory locks, detección de drift
v1.0.0: Versión inicial con repositorio genérico, DbSession, funciones PostgreSQL, JSONB
Licencia
MIT - ver LICENSE para más detalles.
Soporte
Para reportar issues o solicitar features: github.com/gesgocom/MicroOrmGesg
Showing the top 20 packages that depend on Gesgocom.MicroOrmGesg.
| Packages | Downloads |
|---|---|
|
Gesgocom.NeuraNetGes
NeuraNetGes es una librería de apoyo para el uso de librerías de LLM.
|
10 |
|
Gesgocom.NeuraNetGes
NeuraNetGes es una librería de apoyo para el uso de librerías de LLM.
|
13 |
.NET 10.0
- Dapper (>= 2.1.72)
- Newtonsoft.Json (>= 13.0.4)
- Npgsql (>= 10.0.1)
- Npgsql.NodaTime (>= 10.0.1)
| Version | Downloads | Last updated |
|---|---|---|
| 1.2.15 | 78 | 05/11/2026 |