Dinaup.Database
Accede a PostgreSQL desde .NET con PGClient, con lecturas tipadas, upsert y lectura por lotes sin boilerplate.
Dinaup.Database es el módulo de acceso a PostgreSQL incluido dentro del paquete Dinaup. Ofrece una interfaz directa para trabajar con la base de datos sin lidiar con DataReader ni boilerplate de ADO.NET. Cubre:
- Conexión a PostgreSQL con SSL y cadenas en formato estándar.
- Lecturas tipadas (listas, diccionarios, modelos).
- Inserción, actualización y upsert (insertar o actualizar según exista el registro).
- Lectura por lotes de grandes volúmenes de datos.
- Mapeo automático a clases de negocio mediante interfaces simples.
- Acceso multihilo seguro y reutilización de conexión.
Instalación
Dinaup.Database forma parte del paquete principal de Dinaup: al instalar Dinaup ya tienes PGClient y todas las utilidades de base de datos.
dotnet add package Dinaupusing Dinaup.Database;En versiones anteriores Dinaup.Database era un NuGet aparte (dotnet add package Dinaup.Database). Si actualizas un proyecto antiguo, basta con eliminar esa referencia: el namespace y la API son los mismos.
Clase principal: PGClient
La clase PGClient proporciona métodos para:
- Conexión y reconexión a PostgreSQL (con SSL si se requiere).
- Ejecución de sentencias SQL.
- Lectura de resultados (listas, diccionarios, modelos).
- Inserción, actualización, y operaciones de upsert.
- Lectura de datos en lotes.
Propiedades principales
IsConnected(bool): indica si la conexión está abierta.Host,Port,DatabaseName,CurrentSchema: metadatos de la conexión, fijados enConnect.Description(string): nombre de aplicación reportado a PostgreSQL (alias deOptions.ApplicationName).Options(PGClientOptions): opciones con las que se creó el cliente. Inmutables tras el constructor.
PGClient es seguro entre hilos una vez conectado: varios hilos pueden llamar a cualquier método Read/Execute a la vez. Las conexiones se multiplexan por el pool de Npgsql.
Establecer conexión
Conexión con parámetros separados:
var client = new PGClient();
client.Connect("localhost", 5432, "myuser", "mypassword", "mydatabase");
if (client.IsConnected)
{
Console.WriteLine("Conexión establecida correctamente.");
}Con cadena de conexión (formato key=value o URI postgresql://):
client.Connect("postgresql://user:password@host:5432/dbname?sslmode=require");De forma asíncrona:
await client.ConnectAsync("localhost", 5432, "user", "password", "mydb");
// o, creando y conectando en un paso:
var ready = await PGClient.CreateAsync("localhost", 5432, "user", "password", "mydb");SSL está activo por defecto (PGClientOptions.UseSsl = true), así que Connect(host, port, …) ya negocia TLS. Con cadena de conexión, controla el modo con sslmode (require, verify-full, disable).
Opciones de conexión: PGClientOptions
Pasa un PGClientOptions al constructor para ajustar pool, timeouts y reintentos. Todas las propiedades tienen valor por defecto:
| Propiedad | Defecto | Qué controla |
|---|---|---|
MaxPoolSize | 10 | Conexiones máximas del pool. |
MinPoolSize | 1 | Conexiones mínimas del pool. |
CommandTimeoutSeconds | 30 | Tiempo máximo por comando. |
ConnectTimeoutSeconds | 15 | Tiempo máximo para abrir la conexión. |
MaxRetries | 3 | Reintentos ante fallo transitorio. |
RetryBaseDelayMs | 500 | Espera base entre reintentos. |
RetryMaxDelayMs | 8000 | Espera máxima entre reintentos. |
ApplicationName | "" | Nombre visible en pg_stat_activity. |
Schema | "" | Esquema por defecto (search_path). |
UseSsl | true | Negocia TLS en la conexión por parámetros. |
var options = new PGClientOptions
{
ApplicationName = "importador-nocturno",
ConnectTimeoutSeconds = 30,
MaxPoolSize = 20
};
var client = new PGClient(options);
client.Connect("localhost", 5432, "user", "password", "mydb");Lectura de datos
ReadValue(string SQL): retorna un único valor (cadena).ReadList(string SQL): retorna una lista de cadenas (asumiendo una sola columna).ReadKVDictionary(string SQL): retorna unDictionary<string, string>a partir de dos columnas (clave, valor).ReadDictionaryList(string SQL): retorna una secuencia de diccionarios (una fila por diccionario).ReadObjectList<T>(string SQL)(donde T: BaseModelConverter): retorna una secuencia de objetos T mapeados desde la BD.
Cada método de lectura tiene su variante asíncrona con sufijo Async (ReadValueAsync, ReadListAsync, ReadDictionaryListAsync, ReadObjectListAsync, …).
Ejemplos:
// Leer un valor único
var countStr = client.ReadValue("SELECT COUNT(*) FROM test_table;");
int totalRegistros = int.Parse(countStr);
// Leer una lista (una columna)
var nombres = client.ReadList("SELECT name FROM test_table ORDER BY id;");
foreach (var nombre in nombres)
{
Console.WriteLine(nombre);
}
// Leer una lista de diccionarios
var registros = client.ReadDictionaryList("SELECT id, name, value FROM test_table WHERE id < 10;");
foreach (var reg in registros)
{
Console.WriteLine($"ID: {reg["id"]}, Name: {reg["name"]}, Value: {reg["value"]}");
}Lectura de modelos
Si se dispone de clases que heredan de BaseModelConverter, puede mapearse directamente:
public class TestModel : BaseModelConverter
{
public int Id { get; set; }
public string Name { get; set; }
public int Value { get; set; }
public override void FromDic(Dictionary<string, string> dic)
{
this.Id = dic.GetM("id").INT(0);
this.Name = dic.GetM("name");
this.Value = dic.GetM("value").INT(0);
}
public override string Table => "test_table";
public override string[] Fields => new[] { "id", "name", "value" };
public override string LastModifiedFieldDatetimeUTC => ""; // No usado en este ejemplo
}
// Ejemplo de uso
var modelos = client.ReadObjectList<TestModel>("SELECT * FROM test_table ORDER BY id;");
foreach (var m in modelos)
{
Console.WriteLine($"{m.Id} - {m.Name} - {m.Value}");
}Inserción, actualización y upsert
InsertRecord(tableName, record): Inserta un diccionario como registro.InsertRecords(tableName, records): Inserta múltiples registros a la vez.UpdateRecord(tableName, dataDict, idField, idValue): Actualiza el registro cuyo campoidFieldcoincide conidValue.InsertOrIgnoreRecord(tableName, dataDict): Inserta el registro si no hay conflicto.InsertOrUpdateRecord(tableName, dataDict, idField): Inserta o actualiza según exista el registro.InsertOrUpdateRecords(tableName, dataDicts, idField): Inserta o actualiza múltiples registros.
Ejemplo: Insertar un registro
var nuevoRegistro = new Dictionary<string, string>
{
{"name", "NuevoNombre"},
{"value", "123"}
};
int rowsAffected = client.InsertRecord("test_table", nuevoRegistro);
Console.WriteLine("Filas insertadas: " + rowsAffected);Ejemplo: Actualizar un registro
var datosActualizar = new Dictionary<string, string>
{
{"value", "999"}
};
int filasActualizadas = client.UpdateRecord("test_table", datosActualizar, "id", "1");
Console.WriteLine("Filas actualizadas: " + filasActualizadas);Ejemplo: Upsert (insertar o actualizar)
var registroUpsert = new Dictionary<string, string>
{
{"id", "100"},
{"name", "Registro100"},
{"value", "1000"}
};
int affected = client.InsertOrUpdateRecord("test_table", registroUpsert, "id");
Console.WriteLine("Filas modificadas: " + affected);Lectura en lotes (batch reading)
Para grandes cantidades de datos:
BatchReadDictionaries(description, countSQL, dataSQL, batchSize): Iterador que produce lotes de diccionarios.BatchReadObjects<T>(description, countSQL, dataSQL, batchSize): Igual que el anterior pero para objetos de tipo T.
Ejemplo:
var batches = client.BatchReadDictionaries(
"Lectura en lotes",
"SELECT COUNT(*) FROM test_table",
"SELECT id, name, value FROM test_table ORDER BY id",
1000
);
foreach (var batch in batches)
{
Console.WriteLine("Lote de " + batch.Count + " registros");
foreach (var reg in batch)
{
Console.WriteLine($"{reg["id"]} - {reg["name"]} - {reg["value"]}");
}
}Clonar conexiones
DuplicateConnection() crea una nueva instancia PGClient con la misma configuración y abre la conexión.
var repoClonado = client.DuplicateConnection();
if (repoClonado.IsConnected)
{
Console.WriteLine("Conexión clonada y funcionando.");
}Manejo de errores
Se pueden capturar excepciones con try...catch.
try
{
client.ExecuteNonQuery("INSERT INTO test_table (name, value) VALUES ('Test', 1)");
}
catch (Exception ex)
{
Console.WriteLine("Error en la inserción: " + ex.Message);
}Liberación de recursos
PGClient implementa IDisposable, por lo que es recomendable usar using:
using (var client = new PGClient())
{
client.Connect("localhost", 5432, "user", "pass", "db");
// ... Operaciones ...
}
// Aquí se libera la conexión automáticamenteEjemplos completos
Ejemplo 1: leer todos los registros (c#)
var client = new PGClient();
client.Connect("localhost", 5432, "user", "password", "mydb");
var listaNombres = client.ReadList("SELECT name FROM test_table ORDER BY id;");
foreach (var nombre in listaNombres)
{
Console.WriteLine(nombre);
}Ejemplo 2: insertar y luego leer modelos (c#)
var client = new PGClient();
client.ConnectWithSSL("postgres://user:pass@host:port/mydb?sslmode=require");
// Insertar un registro
var nuevo = new Dictionary<string, string>
{
{"name", "NuevoRegistro"},
{"value", "100"}
};
client.InsertRecord("test_table", nuevo);
// Leer como objetos
var objetos = client.ReadObjectList<TestModel>("SELECT * FROM test_table WHERE name='NuevoRegistro'");
foreach (var obj in objetos)
{
Console.WriteLine($"{obj.Id}: {obj.Name} - {obj.Value}");
}Ejemplo 3: actualizar o ignorar si existe (c#)
var client = new PGClient();
client.Connect("localhost", 5432, "user", "password", "mydb");
var registro = new Dictionary<string, string>
{
{"id", "1"},
{"name", "Test1"},
{"value", "999"}
};
// InsertOrUpdate
int afectadas = client.InsertOrUpdateRecord("test_table", registro, "id");
Console.WriteLine("Filas afectadas: " + afectadas);Listados de alto rendimiento
Vuelca un informe entero sin OFFSET con LoadAllRowsAsync: paginación por keyset, coste constante por página y sin filas repetidas.
Dinaup.Logs
Módulo de observabilidad de Dinaup para .NET, basado en Serilog: logs estructurados, contexto, correlación distribuida, medición de tiempos y envío a Loki.