Cliente Dinaup
DinaupClientC, el cliente de la API en el SDK .NET: conexión, informes tipados, secciones, escritura con WriteOperation, archivos, anotaciones, documentos dinámicos, sesiones, fichajes y estructura Flex.
DinaupClientC es el cliente de la API de Dinaup en el paquete Dinaup. Con él consultas informes y lees, exportas y escribes secciones. También maneja archivos, anotaciones, documentos dinámicos, correo, la configuración de la empresa, sesiones, fichajes y la estructura Flex.
Antes de empezar
- Dos paquetes NuGet:
Dinaup(el cliente) y tu biblioteca MyDinaup, con las clases de tus secciones e informes. Los ejemplos usanDemoUp.MyDinaup, un modelo de ejemplo; en tu proyecto cambia el prefijo por el de tu empresa. - Tres credenciales:
ConnectAsyncpideendPoint,publicKeyysecretKey. El endpoint eshttps://api.dinaup.com/v2/<clave de conexión>. La clave de conexión de tu licencia está en la app Desarrollo de Play. La clave pública y la secreta salen de una clave API creada en Administración → Claves API, que hereda los permisos del usuario al que se asocia. Ver Claves API. - Los
using:
using Dinaup; // DinaupClientC, WriteOperation, extensiones
using Dinaup.Vault; // VaultData
using static DemoUp.MyDinaup.Reports.FuncionalidadD; // informes tipados (APIVentasC, APIEntidadesC...)
using static DemoUp.MyDinaup.SectionsD; // secciones tipadas (EntidadesD, ProductosD...)La clave secreta solo se muestra una vez al crearla. Guárdala en un gestor de secretos o en el Vault, nunca en el código ni en el JavaScript de una web.
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");
}Connect es la variante síncrona con los mismos parámetros. Las dos crean el cliente, lo inicializan y comprueban la conexión.
using Dinaup;
using Dinaup.Vault;
// Las únicas credenciales en variables de entorno son las del Vault
var vault = new VaultData("VAULT_URL", "VAULT_PASSWORD");
vault.Initialize();
// Todo lo demás 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;
using Dinaup.Vault;
var builder = WebApplication.CreateBuilder(args);
var vault = new VaultData("VAULT_URL", "VAULT_PASSWORD");
vault.Initialize();
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");
// Un cliente para toda la app
builder.Services.AddSingleton(client);
var app = builder.Build();Otras formas de construirlo: new DinaupClientC(endPoint, publicKey, secretKey, defaultUserId) seguido de Initialize() o InitializeAsync(timeoutMs), o new DinaupClientC(settings) con un DinaupClientSettings. StartAutoPing() comprueba la conexión cada 30 segundos.
Opera
using static DemoUp.MyDinaup.Reports.FuncionalidadD;
// Un informe tipado de tu MyDinaup
var ventasReport = new APIVentasC();
await ventasReport.ExecuteQueryAsync(
dinaupClient: client,
page: 1,
resultsPerPage: 50
);
Console.WriteLine($"{ventasReport.Rows.Count} filas en esta página");
foreach (var row in ventasReport.Rows)
{
Console.WriteLine($"- {row.NumeroDeFacturaCompleto}: {row.Total}");
}using static DemoUp.MyDinaup.SectionsD;
var data = new Dictionary<string, string>
{
{ SeccionDePruebasAPID.SeccionDePruebasAPIES.TextoPrincipal, $"Prueba {Guid.NewGuid()}" }
};
var wOp = new WriteOperation("", data); // "" => alta
await client.RunWriteOperationAsync(SeccionDePruebasAPID._SectionIDGUID, wOp, false);
// Lanza excepción si la operación falló o no se ejecutó
wOp.EnsureSuccess();
// Tras EnsureSuccess, WriteOperationResult no es nulo
Guid newId = wOp.WriteOperationResult.RowID;Guid rowId = /* Id del registro a editar */;
var data = new Dictionary<string, string>
{
{ SeccionDePruebasAPID.SeccionDePruebasAPIES.TextoPrincipal, $"Prueba {Guid.NewGuid()}" }
};
var wOp = new WriteOperation(rowId, data);
await client.RunWriteOperationAsync(SeccionDePruebasAPID._SectionIDGUID, wOp, false);
wOp.EnsureSuccess();
Guid updatedId = wOp.WriteOperationResult.RowID;Tres identificadores de sección en MyDinaup
SeccionD._SectionID es el GUID como string; SeccionD._SectionIDGUID es el mismo valor como Guid; SeccionD.SeccionES._SectionID también es Guid. Las escrituras y RowsGetAsync piden Guid; los informes y componentes DinaZen piden string.
Tiempos de espera y reintentos
CONFIG_TIMEOUT (30.000 ms por defecto) es lo que espera cada petición que no fija su propio tiempo. Las que lo fijan, como ExecuteRawFunctionAsync(functionName, params, timeout) con un timeout positivo, esperan ese tiempo en su lugar.
Si el servidor responde 429, el cliente espera lo que diga Retry-After y repite la petición una vez. Un 503 solo se repite en las lecturas; una escritura no, porque el servidor pudo guardarla. Sin Retry-After espera 1 segundo. Si pide más de 5, o la espera no cabe en el tiempo que queda, devuelve el error sin repetir.
Informes
Los informes son consultas predefinidas y tipadas. MyDinaup genera una clase API{Nombre}C por informe, en el namespace {Empresa}.MyDinaup.Reports.{Categoria}D, que hereda de DinaupReportBase. La categoría es la carpeta del informe en Flex: en DemoUp.MyDinaup, los informes de la API están en Reports.FuncionalidadD (APIVentasC, APIEntidadesC, APIImpuestosC, APISeccionDePruebasAPIC). Los nombres salen de tu esquema, así que suelen estar en español.
ExecuteQueryAsync(dinaupClient, page, resultsPerPage, querysearch, adminMode, includeFiles, includeFieldsDetails) ejecuta el informe y deja las filas tipadas en Rows (y por id en RowsDic). resultsPerPage vale 2.000 si no lo indicas.
using static DemoUp.MyDinaup.Reports.FuncionalidadD;
var report = new APISeccionDePruebasAPIC();
await report.ExecuteQueryAsync(client, page: 1, resultsPerPage: 10);
foreach (var fila in report.Rows)
{
// fila.TextoPrincipal, fila.ValorEntero
}Cada informe expone tiempos del servidor para diagnóstico: DatabaseTimeMS (lo que tardó la base de datos), PrepareTimeMS, PostProcessTimeMS y ServerTimeMS (la suma). TraceId identifica la consulta si necesitas pedir soporte. Todos valen 0 o vacío hasta la primera consulta.
-
Filtro por campo.
AddFilter(campo, operador, valor)admitestring,int,decimal,double,bool,Guid,DateTime,DateOnlyyTimeOnly.var report = new APISeccionDePruebasAPIC(); report.AddFilter(SeccionDePruebasAPID.SeccionDePruebasAPIES.ValorEntero, "=", 3); await report.ExecuteQueryAsync(client, page: 1, resultsPerPage: 10); -
Filtro por un campo de la sección relacionada. La clave es una ruta de cuatro tramos: sección, campo de referencia, sección relacionada y campo de esa sección.
ReferenciaAutorDelAltaapunta a Base - Entidades.var relationKey = SeccionDePruebasAPID.SeccionDePruebasAPIES._SectionID.STR() + "." + SeccionDePruebasAPID.SeccionDePruebasAPIES.ReferenciaAutorDelAlta + "." + EntidadesBaseD.EntidadesBaseES._SectionID.STR() + "." + EntidadesBaseD.EntidadesBaseES.NombrePersonalRazonSocial; var report = new APISeccionDePruebasAPIC(); report.AddFilter(relationKey, "=", "Ana López"); await report.ExecuteQueryAsync(client, page: 1, resultsPerPage: 10); -
Entre dos valores.
AddFilterBetween(campo, desde, hasta)constring,int,decimal,DateOnlyoDateTime.var reportBetween = new APISeccionDePruebasAPIC(); reportBetween.AddFilterBetween(SeccionDePruebasAPID.SeccionDePruebasAPIES.ValorEntero, 1, 2999); await reportBetween.ExecuteQueryAsync(client, page: 1, resultsPerPage: 10); -
Solapamiento de rangos de fechas.
AddFilterDateRangeOverlapFilter(campoInicio, campoFin, desde, hasta)conDateOnly,DateTimeo texto.var eventosReport = new APIEventosDeCRMC(); eventosReport.AddFilterDateRangeOverlapFilter( EventosDeCRMD.EventosDeCRMES.InicioEvento_UTC, EventosDeCRMD.EventosDeCRMES.FinEvento_UTC, new DateOnly(2026, 1, 1), new DateOnly(2026, 12, 31)); await eventosReport.ExecuteQueryAsync(client, 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.await report.ExecuteQueryAsync(client, 1, 25, "iPhone Pro Max"); -
Filtro IN.
AddFilterIn(campo, valores)conGuid[],int[]ostring[]. Un solo valor se envía como=. Con la lista vacía no añade filtro y el informe devuelve todas sus filas.report.AddFilterIn(SeccionDePruebasAPID.SeccionDePruebasAPIES.ReferenciaAutorDelAlta, new[] { id1, id2 }); -
OR implícito. Dos filtros sobre el mismo campo se combinan con OR.
report.AddFilter(PaisesD.PaisesES.CodigoDePaisAlfabeticoDe2Caracteres, "=", "ES"); report.AddFilter(PaisesD.PaisesES.CodigoDePaisAlfabeticoDe2Caracteres, "=", "IT"); // Devuelve España e Italia -
Orden.
AddOrder(campo, descendente).report.AddOrder(SeccionDePruebasAPID.SeccionDePruebasAPIES.ValorEntero, true); -
Variables de informe. Si el informe define variables (un rango de fechas parametrizado, por ejemplo), pásalas con
AddVariable. Admitestring,Guid,int,decimal,bool,DateTimeyDateOnly.report.AddVariable("ClienteId", clienteId); report.AddVariable("FechaDesde", new DateOnly(2026, 1, 1)); -
Comprobar antes de añadir.
ContainsFilter(campo)dice si ya hay un filtro sobre ese campo yContainsVariable(clave), si ya hay esa variable.
Pide páginas con ExecuteQueryAsync(client, page, resultsPerPage) hasta que una vuelva con menos filas que resultsPerPage: esa es la última. CurrentPage dice qué página tienes cargada.
const int filasPorPagina = 300;
var report = new APISeccionDePruebasAPIC();
report.RequestParams.GetPageSummary = false; // sin recuento: para recorrer no hace falta
for (int pagina = 1; ; pagina++)
{
await report.ExecuteQueryAsync(client, pagina, filasPorPagina);
foreach (var row in report.Rows)
{
// procesar la fila
}
if (report.Rows.Count < filasPorPagina) break;
}Para saber si hay más filas de las que muestras, pide una de más: si con resultsPerPage + 1 llega la extra, hay más. Para recorrer el informe entero, LoadAllRowsAsync no usa OFFSET y va más rápido.
El recuento del servidor no es fiable
TotalPages y TotalResults salen de un recuento aparte que puede no completarse, y el SDK las marca [Obsolete]. Sobre un filtro sin índice el recuento no llega a tiempo: TotalPages vuelve a -1 y TotalResults con las filas de esa página. ExistNextPage compara contra TotalPages, así que entonces dice que no hay siguiente y ExecuteQuery_NextPageAsync() para en la primera página.
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.
var report = new APISeccionDePruebasAPIC();
var todas = await report.LoadAllRowsAsync(client, 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: ordénalo en memoria después. No llames a AddOrder antes: el keyset impone su propio orden y lanza excepción si ya hay uno.
Detalle, límites y excepciones en LoadAllRowsAsync.
Columnas ausentes en la respuesta
Si el modelo tipado declara una columna que el servidor ya no devuelve, el informe lanza una excepción: 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 para todo el proceso, una vez al arrancar
DinaupReportSettings.TolerateMissingColumns = true;Cada informe hereda DinaupReportSettings.TolerateMissingColumns (false por defecto) al crearse y puede sobrescribirlo por instancia.
Sin MyDinaup
Report_GetAsync(new ReportRequestParameters(reportId)) ejecuta cualquier informe por su GUID y devuelve un ReportResponse sin tipar (Report con la definición, DataList con las filas). ListReportsAsync(filtroCategoria) lista los informes disponibles. Es lo que usan los componentes de DinaZen por dentro.
Archivos
Sube archivos desde memoria o desde una URL y obtén URLs firmadas para servirlos. El tamaño máximo es Limits.MaxFileSizeInBytes: 150 MB.
-
Subir desde un array de bytes
var bytes = System.Text.Encoding.UTF8.GetBytes("hola " + Guid.NewGuid()); var upload = await client.File_UploadBytesAsync(bytes, "prueba.txt"); Guid fileId = upload.FileId; // upload.FileData trae el DinaupFileDTO: Name, Extension, Mime, SizeInBytes, CRC (SHA1), IsImage, url_original, url_1080, url_720, url_300, url_100, url_32 -
Subir desde una URL
var upload = await client.File_UploadURLAsync( "https://cdn.dinaup.com/dinaup/web/portal/img/dinabot_marca.png", "dinabot.png" ); Guid fileId = upload.FileId; -
Obtener una URL firmada de lectura
var signed = await client.File_SignURLGetAsync(fileId); var url = signed.url_original; // lista para usar; signed.Expirado dice si caducó var sinCache = await client.File_SignURLGetAsync(fileId, cachear: false);Files_SignURLGetAsync(lista de ids)firma varios en una llamada,FileGetAsync(id)yFilesGetAsync(ids)devuelven los metadatos, yUpdateSignedURL(html)refresca las URLs firmadas de un HTML a partir de su atributodata-file-id.
Calcula el SHA1 en cliente y consulta el índice de archivos antes de subir: ahorra almacenamiento y ancho de banda.
private async Task<Dinaup.DinaupFileDTO> GetExistingFileAsync(string sha1)
{
var report = new APIIndiceDeArchivosEnSistemaC();
report.AddFilter("crcbase", "=", sha1);
await report.ExecuteQueryAsync(client, 1, 1, "", true, true); // includeFiles: 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 de Dinaup admiten tres tipos de anotaciones, en el enum AnnotationTypeE:
Files(1): documentos privados asociados al registro (contratos, presupuestos, soportes).Comments(2): mensajería tipo chat para el equipo, dentro del contexto del registro.PublicGallery(3): archivos públicos en el CDN, como fotos de producto.
Los archivos de PublicGallery son públicos: cualquiera con el enlace puede verlos.
-
Agregar un comentario de texto
var anotacion = new AnotationParameters(sectionId, rowId, AnnotationTypeE.Comments) .WithText("Primer comentario desde la API."); await client.Annotation_PutAsync(anotacion); -
Agregar un comentario con archivo adjunto
var bytes = System.Text.Encoding.UTF8.GetBytes("contenido adjunto"); var upload = await client.File_UploadBytesAsync(bytes, "nota.txt"); var anotacion = new AnotationParameters(sectionId, rowId, AnnotationTypeE.Comments) .WithText("Comentario con adjunto") .WithFile(upload.FileId); await client.Annotation_PutAsync(anotacion); -
Leer anotaciones
var resultado = await client.Annotations_GetAsync(sectionId, rowId, AnnotationTypeE.Comments); foreach (var anotacion in resultado.Annotations) { var texto = anotacion.Text; var autor = anotacion.Autor; var fecha = anotacion.AnnotationDate; var adjuntos = anotacion.AttachedFiles; // List<DinaupFileDTO> }
Documentos dinámicos
Un documento dinámico es un guion que se ejecuta en el servidor y devuelve HTML, JSON u otro formato: una factura, un correo, un volcado de datos. Úsalos para componer documentos de impresión o para agrupar varias consultas en una llamada; para leer datos, un informe es más barato.
MyDinaup genera una clase por documento bajo DynamicDocuments.{Categoria}D, heredera de DinaupDynamicDocumentBase:
using DemoUp.MyDinaup;
var doc = new DynamicDocuments.APID.SesionC();
doc.SetVariableValue("ventaId", ventaId); // string, Guid, int, decimal, bool o DateOnly
var response = await doc.ExecuteAsync(client);
var html = response.Content; // el documento generado
// response.URL, response.Name, response.Status y response.Metadata completan la respuestaSin MyDinaup, DynamicDocuments_ExecuteAsync(documentId) o DynamicDocuments_ExecuteAsync(documentId, parametros) ejecutan el documento por su GUID, y DynamicDocumentsList() devuelve los documentos disponibles.
Secciones
Las secciones son las tablas de tu empresa (Clientes, Facturas, Empleados). MyDinaup genera una clase {Seccion}D por sección con lectura directa:
-
Un registro por id
var pais = await PaisesD.GetRowByIdAsync(client, paisId); -
Una lista por criterios
var parametros = new RowsRequestParameters(PaisesD.PaisesES.CodigoDePaisAlfabeticoDe2Caracteres, "=", "ES"); var paises = await PaisesD.GetRowsAsync(client, parametros); // List<PaisesC>RowsRequestParametersse construye con un id, una lista de ids, un campo con operador y valor, o variosFilterCondition(new FilterCondition(campo, operador, valor)); varios arrays de condiciones se combinan con OR.QuerySearch,Limit,SkipeIncludeAnnotationsCommentsafinan la petición.Limitvale 100 por defecto. Para leer muchas filas, pagina conSkip. -
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 AsientosContablesD.GetRowsWithListAsync(client, new RowsRequestParameters(asientoId)); var asiento = filas.FirstOrDefault(); foreach (var apunte in asiento.ListRows) { }
Los campos de referencia de una fila tipada llegan como DinaupBasicInformation: Id y SectionID del registro relacionado y su texto principal en Title (Label devuelve lo mismo). ToWriteOperationValue() da el texto que espera una escritura.
var ventas = await VentasIngresosD.GetRowsAsync(client, new RowsRequestParameters(ventaId));
foreach (var v in ventas)
{
Console.WriteLine($"{v.ReferenciaCliente.Title} ({v.ReferenciaCliente.Id})");
}Sin MyDinaup, RowsGetAsync(sectionId, includeList, parameters) devuelve las filas sin tipar. GetHistoryChanges(sectionId, rowId, campo) lista el histórico de cambios de un registro y GetBacklinksAsync(sectionId, rowId) quién lo referencia.
Leer con SectionsD recupera la sección entera y los textos principales de sus relaciones (en una venta, también empleados, clientes, productos e impuestos), así que cuesta más. Para rendimiento, usa informes; para volumen, PG Sync. SectionsD encaja en código que se ejecuta pocas veces o en prototipos.
Exportar una sección a CSV
ExportAsync vuelca una sección a un CSV en el servidor y devuelve un ExportDTO con su URL firmada. Es la única lectura del cliente sin tope de filas.
// La sección entera, con todas sus columnas exportables
var export = await client.ExportAsync(ProductosD._SectionIDGUID);
// Solo esas columnas y solo las filas que cumplen el filtro
var columnas = new[] { ProductosD.ProductosES.TextoPrincipal, ProductosD.ProductosES.ReferenciaCodigoDeBarras };
var filtro = new List<FilterCondition> { new FilterCondition(ProductosD.ProductosES.Eliminado, "=", "0") };
var parcial = await client.ExportAsync(ProductosD._SectionIDGUID, columnas, filtro);
// Lo que cambió desde el export anterior: encadena siempre con NextFromUtc
var cambios = await client.ExportAsync(ProductosD._SectionIDGUID, export.NextFromUtc);
if (cambios.State == ExportStateE.Ready)
{
var csv = await client.DownloadExportAsync(cambios);
foreach (var fila in csv.Rows)
{
var nombre = fila[ProductosD.ProductosES.TextoPrincipal];
}
}| Miembro | Qué hace |
|---|---|
ExportAsync(sectionId, fromUtc) | Todas las columnas exportables. Con fromUtc, solo las filas con cambios desde esa fecha, bajas incluidas. |
ExportAsync(sectionId, columns, fromUtc) | Solo esas columnas, en ese orden. Pedir una que no se exporta es un error con la lista de las que no valen. |
ExportAsync(sectionId, columns, filter, fromUtc) | Filtra filas con condiciones en AND (=, <>, >, <, >=, <=); varias = sobre el mismo campo son un IN. |
ExportStateAsync(exportId) | El estado de una exportación ya iniciada, sin esperar. |
DownloadExportAsync(export) | Descarga el fichero y devuelve un ExportResult: Columns, TotalRows y Rows, que se leen por nombre de campo al recorrerlas. |
ExportDTO trae State (Working, Ready, Failed o Busy), ExportId, Url, Rows, Bytes, NextFromUtc, Filter y Message. Dinaup hace una exportación a la vez por empresa: ExportAsync espera turno un rato. Si el fichero no está listo, devuelve Working con su ExportId; consulta su estado con ExportStateAsync y descárgalo al estar Ready, que el resultado no se guarda indefinidamente.
En el CSV no salen la lista de la sección ni los campos de contraseña, confidenciales, eliminados u obsoletos. Para las líneas, exporta la sección de lista y crúzala con su registro principal. Con fromUtc y filtro a la vez, una fila que deja de cumplir el filtro no sale en el siguiente incremental: para sincronizar, filtra en destino.
Escritura
Las altas y las ediciones viajan en un WriteOperation: el id del registro más un diccionario campo a valor. Un id vacío ("" o Guid.Empty) es un alta; un id existente, una edición; un id que no existe, un error.
Los valores viajan como texto y el formato importa: decimal con punto y sin separador de miles ("1234.5"), booleano "1" o "0", fecha y hora "yyyy-MM-dd HH:mm:ss", fecha "yyyy-MM-dd", hora "HH:mm:ss" y una referencia vacía como cadena vacía. En .NET, .STR() sobre Guid, bool, DateTime y DateOnly y .ToSQL() sobre números y fechas ya dan ese formato.
Si falla, el motivo está en UserError
RunWriteOperationAsync devuelve un WriteOperationResponse. UserError agrega el error de cada fila y, si no hubo respuesta, usa el error HTTP. resp.EnsureSuccess() lanza ese texto; wOp.EnsureSuccess() lanza el error de esa fila (WriteOperationResult.AError) o "No se ha ejecutado la operación." si no llegó resultado.
var resp = await client.RunWriteOperationAsync(sectionId, wOp, true);
if (resp.IsSuccess == false)
{
throw new Exception(resp.UserError);
}Un rechazo de negocio no lanza excepción
Si el servidor rechaza la fila por validación no hay excepción que capturar: la operación vuelve con Confirmed en false y el motivo en AError. Un try/catch alrededor de la escritura no detecta ese caso; comprueba el resultado.
Un timeout no significa que no se guardó
Cada escritura espera como mucho lo que marque CONFIG_TIMEOUT: 30 segundos por defecto. Si expira, la petición ya viajó y el registro puede estar guardado: repetirla a ciegas duplica datos. Ante un timeout, consulta el estado antes de reintentar.
-
Alta simple
var data = new Dictionary<string, string> { { SeccionDePruebasAPID.SeccionDePruebasAPIES.TextoPrincipal, $"Prueba {Guid.NewGuid()}" } }; var wOp = new WriteOperation("", data); // "" => alta await client.RunWriteOperationAsync(SeccionDePruebasAPID._SectionIDGUID, wOp, false); wOp.EnsureSuccess(); Guid newId = wOp.WriteOperationResult.RowID; -
Edición
var wOp = new WriteOperation(rowId, data); // Guid del registro await client.RunWriteOperationAsync(SeccionDePruebasAPID._SectionIDGUID, wOp, false); wOp.EnsureSuccess(); -
Alta con líneas. 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> { { SeccionDePruebasAPIListaD.SeccionDePruebasAPIListaES.TextoPrincipal, "Línea 1" } }); await client.RunWriteOperationAsync(SeccionDePruebasAPID._SectionIDGUID, wOp, false); -
Editar, añadir o borrar líneas de un registro existente. Cada línea de
DataListRowslleva su"id": el Guid de una línea existente la edita, vacío o ausente crea una nueva y"eliminado"a"1"la borra. Las líneas que no envías quedan como están.var wOp = new WriteOperation(rowId); // la cabecera, aunque no cambie ningún campo suyo wOp.DataListRows.Add(new Dictionary<string, string> { { "id", lineaId.ToString() }, { SeccionDePruebasAPIListaD.SeccionDePruebasAPIListaES.TextoPrincipal, "Línea corregida" } }); wOp.DataListRows.Add(new Dictionary<string, string> { { "id", "" }, { SeccionDePruebasAPIListaD.SeccionDePruebasAPIListaES.TextoPrincipal, "Línea nueva" } }); wOp.DataListRows.Add(new Dictionary<string, string> { { "id", otraLineaId.ToString() }, { "eliminado", "1" } }); await client.RunWriteOperationAsync(SeccionDePruebasAPID._SectionIDGUID, wOp, false); wOp.EnsureSuccess();La sección destino es siempre la principal
MyDinaup genera constantes también para la sección de lista, pero escribir contra ella no está soportado: los scripts y cálculos están en la sección principal. Las líneas viajan siempre en
DataListRows. El detalle, en Escribir en secciones. -
Editar un solo campo
await client.RunInlineWriteOperationAsync( SeccionDePruebasAPID._SectionIDGUID, rowId, SeccionDePruebasAPID.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 de la sección relacionada. Solo resuelve campos cuyo nombre interno empieza pornombreo porpr_.DinaupBasicInformation.SetDynamicSelector(campo, valor)construye el mismo texto.var data = new Dictionary<string, string> { { ProductosD.ProductosES.ReferenciaUnidadDeMedidaPesoBase, "[nombre=Kilogramos]" } };Un enlace sin fila hace fallar la escritura entera
Un
[Campo=Valor]que no encuentra ningún registro rechaza la operación completa, con el lote entero, antes de escribir nada. Comprueba que el valor existe o crea antes el registro relacionado. -
Editar por un campo alternativo. Con
mainKeyla operación identifica el registro por otro campo único (un SKU, por ejemplo) en vez de porid;listKeyhace lo mismo con las líneas.await client.RunWriteOperationAsync(ProductosD._SectionIDGUID, wOp, false, mainKey: ProductosD.ProductosES.ReferenciaCodigoDeBarras);
-
Alta de 10 registros en una llamada
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)); } var batchAdd = await client.RunWriteOperationAsync(SeccionDePruebasAPID._SectionIDGUID, operaciones, false); batchAdd.EnsureSuccess(); var ids = operaciones .Where(o => o.WriteOperationResult != null) .Select(o => o.WriteOperationResult.RowID) .ToList(); -
Edición de 10 registros
var operacionesEdicion = ids.Select(id => new WriteOperation(id, new Dictionary<string, string> { { SeccionDePruebasAPID.SeccionDePruebasAPIES.TextoPrincipal, $"Editado {DateTime.UtcNow.Ticks}" } })).ToList(); var batchEdit = await client.RunWriteOperationAsync(SeccionDePruebasAPID._SectionIDGUID, operacionesEdicion, false); batchEdit.EnsureSuccess();
Cada llamada admite un máximo de 25 operaciones (DinaupClientC.MaxItemsPerWriteOperation) y 2.500 elementos contando las líneas (MaxTotalItemsPerWriteOperation). Para más, divide la lista con .Chunk(25). Una lista vacía se rechaza.
Todas las operaciones del lote, con los mismos campos
Un lote exige que todas las operaciones lleven el mismo juego de campos. Una con tres y otra con cuatro (o con tres distintos) lanza ArgumentException y no se escribe ninguna. Si un valor no aplica a una fila, añádelo vacío.
Un lote puede quedar a medias
Por defecto (allOrNone: false) no se deshace nada: si una operación falla, las demás se escriben igual. Recorre la lista al terminar, comprueba el WriteOperationResult de cada operación y reintenta solo las que fallaron. Con allOrNone: true, el servidor deshace el lote entero si alguna falla. Solo vale sin scripts: con scripts, el servidor rechaza la llamada (E-6191).
El tercer parámetro de RunWriteOperationAsync decide si el servidor ejecuta los scripts de la sección. Con true, la escritura se comporta como si viniera del formulario: scripts, validaciones, autorrellenados y campos calculados (totales, impuestos, vencimientos). Con false, los valores entran tal cual, con histórico, y nada más.
Activa los scripts cuando la sección tenga lógica que deba ejecutarse: una venta nueva, el cierre de un pedido, una factura. Desactívalos en cambios simples que no dependen de esa lógica (cambiar un estado, asignar un técnico, marcar una tarea) y en los borrados. Cuesta más: cada operación ejecuta la lógica de la sección en el servidor.
Con scripts, Dinaup ejecuta las operaciones de una en una. Lanzar lotes en paralelo desde varios hilos no acelera: esperan turno. Las escrituras sin scripts no esperan ese turno.
Qué hace cada modo con los autorrellenados, el orden de los campos y las líneas, en Escribir en secciones.
Las filas tipadas de MyDinaup ({Seccion}C) también se escriben directamente: RunWriteOperationAsync(fila, withsSripts) acepta un DinaupRowBase, una lista de ellos, o una cabecera con su lista de líneas, y fila.ToWriteOperation() construye el WriteOperation equivalente.
Contexto de ejecución
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 client.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 client.RunWriteOperationAsync(sectionId, wOp, false);
}El contexto es AsyncLocal: vale para el flujo asíncrono en curso y se restaura al salir del using. DinaupContext.CurrentSession devuelve la sesión activa.
Fichajes
Dos familias de métodos registran tiempo:
- Jornada.
Timesheet_StatusAsync(employeeIdentifier)devuelve el estado del fichaje del empleado yTimesheet_ClockAsync(employeeIdentifier, fingerprintIp, fingerprintBrowser, fingerprintUrl, fingerprintId, fingerprintRaw)ficha entrada o salida con la huella del dispositivo. - Pausas.
Break_StatusAsync,Break_StartAsync(employeeIdentifier, breakTypeId, ...)yBreak_StopAsyncabren y cierran una pausa con la misma huella.
Correo
SendEmailAsync(parameters, timeoutMS) envía un correo desde Dinaup. El cuerpo es HTML y sale tal cual, sin plantilla ni firma. timeoutMS vale 30.000 por defecto.
var upload = await client.File_UploadBytesAsync(pdf, "factura.pdf");
var correo = new SendEmailParameters("cliente@ejemplo.com", "Tu factura", cuerpoHtml)
{
Sender = "facturacion@miempresa.com",
ReplyTo = "facturacion@miempresa.com",
AttachmentIds = new List<Guid> { upload.FileId }
};
var resp = await client.SendEmailAsync(correo);
if (resp.Sent == false)
{
throw new Exception(resp.PendingReason);
}SendEmailParameters recibe destinatario, asunto y cuerpo, y admite RecipientName, Cc, Bcc, ReplyTo, Sender, SenderName, AttachmentIds, EntityId, RelatedSectionId y RelatedRowId.
Sendertiene que ser de un dominio verificado de tu licencia; si no, el correo no sale yPendingReasondice por qué. SinSender, el remitente es elnoreply@del primer dominio verificado, nunca una dirección@dinaup.com.- Como mucho 50 direcciones contando las copias y 10 adjuntos. Los adjuntos van por id de un archivo ya subido.
- Comprueba siempre
Sent. El SDK compara el eco del servidor (destinatario, copias, adjuntos, dirección de respuesta y remitente) con lo pedido. Si no coincide, dejaSentenfalsecon el motivo enPendingReason.Result.Fromdice desde qué dirección salió. - En una instalación local (on-premise) el envío no está disponible y
PendingReasonlo indica.
Clave-valor de la empresa
KV_GetAsync(space, key) y KV_SetAsync(space, key, value) guardan pares en la sección Dinaup KV de la empresa. Es configuración de tu app, visible y editable como cualquier otro dato. No caduca: el valor queda hasta que alguien lo reescribe.
bool guardado = await client.KV_SetAsync("miapp", "modo", "produccion");
string modo = await client.KV_GetAsync("miapp", "modo"); // "" si no existe
// Un objeto viaja como JSON y se lee con la sobrecarga genérica
await client.KV_SetAsync("miapp", "ajustes", ajustes);
var leidos = await client.KV_GetAsync<AjustesDTO>("miapp", "ajustes");- El espacio es libre y
""es el global. Dos apps que escriben en el mismo espacio sobrescriben los valores de la otra. - Espacio y clave admiten hasta 200 caracteres; el valor, hasta 100.000. Por encima, el SDK lanza
ArgumentExceptionsin llamar al servidor. - Escribir
""deja el par vacío, no lo borra.
MyAppKVClient es otro almacén: el de tu app, compartido por todas las empresas. Ver Apps multi-tenant.
Patrones
-
Servicio con inyección de dependencias
public class ProductosService(DinaupClientC client) { public async Task<List<APIProductosC.APIProductos_RowC>> GetProductosAsync() { var report = new APIProductosC(); await report.ExecuteQueryAsync(client, 1, 1000); return report.Rows; } } -
Sincronizar desde una API externa. Mismo método para crear y para actualizar: id vacío crea, id existente actualiza. Divide la lista por el tope del lote.
public async Task SyncProductosAsync(List<ProductoExterno> externos) { var operaciones = externos.Select(e => new WriteOperation(e.DinaupId ?? Guid.Empty, new Dictionary<string, string> { { ProductosD.ProductosES.ReferenciaCodigoDeBarras, e.SKU }, { ProductosD.ProductosES.TextoPrincipal, e.Nombre } })).ToList(); foreach (var lote in operaciones.Chunk(DinaupClientC.MaxItemsPerWriteOperation)) { var resp = await client.RunWriteOperationAsync(ProductosD._SectionIDGUID, lote, false); resp.EnsureSuccess(); } } -
Servicio en segundo plano
public class TareasWorker(DinaupClientC client) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken ct) { while (ct.IsCancellationRequested == false) { var pendientes = new APITareasDeProyectosC(); await pendientes.ExecuteQueryAsync(client, 1, 100); foreach (var tarea in pendientes.Rows) await ProcesarAsync(tarea); await Task.Delay(TimeSpan.FromMinutes(5), ct); } } }
Todos los métodos del cliente
Referencia por familias. Cada método público de DinaupClientC está en una fila.
| Familia | Métodos |
|---|---|
| Conexión | ConnectAsync y Connect (estáticos), Initialize(enableAutoPing), InitializeAsync(timeoutMs, enableAutoPing), InitializeWitOuthCheckConnections, setConfig, setDefaultHeaders, setDefaultParameters, PING(forzar), PingAsync, PingCmd, PingCmdWithExceptions, Reconnect, ReconnectAsync, StartAutoPing, StopAutoPing, HealthCheckAsync, CheckHealthAsync (IHealthCheck), IsConnected, DescripcionDeErrorDeConexion, CONFIG_TIMEOUT, CONFIG_MAX_ROWS_PAGE_REPORT, Dispose. |
| Llamadas directas | ExecuteRawFunctionAsync(functionName, params, timeout) ejecuta cualquier función de la API por nombre; ExecuteApiFunctionAsJsonAsync. |
| Informes y consultas | Report_GetAsync(ReportRequestParameters), ListReportsAsync(filtroCategoria), IAQuery_GetAsync (IAQuery), QuickQuery (QuickQuery), Dashboards_Get(dashId). El filtro guardado de un informe, en piezas: GetReportFilterAsync(reportId), SaveReportFilterAsync(reportId, blocks, expectedVersion) y GetReportFilterOperatorsAsync(). |
| Filas | RowsGetAsync(sectionId, includeList, parameters), GetHistoryChanges(sectionId, rowId, field), GetBacklinksAsync(sectionId, rowId), FechaIAGetAsync(parameters). |
| Exportación | ExportAsync(sectionId, columns, filter, fromUtc) y sus sobrecargas, ExportStateAsync(exportId), DownloadExportAsync(export), ExportTimeoutMs (60.000). Ver Exportar una sección a CSV. |
| Escritura | RunWriteOperation y RunWriteOperationAsync (por WriteOperation, lista de WriteOperation, DinaupRowBase, lista de filas o cabecera con líneas; parámetros withsSripts, mainKey, listKey, allOrNone), RunInlineWriteOperationAsync(sectionId, rowId, field, value), VirtualFormToWriteOperation (estático), MaxItemsPerWriteOperation (25), MaxTotalItemsPerWriteOperation (2.500). |
| Archivos | File_UploadBytesAsync, File_UploadURLAsync (y sus variantes síncronas), File_SignURLGetAsync(id, cachear, onlycache), Files_SignURLGetAsync(ids), File_SignURLInTextGet(html), UpdateSignedURL(html), FileGetAsync(id, disablecache), FilesGetAsync(ids), File_GetListAsync(ids), File_GetURLPublicFile(keywordLic, publicFileId, fileName), File_IsPublicFile(id) (estático), DownloadFileURL(url). |
| Anotaciones | Annotation_PutAsync(AnotationParameters), Annotations_GetAsync(sectionId, rowId, type). |
| Documentos dinámicos | DynamicDocuments_ExecuteAsync(documentId), DynamicDocuments_ExecuteAsync(documentId, params), DynamicDocuments_ExecuteAsync(DynamicDocumentGetRequestParameters), DynamicDocumentsList(). |
| Esquema | GetBusinessSchem() (todo el esquema), GetBusinessSchemaFields(sectionId, includeAll) (los campos de una sección con su maqueta), GetCorruptObjectsAsync() (objetos de estructura rotos y su motivo). |
| Estructura Flex | Lectura: Flex_GetObjectAsync(objectType, id), Flex_ListObjectsAsync(objectType, sectionId, includeDeleted, includeObsolete, search), Flex_GetEditablesAsync(objectType) y Flex_GetEditablesAllAsync() (contrato de escritura de cada tipo), Flex_GetEnumAsync(enumName), Flex_GetFieldEnumAsync(fieldId), Flex_GetEnumsAllAsync(), Flex_GetScriptFunctionsAsync() (catálogo de funciones de DinaScript con su firma), Flex_GetRoutesAsync(prefix, sectionId, path) (qué se puede escribir en un script de una sección), Flex_GetQuestionsAsync(objectType, id) (las preguntas de un informe, documento o algoritmo). Escritura: Flex_ValidateCodeAsync(code, sectionId, eventFields, relatedSectionId, algorithmId, algorithmType, isFilter, mainRowsToProcess, relatedRowsToProcess) compila un script sin guardarlo y devuelve los fallos con su rango de caracteres. Con algorithmId o algorithmType valida la fórmula de un algoritmo, y con isFilter, su filtro. Flex_CreateObjectAsync(objectType, parentId, patch, actorId), Flex_UpdateObjectAsync(objectType, id, patch, expectedVersion, dryRun, ...), Flex_SetQuestionsAsync(objectType, id, questions, expectedVersion, dryRun, actorId), Flex_DeleteObjectAsync (borrado lógico), Flex_UndeleteObjectAsync, Flex_ApplyLayoutAsync (lote atómico de posiciones de campo, o un campo suelto) y Flex_ConsolidateAsync() (hace efectivos los cambios pendientes del esquema). Todas admiten dryRun donde tiene sentido. Una sección se retira con Flex_UpdateObjectAsync(FlexObjectTypes.Sections, id, patch) y obsoleto a "1" en el patch: sigue en la base y sale del producto. En una sección de lista se rechaza; se retira la principal. Al crear campos (FlexObjectTypes.Fields) o columnas de informe (FlexObjectTypes.ReportColumns), consolidar a "0" en el patch crea sin consolidar. El objeto no se usa hasta Flex_ConsolidateAsync(), que se llama una vez al final. Qué es cada objeto, en Dinaup Flex. |
| Paneles | GetDashboardForEditAsync(dashboardId), SaveDashboardAsync(dashboardId, name, iconId, accessAll, accessRecords), DeleteDashboardAsync(dashboardId) (baja lógica que no se deshace desde fuera; devuelve "" o el motivo del rechazo), AddDashboardWidgetAsync(dashboardId, kind, target, gridColumns), SetDashboardWidgetModeAsync, DeleteDashboardWidgetAsync, SetDashboardPositionsAsync. |
| Roles | GetRolesAsync(), GetRoleAsync(rolId), SaveRoleAsync(RoleDTO), CreateRoleAsync(name, descripcion) (nace sin permisos), DeleteRoleAsync (baja lógica), RestoreRoleAsync. |
| Sesiones y cuentas | Session_SignInAsync, Session_SignOutAsync, Session_GetDetailsAsync, Session_GetDetailsWithCacheCompatibilityAsync, Session_GetIfCached, Session_CheckTwoFactor, Session_CreateLoginCodeAsync y Session_SignInWithCodeAsync (acceso sin contraseña), Session_RegisterAccountAsync, Session_ConfirmAccountRegistrationAsync, Session_RecoverPasswordAsync, Session_CreatePasswordRecoveryCodeAsync, Session_ChangePasswordWithCodeAsync, Session_EntitiesIdWithActiveSession, DefaultSession, DefaultSessionUpdateAsync. Para cambiar una contraseña: Session_CreatePasswordRecoveryCodeAsync y después Session_ChangePasswordWithCodeAsync. Firmas y flujo en Apps multi-tenant. |
| Fichajes | Timesheet_StatusAsync, Timesheet_ClockAsync, Break_StatusAsync, Break_StartAsync, Break_StopAsync. |
| Correo | SendEmailAsync(parameters, timeoutMS). Ver Correo. |
| Clave-valor de la empresa | KV_GetAsync(space, key), KV_GetAsync<T>(space, key), KV_SetAsync(space, key, value), KV_SetAsync<T>(space, key, value). Ver Clave-valor de la empresa. |
| Validación fiscal | CheckVATViesAsync(vatid), CheckVATAEATAsync(vatid, name). Ver Dinaup.Validations. |
| Módulos Flex | GetModuleGalleryAsync() y GetModulePlanAsync(licserie, canal, version) (qué instalaría, sin cambiar nada). |
| Formularios virtuales | FormOpenAsync, FormOpenDataFlowAsync, FormUpdate, FormSave, FormCancel, FormSendClick, FormNewListItem, FormRemoveListItem, FormAutoCumpleteDataFlow, GetOpenedWindows, OpenedFormAddRelation. Son el motor de DnzFormView en DinaZen; en una app normal no se llaman a mano. FormSave envía también las ediciones que el servidor aún no ha recibido y lanza Exceptions.FormClosedException si el formulario ya no está abierto. VirtualFormDTO.RowExists es true cuando el registro existe. |
Otros canales
Los mismos datos salen de Dinaup por otras vías cuando el SDK .NET no encaja:
| Necesitas | Canal |
|---|---|
| Probar una petición sin escribir código | Zona de pruebas de Play |
| Usar la API desde cualquier lenguaje | Referencia de la API REST |
| Que Dinaup avise a otro programa cuando entra un pedido o cambia una ficha | Webhooks salientes |
| Reaccionar a miles de cambios con latencia mínima | Eventos Redis |
| Que otro programa envíe datos a Dinaup sin programar | Zapier, Make y n8n |
| Leer volumen por SQL | PG Sync |
| Pedir datos en lenguaje natural | Chat con IA |
Después
- Las primeras escrituras, sin afectar a datos reales, en la Zona de pruebas de Play.
- Los IDs de secciones, campos e informes, en Esquema.
- Qué pasa en el servidor al escribir (autorrellenados, orden de los campos, scripts), en Escribir en secciones.
- Los mismos datos por REST desde cualquier lenguaje, en Referencia de la API REST.