Apps multi-tenant
Conecta tu aplicación a Dinaup como plataforma: login de usuarios, un espacio de datos por empresa y almacenamiento propio de la app.
Una app multi-tenant es una sola aplicación tuya que da servicio a muchas empresas, cada una con sus usuarios y sus datos. Tú escribes la interfaz y la lógica de tu producto; Dinaup pone el login, el aislamiento entre empresas y la base de datos. No montas backend.
Las tres piezas
| Pieza | Qué es | Ciclo de vida |
|---|---|---|
DinaupExternalAppSettings | La identidad de tu aplicación: AppId + AppToken. | Singleton |
DinaupAuthClient | Autentica usuarios por email y contraseña. Devuelve a qué empresa pertenecen. | Singleton |
MyAppClient | El cliente de datos de un usuario concreto, atado a su empresa. | Uno por sesión (Scoped) |
MyAppClient hereda de DinaupClientC: informes, escritura, archivos y anotaciones funcionan igual que en el Cliente Dinaup. La diferencia es a qué datos llega: cada instancia opera solo sobre la empresa del usuario autenticado.
Montar la app
Carga la identidad de la app
Las credenciales viven en el Vault con dos parámetros obligatorios: DINAUP_APPID (un GUID) y DINAUP_APPTOKEN.
var vault = new Dinaup.Vault.VaultData(vaultUrl, vaultPassword);
vault.Initialize();
var appConfig = new Dinaup.Models.DinaupExternalAppSettings(vault);Si prefieres pasarlas directas: new DinaupExternalAppSettings(appId, appToken).
Registra los servicios
// Compartidos por todos los usuarios
builder.Services.AddSingleton<Dinaup.Models.DinaupExternalAppSettings>(appConfig);
builder.Services.AddSingleton<Dinaup.Auth.DinaupAuthClient>(new Dinaup.Auth.DinaupAuthClient());
builder.Services.AddSingleton<Dinaup.MyAppKVClient>();
// Uno por usuario/circuito
builder.Services.AddScoped<SessionUserContext>();
builder.Services.AddHealthChecks().AddCheck<Dinaup.MyAppKVClient>("MyAppKVClient");El MyAppClient y cualquier servicio que lo use van en Scoped. Un singleton compartiría la sesión de una empresa con los usuarios de otra.
Autentica al usuario
LoginAsync valida las credenciales contra Dinaup y devuelve un AuthResponse con el TenantConnectionKeyword: la clave que identifica la empresa del usuario. Con ella construyes su MyAppClient.
var authResponse = await _authClient.LoginAsync(email, password);
if (authResponse == null) throw new Exception("Credenciales inválidas");
DinaupClient = new Dinaup.MyAppClient(_appSettings, authResponse);Para que la sesión sobreviva a recargas, el navegador guarda solo una cookie HttpOnly con un id de sesión. El estado (keyword y email) se guarda en el almacén clave-valor bajo ese id, y al volver se recupera y se reconstruye el cliente:
// Al hacer login: estado al KV, solo el id a la cookie
var sessionId = Guid.NewGuid().ToString();
await _kvClient.SetKVAsync($"session:{sessionId}",
$"{authResponse.TenantConnectionKeyword}|{authResponse.UserEmail}");
httpContext.Response.Cookies.Append("dinaup_sessionid", sessionId, cookieOptions);
// Al volver (InitializeAsync): recuperar el estado y reconstruir
var sessionId = httpContext.Request.Cookies["dinaup_sessionid"];
if (!string.IsNullOrEmpty(sessionId))
{
var estado = await _kvClient.GetKVAsync($"session:{sessionId}");
if (!string.IsNullOrEmpty(estado))
{
var partes = estado.Split('|');
DinaupClient = new Dinaup.MyAppClient(_appSettings, partes[0], partes[1]);
}
}Con el estado de sesión fuera del proceso, la app es stateless: puedes desplegar el mismo contenedor en varios nodos detrás de un balanceador y la cookie vale en cualquiera. Un nodo caído no cierra la sesión de nadie.
Opera con los datos de su empresa
El cliente ya está atado al tenant. Un servicio de dominio típico recibe el contexto de sesión y usa su cliente:
public class PaisesService
{
private readonly SessionUserContext _session;
public PaisesService(SessionUserContext session) => _session = session;
public async Task<List<APIPaisesC.APIPaises_RowC>> GetAllAsync()
{
var rpt = new APIPaisesC();
await rpt.ExecuteQueryAsync(_session.DinaupClient, 1, 1000);
return rpt.Rows;
}
}Informes, WriteOperation, archivos y anotaciones: todo lo del Cliente Dinaup aplica sin cambios.
Cuentas de usuario
MyAppClient hereda las operaciones de cuenta de DinaupClientC. Tu app puede dar de alta usuarios, activarlos y gestionar contraseñas sin pantallas de Dinaup. Todas reciben el userAgent y la IP del usuario final.
| Método | Qué hace |
|---|---|
Session_RegisterAccountAsync(model, userAgent, ip) | Crea la cuenta temporal y genera el código de activación. |
Session_ConfirmAccountRegistrationAsync(model, userAgent, ip) | Activa la cuenta con el código recibido. Segundo paso del registro. |
Session_SignInAsync(email, password, userAgent, ip) | Inicia sesión y devuelve el estado (correcta, 2FA pendiente, rechazada). |
Session_CheckTwoFactor(sessionId, code, userAgent, ip) | Verifica el código 2FA de una sesión pendiente. |
Session_CreatePasswordRecoveryCodeAsync(model, userAgent, ip) | Genera un código de recuperación de contraseña. |
Session_ChangePasswordWithCodeAsync(code, newPassword, userAgent, ip) | Cambia la contraseña con el código de recuperación. |
Session_ChangePasswordAsync(sessionId, newPassword, confirm, userAgent, ip) | Cambia la contraseña de una sesión activa. |
Los modelos de entrada (AccountCreationModel, AccountActivationModel, PasswordRecoveryModel) validan sus campos antes de llamar al servidor.
Almacenamiento clave-valor de la app
MyAppKVClient guarda pares clave-valor propios de tu aplicación: configuración, flags y las sesiones de tus usuarios (el patrón del paso 3). Es el estado compartido entre nodos que hace la app stateless.
await _kvClient.SetKVAsync("clave", "valor"); // devuelve bool
var valor = await _kvClient.GetKVAsync("clave");- El valor es texto plano, hasta 10 MB por clave.
- El espacio de claves es único por aplicación, no por empresa. Si un valor pertenece a un tenant o a una sesión, prefíjalo:
session:{id},config:{empresa}. GetKVAsyncdevuelve cadena vacía si la clave no existe.
Registrado como health check (AddCheck<MyAppKVClient>), el endpoint /HealthCheck verifica la conectividad con Dinaup en cada sonda.
Ejecutar como un usuario concreto
En procesos donde no hay sesión interactiva (importaciones, tareas programadas), envuelve la operación con DinaupContext.WithUser para atribuirla a un usuario: afecta al autor del alta, al histórico y a las anotaciones.
using (DinaupContext.WithUser(userId, ip, userAgent)) // ip y userAgent opcionales
{
await client.RunWriteOperationAsync(sectionId, op, false);
}Para el detalle de informes, filtros y escritura, ver el Cliente Dinaup.
Ejemplo: agregar un recambio en Dinaup (.NET)
Cómo agregar un nuevo recambio a la base de datos de Dinaup usando el SDK para .NET y la clase WriteOperation.
Listados de alto rendimiento
Vuelca un informe entero sin OFFSET con LoadAllRowsAsync: paginación por keyset, coste constante por página y sin filas repetidas.