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 usan DemoUp.MyDinaup, un modelo de ejemplo; en tu proyecto cambia el prefijo por el de tu empresa.
  • Tres credenciales: ConnectAsync pide endPoint, publicKey y secretKey. El endpoint es https://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.MyDinaup

Conecta

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) admite string, int, decimal, double, bool, Guid, DateTime, DateOnly y TimeOnly.

    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. ReferenciaAutorDelAlta apunta 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) con string, int, decimal, DateOnly o DateTime.

    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) con DateOnly, DateTime o 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 querysearch de ExecuteQueryAsync busca 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) con Guid[], int[] o string[]. 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. Admite string, Guid, int, decimal, bool, DateTime y DateOnly.

    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 y ContainsVariable(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) y FilesGetAsync(ids) devuelven los metadatos, y UpdateSignedURL(html) refresca las URLs firmadas de un HTML a partir de su atributo data-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 respuesta

Sin 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>

    RowsRequestParameters se construye con un id, una lista de ids, un campo con operador y valor, o varios FilterCondition (new FilterCondition(campo, operador, valor)); varios arrays de condiciones se combinan con OR. QuerySearch, Limit, Skip e IncludeAnnotationsComments afinan la petición. Limit vale 100 por defecto. Para leer muchas filas, pagina con Skip.

  • Cabecera y líneas en una llamada. En secciones con lista (una factura y sus líneas), GetRowsWithListAsync devuelve cada registro como MainRow más su colección ListRows.

    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];
    }
}
MiembroQué 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 DataListRows lleva 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 por nombre o por pr_. 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 mainKey la operación identifica el registro por otro campo único (un SKU, por ejemplo) en vez de por id; listKey hace 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 y Timesheet_ClockAsync(employeeIdentifier, fingerprintIp, fingerprintBrowser, fingerprintUrl, fingerprintId, fingerprintRaw) ficha entrada o salida con la huella del dispositivo.
  • Pausas. Break_StatusAsync, Break_StartAsync(employeeIdentifier, breakTypeId, ...) y Break_StopAsync abren 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.

  • Sender tiene que ser de un dominio verificado de tu licencia; si no, el correo no sale y PendingReason dice por qué. Sin Sender, el remitente es el noreply@ 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, deja Sent en false con el motivo en PendingReason. Result.From dice desde qué dirección salió.
  • En una instalación local (on-premise) el envío no está disponible y PendingReason lo 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 ArgumentException sin 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.

FamiliaMétodos
ConexiónConnectAsync 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 directasExecuteRawFunctionAsync(functionName, params, timeout) ejecuta cualquier función de la API por nombre; ExecuteApiFunctionAsJsonAsync.
Informes y consultasReport_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().
FilasRowsGetAsync(sectionId, includeList, parameters), GetHistoryChanges(sectionId, rowId, field), GetBacklinksAsync(sectionId, rowId), FechaIAGetAsync(parameters).
ExportaciónExportAsync(sectionId, columns, filter, fromUtc) y sus sobrecargas, ExportStateAsync(exportId), DownloadExportAsync(export), ExportTimeoutMs (60.000). Ver Exportar una sección a CSV.
EscrituraRunWriteOperation 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).
ArchivosFile_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).
AnotacionesAnnotation_PutAsync(AnotationParameters), Annotations_GetAsync(sectionId, rowId, type).
Documentos dinámicosDynamicDocuments_ExecuteAsync(documentId), DynamicDocuments_ExecuteAsync(documentId, params), DynamicDocuments_ExecuteAsync(DynamicDocumentGetRequestParameters), DynamicDocumentsList().
EsquemaGetBusinessSchem() (todo el esquema), GetBusinessSchemaFields(sectionId, includeAll) (los campos de una sección con su maqueta), GetCorruptObjectsAsync() (objetos de estructura rotos y su motivo).
Estructura FlexLectura: 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.
PanelesGetDashboardForEditAsync(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.
RolesGetRolesAsync(), GetRoleAsync(rolId), SaveRoleAsync(RoleDTO), CreateRoleAsync(name, descripcion) (nace sin permisos), DeleteRoleAsync (baja lógica), RestoreRoleAsync.
Sesiones y cuentasSession_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.
FichajesTimesheet_StatusAsync, Timesheet_ClockAsync, Break_StatusAsync, Break_StartAsync, Break_StopAsync.
CorreoSendEmailAsync(parameters, timeoutMS). Ver Correo.
Clave-valor de la empresaKV_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 fiscalCheckVATViesAsync(vatid), CheckVATAEATAsync(vatid, name). Ver Dinaup.Validations.
Módulos FlexGetModuleGalleryAsync() y GetModulePlanAsync(licserie, canal, version) (qué instalaría, sin cambiar nada).
Formularios virtualesFormOpenAsync, 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:

NecesitasCanal
Probar una petición sin escribir códigoZona de pruebas de Play
Usar la API desde cualquier lenguajeReferencia de la API REST
Que Dinaup avise a otro programa cuando entra un pedido o cambia una fichaWebhooks salientes
Reaccionar a miles de cambios con latencia mínimaEventos Redis
Que otro programa envíe datos a Dinaup sin programarZapier, Make y n8n
Leer volumen por SQLPG Sync
Pedir datos en lenguaje naturalChat con IA

Después

En esta página