Esta documentación está en fase de desarrollo y puede contener errores.

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 Dinaup
using 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 en Connect.
  • Description (string): nombre de aplicación reportado a PostgreSQL (alias de Options.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:

PropiedadDefectoQué controla
MaxPoolSize10Conexiones máximas del pool.
MinPoolSize1Conexiones mínimas del pool.
CommandTimeoutSeconds30Tiempo máximo por comando.
ConnectTimeoutSeconds15Tiempo máximo para abrir la conexión.
MaxRetries3Reintentos ante fallo transitorio.
RetryBaseDelayMs500Espera base entre reintentos.
RetryMaxDelayMs8000Espera máxima entre reintentos.
ApplicationName""Nombre visible en pg_stat_activity.
Schema""Esquema por defecto (search_path).
UseSsltrueNegocia 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 un Dictionary<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 campo idField coincide con idValue.
  • 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áticamente

Ejemplos 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);

On this page