Cliente Dinaup
El Cliente Dinaup conecta tus aplicaciones .NET con la plataforma Dinaup: conexión, sesiones, informes, archivos, anotaciones y WriteOperations.
El cliente Dinaup en .NET te conecta al servicio para consultar informes, gestionar archivos, añadir anotaciones, generar documentos dinámicos, leer/escribir datos de secciones y trabajar con paginación y filtros.
Conectar y operar
Instala
dotnet add package Dinaup
dotnet add package Demoup.MyDinaupConecta
var client = await DinaupClientC.ConnectAsync(
endPoint: "https://api.dinaup.com/v2/tu-codigo",
publicKey: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
secretKey: "tu-secret-key-aqui"
);
if (client == null || client.IsConnected == false)
{
throw new Exception("No se pudo conectar a Dinaup");
}
using Dinaup;
// Las unicas credenciales en variables de entorno
var vault = new VaultData(
Environment.GetEnvironmentVariable("VAULT_URL"),
Environment.GetEnvironmentVariable("VAULT_PASSWORD")
);
vault.Initialize();
// Todo lo demas viene del Vault
var client = await DinaupClientC.ConnectAsync(
endPoint: vault.Read("dinaup.endpoint"),
publicKey: vault.Read("dinaup.publickey"),
secretKey: vault.Read("dinaup.secretkey")
);
if (client == null || client.IsConnected == false)
{
throw new Exception("No se pudo conectar a Dinaup");
}
using Dinaup;
var builder = WebApplication.CreateBuilder(args);
// Cargar Vault
var vault = new VaultData(
Environment.GetEnvironmentVariable("VAULT_URL"),
Environment.GetEnvironmentVariable("VAULT_PASSWORD")
);
vault.Initialize();
// Conectar a Dinaup
var client = await DinaupClientC.ConnectAsync(
endPoint: vault.Read("dinaup.endpoint"),
publicKey: vault.Read("dinaup.publickey"),
secretKey: vault.Read("dinaup.secretkey")
);
if (client == null || client.IsConnected == false)
throw new Exception("No se pudo conectar a Dinaup");
// Registrar como Singleton
builder.Services.AddSingleton(client);
var app = builder.Build();
Opera
// Use pre-built reports from DemoUp.MyDinaup
using DemoUp.MyDinaup.Reports.FuncionalidadD;
// Create and execute the report
var ventasReport = new APIVentasC();
await ventasReport.ExecuteQueryAsync(
dinaupClient: client,
page: 1,
resultsPerPage: 50
);
// Access results
Console.WriteLine($"Total: {ventasReport.TotalResults} records");
foreach (var row in ventasReport.Rows)
{
Console.WriteLine($"- {row.NumerodefacturaCompleto}: {row.Total}");
}
Guid sectionId = /* sección destino */;
var data = new Dictionary<string, string>
{
{
SeccionDePruebasAPIES.TextoPrincipal,
$"Prueba {Guid.NewGuid()}"
}
};
var wOp = new WriteOperation("", data); // "" => alta
// El sectionId es un Guid: usa _SectionIDGUID (no _SectionID, que es string)
await dinaupClient.RunWriteOperationAsync(SeccionDePruebasAPIES._SectionIDGUID, wOp, false);
// Lanza excepción si la operación falló o no se ejecutó
wOp.EnsureSuccess();
// Tras EnsureSuccess, WriteOperationResult está garantizado (no nulo)
Guid newId = wOp.WriteOperationResult.RowID;Guid rowId = /* Id del registro a editar */;
var data = new Dictionary<string, string>
{
{
SeccionDePruebasAPIES.TextoPrincipal,
$"Prueba {Guid.NewGuid()}"
}
};
var wOp= new WriteOperation(rowId, data);
await dinaupClient.RunWriteOperationAsync(sectionId, wOp, false);
// Lanza excepción si la operación falló o no se ejecutó
wOp.EnsureSuccess();
Guid updatedId = wOp.WriteOperationResult.RowID;
Referencias
Dinaup .NET
Antes de empezar, instala el paquete Dinaup, que incluye el Cliente base: conexión, autenticación, sesiones, archivos, anotaciones, WriteOperations, etc.
https://www.nuget.org/packages/Dinaup
MyDinaup
Cada empresa tiene su propio modelo de datos. Para trabajarlo tipado, añade tu biblioteca MyDinaup: las secciones, informes y documentos de tu tenant convertidos en clases con sus nombres reales. Qué contiene, qué paquete elegir y cómo se regenera, en MyDinaup.
Informes
Los informes son consultas predefinidas y tipadas para acceder a datos de manera eficiente. Están en el espacio de nombres:
{Empresa}.MyDinaup.Reports.{CategoriaD}- Convención de nombres de clases: API + NombreDeSeccion + C
- Los nombres de secciones/clases están en español porque se generan a partir de tu licencia (por ejemplo, “Ventas”, “Impuestos”, etc.).
Ejemplos de clases:
- DemoUp.MyDinaup.Reports.FuncionalidadD.APISeccionDePruebasAPIC
- DemoUp.MyDinaup.Reports.FuncionalidadD.APIEntidadesC
- DemoUp.MyDinaup.Reports.VentasD.APIVentasC
- DemoUp.MyDinaup.Reports.ImpuestosD.APIImpuestosC
Usa ExecuteQueryAsync para obtener resultados paginados. Accede a filas fuertemente tipadas desde report.Rows.
using DemoUp.MyDinaup.Reports.FuncionalidadD;
var report = new APISeccionDePruebasAPIC(); //
await report.ExecuteQueryAsync(dinaupClient, page: 1, resultsPerPage: 10);
Console.WriteLine($"Total resultados: {report.TotalResults}");
if (report.Rows.IsNotEmpty())
{
foreach (var fila in report.Rows)
{
// Accede a propiedades tipadas de cada fila
// Ej.: fila.TextoPrincipal, fila.ValorEntero, etc.
}
}Ajusta resultsPerPage a tus necesidades. Si esperas muchos resultados, usa paginación.
-
Filtrar resultados
Filtro simple por campo de la misma secciónvar report = new APISeccionDePruebasAPIC(); // Igualdad estricta report.AddFilter( SeccionDePruebasAPIES.ValorEntero, "=", 3); await report.ExecuteQueryAsync(dinaupClient, page: 1, resultsPerPage: 10); if (report.Rows.IsNotEmpty()) { foreach (var fila in report.Rows) { // Usar fila... } } -
Filtrar utilizando datos relacionados (composición de rutas):
Construye una clave de relación combinando el ID de sección y el campo de interés en la entidad relacionada.
// Clave de relación (ruta compuesta) var relationKey = SeccionDePruebasAPIES._SectionID.STR() + "." + SeccionDePruebasAPIES.ReferenciaAutorDelAlta + "." + EntidadesBaseES._SectionID.STR() + "." + EntidadesBaseES.VIP; var report = new APISeccionDePruebasAPIC(); report.AddFilter(relationKey, "=", 3); await report.ExecuteQueryAsync(dinaupClient, page: 1, resultsPerPage: 10); if (report.Rows.IsNotEmpty()) { foreach (var fila in report.Rows) { // Usar fila... } } -
Filtrado Between
// Between (ej.: 1 a 2999) var reportBetween = new APISeccionDePruebasAPIC(); reportBetween.AddFilterBetween( SeccionDePruebasAPIES.ValorEntero, 1, 2999 ); await reportBetween.ExecuteQueryAsync(dinaupClient, page: 1, resultsPerPage: 10); -
Solapamiento de rangos de fechas (overlap)
var eventosReport = new APIEventosDeCRMC(); var campoInicio = EventosDeCRMES.InicioEvento_UTC; var campoFin = EventosDeCRMES.FinEvento_UTC; var rangoInicio = DateOnly.FromDateTime(new DateTime(2000, 1, 1)); var rangoFin = DateOnly.FromDateTime(DateTime.Today.AddYears(50)); eventosReport.AddFilterDateRangeOverlapFilter(campoInicio, campoFin, rangoInicio, rangoFin); await eventosReport.ExecuteQueryAsync(dinaupClient, page: 1, resultsPerPage: 25); -
Búsqueda de texto libre
El parámetro
querySearchdeExecuteQueryAsyncbusca en los campos de texto del informe, como el buscador de la interfaz web.var report = new APISeccionDePruebasAPIC(); await report.ExecuteQueryAsync(dinaupClient, 1, 25, "iPhone Pro Max"); -
Filtro IN (varios valores a la vez)
// Con textos report.AddFilter(SeccionDePruebasAPIES.TextoPrincipal, new[] { "Valor A", "Valor B" }); // Con Guids (relaciones) report.AddFilterIn(SeccionDePruebasAPIES.ReferenciaAutorDelAlta, new[] { id1, id2 }); -
OR implícito
Dos filtros sobre el mismo campo se combinan con OR, no con AND.
report.AddFilter(PaisesD.PaisesES.CodigoDePaisAlfabeticoDe2Caracteres, "=", "ES"); report.AddFilter(PaisesD.PaisesES.CodigoDePaisAlfabeticoDe2Caracteres, "=", "IT"); // Devuelve España e Italia -
Variables de informe
Si el informe define variables (por ejemplo, un rango de fechas parametrizado), pásalas con
AddVariable. Admitestring,Guid,int,decimal,bool,DateyDateOnly.report.AddVariable("ClienteId", clienteId); report.AddVariable("FechaDesde", new DateOnly(2026, 1, 1));
Cuando el informe tiene muchos resultados, recorre página a página con ExecuteQuery_NextPage_Async.
var report = new APISeccionDePruebasAPIC();
await report.ExecuteQueryAsync(_dinaupClient, 1, 300);
if (report.Rows.IsNotEmpty())
{
do
{
foreach (var row in report.Rows)
{
}
} while (await report.ExecuteQuery_NextPageAsync());
}
Para exportar o procesar todas las filas de un informe, usa LoadAllRowsAsync. Pagina por keyset (cursor sobre el id), sin OFFSET, con coste constante por página y sin filas repetidas ni saltadas. Escala a millones.
// Devuelve TODAS las filas del informe, ordenadas por id.
var report = new APISeccionDePruebasAPIC();
var todas = await report.LoadAllRowsAsync(dinaupClient, pageSize: 10000, adminMode: true);
Console.WriteLine($"{todas.Count} filas volcadas");Es para exportar y procesar por lotes, no para mostrar en pantalla. El orden es por id (un GUID), así que no tiene orden de presentación. Si necesitas otro orden, ordénalo en memoria después. No llames a AddOrder antes: el keyset impone su propio orden por id y lanza excepción si ya hay uno.
Ver la guía Listados de alto rendimiento para el detalle, los límites y las excepciones.
Cada informe expone tiempos del servidor para diagnóstico: DatabaseTimeMS (lo que tardó Postgres), PrepareTimeMS, PostProcessTimeMS y ServerTimeMS (la suma). TraceId correlaciona la consulta con los logs del servidor y SQL devuelve la sentencia ejecutada. Todos valen 0/vacío hasta la primera consulta.
Columnas ausentes en la respuesta
Si el modelo tipado declara una columna que el servidor ya no devuelve, el informe lanza una excepción por defecto: así detectas de inmediato un desajuste entre tu MyDinaup y la estructura real. Para tolerarlo (registrar un aviso y dejar esas propiedades en su valor por defecto), activa TolerateMissingColumns:
// Por informe:
var report = new APISeccionDePruebasAPIC();
report.TolerateMissingColumns = true;
// O como valor por defecto de todo el proceso, una vez al arrancar:
DinaupReportSettings.TolerateMissingColumns = true;Cada informe hereda el valor de DinaupReportSettings.TolerateMissingColumns (por defecto false) al crearse, y puede sobrescribirlo por instancia.
Archivos
Dinaup permite subir archivos desde memoria o URL y obtener URLs firmadas para su consumo.
-
Subir desde un array de bytes
var bytes = System.Text.Encoding.UTF8.GetBytes("hola " + Guid.NewGuid()); var upload = await dinaupClient.File_UploadBytesAsync(bytes, "prueba.txt"); Guid fileId = upload.FileId; -
Subir desde una URL
var upload = await dinaupClient.File_UploadURLAsync( "https://cdn.dinaup.com/dinaup/web/portal/img/dinabot_marca.png", "dinabot.png" ); Guid fileId = upload.FileId; -
Obtener una URL firmada (lectura)
var signed = await dinaupClient.File_SignURLGetAsync(fileId); var url = signed.url_original; // URL lista para usar
Calcula el SHA1 en cliente y consulta antes de subir. Ahorra almacenamiento y ancho de banda.
private async Task<Dinaup.DinaupFileDTO> GetExistingFileAsync(string sha1)
{
var report = new MyDinaup.Reports.FuncionalidadD.APIIndiceDeArchivosEnSistemaC();
report.AddFilter("crcbase", "=", sha1);
await report.ExecuteQueryAsync(_dinaupClient, 1, 1, "", true, true);
return report.Files.Values.FirstOrDefault();
}
private async Task<Dinaup.DinaupFileDTO> GetExistingFileAsync(byte[] data)
{
string sha1 = Dinaup.extensions.ToSHA1(data);
return await GetExistingFileAsync(sha1);
}
Anotaciones
Todas las filas (registros) de Dinaup admiten 3 tipos de anotaciones:
AnnotationTypeE.Comments: Mensajería tipo chat para el equipo, dentro del contexto del registro.AnnotationTypeE.PublicGallery: Archivos públicos en el CDN (p. ej., fotos de productos).AnnotationTypeE.Files: Documentos privados asociados (contratos, presupuestos, soportes, etc.).
Los archivos en PublicGallery son públicos: cualquiera con el enlace puede verlos.
A continuación, ejemplos usando AnnotationTypeE.Comments.
-
Agregar un comentario (solo texto)
Guid sectionId = /* tu sección */; Guid rowId = /* registro al que agregas el comentario */; var anotacion = new AnotationParameters(sectionId, rowId, AnnotationTypeE.Comments) .WithText("¡Hola mundo! Este es mi primer comentario."); await dinaupClient.Annotation_PutAsync(anotacion); -
Agregar un comentario con archivo adjunto
var bytes = System.Text.Encoding.UTF8.GetBytes("contenido adjunto"); var upload = await dinaupClient.File_UploadBytesAsync(bytes, "nota.txt"); var anotacion = new AnotationParameters(sectionId, rowId, AnnotationTypeE.Comments) .WithText("Comentario con adjunto") .WithFile(upload.FileId); // adjuntamos el archivo await dinaupClient.Annotation_PutAsync(anotacion); -
Leer anotaciones
var resultado = await dinaupClient.Annotations_GetAsync( sectionId, rowId, AnnotationTypeE.Comments ); foreach (var anotacion in resultado.Annotations) { var texto = anotacion.Text; var adjuntos = anotacion.AttachedFiles; // si hay archivos adjuntos }
Documentos dinámicos
Los Documentos Dinámicos permiten:
- Agrupar varias consultas en una sola operación para reducir latencia.
- Generar documentos de impresión (facturas, albaranes) delegando la composición en Dinaup.
Recomendación:
- Para consultar datos en general, usa Reports siempre que sea posible. Documentos Dinámicos son útiles para orquestar múltiples consultas y componer resultados.
-
Ejecutar un documento dinámico
var documentoSesionExecutor = new DynamicDocuments.APID.SesionC(); await documentoSesionExecutor.ExecuteAsync(dinaupClient);
Secciones
Las secciones son el equivalente a tablas/entidades por empresa (Clientes, Facturas, Empleados, etc.). En MyDinaup existen utilidades tipadas para trabajar con ellas:
- Espacio de nombres: MyDinaup.SectionsD
- Incluye métodos para leer registros por Id o por criterios.
Advertencia de rendimiento:
- Las operaciones de lectura en SectionsD consultan todos los datos de la sección y los textos principales de los datos relacionados (p. ej., en una venta también recupera empleados, clientes, productos, impuestos, etc.). Esto tiene mayor coste computacional.
- Para rendimiento óptimo, usa Reports. Para escenarios “super optimizados”, usa PGSync.
- Usa SectionsD cuando el código se ejecute con poca frecuencia o para prototipos rápidos.
-
Recibir un dato por Id
Guid paisId = /* Id del país */; var pais = await MyDinaup.SectionsD.PaisesD.GetRowByIdAsync(dinaupClient, paisId); // Usa pais.Propiedad... -
Recibir una lista
var filtros = new List<FilterCondition> { new FilterCondition( MyDinaup.SectionsD.PaisesD.PaisesES.CodigoDePaisAlfabeticoDe2Caracteres, "=", "ES" ) }; var parametros = new RowsRequestParameters(filtros.ToArray()); var paises = await MyDinaup.SectionsD.PaisesD.GetRowsAsync(dinaupClient, parametros); // GetRowsAsync devuelve directamente una List<PaisesC> foreach (var p in paises) { // p.Propiedad... } -
Recibir cabecera y líneas en una llamada
En secciones con lista (una factura y sus líneas),
GetRowsWithListAsyncdevuelve cada registro comoMainRowmás su colecciónListRows.var filas = await MyDinaup.SectionsD.FacturasD .GetRowsWithListAsync(dinaupClient, new RowsRequestParameters(facturaId.STR())); var factura = filas.FirstOrDefault(); // factura.MainRow.Propiedad... foreach (var linea in factura.ListRows) { }
Escritura
Las operaciones de escritura usan WriteOperation para altas y ediciones.
- El ID vacío ("") indica alta
- Un ID válido indica edición.
- Un ID que no existe provocará una excepción.
-
Alta simple (crear un registro)
Guid sectionId = /* sección destino */; var data = new Dictionary<string, string> { { SeccionDePruebasAPIES.TextoPrincipal, $"Prueba {Guid.NewGuid()}" } }; var wOp = new WriteOperation("", data); // "" => alta // El sectionId es un Guid: usa _SectionIDGUID (no _SectionID, que es string) await dinaupClient.RunWriteOperationAsync(SeccionDePruebasAPIES._SectionIDGUID, wOp, false); // Lanza excepción si la operación falló o no se ejecutó wOp.EnsureSuccess(); // Tras EnsureSuccess, WriteOperationResult está garantizado (no nulo) Guid newId = wOp.WriteOperationResult.RowID; -
Edición (actualizar un registro existente)
Guid rowId = /* Id del registro a editar */; var data = new Dictionary<string, string> { { SeccionDePruebasAPIES.TextoPrincipal, $"Prueba {Guid.NewGuid()}" } }; var wOp= new WriteOperation(rowId, data); await dinaupClient.RunWriteOperationAsync(sectionId, wOp, false); // Lanza excepción si la operación falló o no se ejecutó wOp.EnsureSuccess(); Guid updatedId = wOp.WriteOperationResult.RowID; -
Alta con líneas (cabecera + lista)
En secciones con lista, añade cada línea como diccionario en
DataListRows. Se guardan en la misma operación que la cabecera.var wOp = new WriteOperation("", data); wOp.DataListRows.Add(new Dictionary<string, string> { { SeccionDePruebasAPIListaES.TextoPrincipal, "Línea 1" } }); await dinaupClient.RunWriteOperationAsync(SeccionDePruebasAPIES._SectionIDGUID, wOp, false); -
Editar un solo campo
Para cambiar un valor sin montar el diccionario:
await dinaupClient.RunInlineWriteOperationAsync( SeccionDePruebasAPIES._SectionIDGUID, rowId, SeccionDePruebasAPIES.TextoPrincipal, "Nuevo valor"); -
Relación por texto (selector dinámico)
Si no tienes el Guid del registro relacionado, escribe
[Campo=Valor]y el servidor lo resuelve buscando por ese campo.var data = new Dictionary<string, string> { { ProductosES.UnidadMedidaID, $"[{UnidadesMedidaES.TextoPrincipal}=Litros]" } };
-
Alta 10 registros de golpe
var operaciones = new List<WriteOperation>(); for (int i = 0; i < 10; i++) { var fila = new Dictionary<string, string> { { SeccionDePruebasAPID.SeccionDePruebasAPIES.TextoPrincipal, $"Lote {i}: {Guid.NewGuid()}" } }; operaciones.Add(new WriteOperation("", fila)); // alta } var batchAdd = await dinaupClient.RunWriteOperationAsync(sectionId, operaciones, false); batchAdd.EnsureSuccess(); var ids = operaciones .Where(o => o.WriteOperationResult != null) .Select(o => o.WriteOperationResult.RowID) .ToList(); -
Editar 10 registros
// Partiendo de la lista 'ids' creada en el ejemplo anterior var operacionesEdicion = new List<WriteOperation>(); foreach (var id in ids) { var actualizado = new Dictionary<string, string> { { SeccionDePruebasAPID.SeccionDePruebasAPIES.TextoPrincipal, $"Editado {DateTime.UtcNow.Ticks}" } }; operacionesEdicion.Add(new WriteOperation(id.ToString(), actualizado)); // edición } var batchEdit = await dinaupClient.RunWriteOperationAsync(sectionId, operacionesEdicion, false); batchEdit.EnsureSuccess();
Cada llamada admite un máximo de 25 operaciones (MaxItemsPerWriteOperation). Si vas a procesar más, usa .Chunk(25) para dividir en bloques.
La escritura virtualizada permite que Dinaup ejecute la lógica de negocio definida en una sección. Al activarla, cualquier operación de escritura se comporta como si se hubiera realizado desde la interfaz web: con scripts, validaciones, eventos, y cálculos incluidos.
¿Qué hace la virtualización?
Cuando se activa la virtualización en una operación de escritura (RunWriteOperationAsync con el tercer parámetro, ejecución de scripts, en true), Dinaup:
- Ejecuta scripts personalizados de la sección (
onBeforeSave,onAfterSave, etc.) - Recalcula campos automáticos (ej. totales, impuestos)
- Aplica reglas de validación y lógica de negocio
- Dispara eventos definidos en la sección
Cuando se desactiva (tercer parámetro en false), se guardan los valores tal como se reciben (creando histórico). No se ejecuta ningún comportamiento adicional.
La escritura virtualizada es más costosa en términos de rendimiento.
Cada operación implica ejecutar scripts del backend y lógica personalizada, lo que puede afectar la velocidad si estás procesando muchos registros o usas esta opción innecesariamente.
¿Cuándo usar virtualización?
Usa escritura virtualizada cuando la sección contiene lógica de negocio activa que debe ejecutarse. Esto incluye cálculos automáticos de totales en ventas, generación de fechas de vencimiento, o scripts que actualizan otras entidades relacionadas. Por ejemplo, al registrar una nueva venta, al cerrar un pedido o al generar una factura, la virtualización asegura que todo se procese correctamente.
¿Cuándo evitar virtualización?
Evita la virtualización cuando realizas modificaciones simples que no requieren ejecutar lógica adicional. Esto incluye acciones como cambiar el estado de una venta, asignar un técnico a un ticket de soporte, registrar una nota interna o marcar una tarea como completada. En estos casos, desactivarla mejora el rendimiento sin afectar el comportamiento esperado.
Contexto de ejecución (atribuir operaciones a un usuario)
Cuando la app conecta con credenciales de aplicación, las operaciones no tienen un usuario detrás. DinaupContext.WithUser establece uno para todo lo que ocurra dentro del bloque using: afecta al autor del alta, al histórico y a las anotaciones.
// Solo userId
using (DinaupContext.WithUser(userId))
{
await dinaupClient.RunWriteOperationAsync(sectionId, wOp, false);
}
// Con IP y UserAgent del usuario final (quedan en el histórico)
using (DinaupContext.WithUser(userId, "192.168.1.1", "Mozilla/5.0..."))
{
await dinaupClient.RunWriteOperationAsync(sectionId, wOp, false);
}El contexto es AsyncLocal: vale para el flujo async en curso y se restaura solo al salir del using.
Fichajes (cronómetro)
Los campos de tipo cronómetro registran tiempo sobre un registro: entrada, salida y latido de continuidad (fichajes de jornada, tiempo por tarea, etc.). Se operan con Chronometer, indicando el campo, el registro y la acción.
Guid campoId = /* Id del campo cronómetro */;
Guid registroId = /* Id del registro (empleado, tarea…) */;
// Iniciar el fichaje
var inicio = dinaupClient.Chronometer(campoId, registroId, DinaupClientC.ChronometerActionE.actionStart);
// Latido periódico mientras el cronómetro corre
dinaupClient.Chronometer(campoId, registroId, DinaupClientC.ChronometerActionE.actionPing);
// Detener el fichaje
var fin = dinaupClient.Chronometer(campoId, registroId, DinaupClientC.ChronometerActionE.actionStop);
if (fin.Ok == false)
Console.WriteLine($"El fichaje no se registró: {fin.Description}");Las tres acciones (actionStart, actionPing, actionStop) devuelven un ChronometerResponse con Ok, Description, SuccessNotices y ErrorNotices. Chronometer es síncrono.
SDK .NET
Familia de paquetes NuGet oficiales de Dinaup para .NET: cliente API, base de datos, logging, validaciones, almacenamiento, utilidades y más.
Ejemplo: ejecución de WriteOperations por lotes (.NET)
Inserta o actualiza varios registros de una sección Dinaup en una sola llamada y reduce las operaciones contra la API.