Ciao a tutti, in questo articolo voglio parlarvi di caso d’uso reale, lo sviluppo che mi ha visto coinvolto sia come solution architect che sviluppatore: gestire una UI dinamica all’interno di un’applicazione enterprise, con campi configurabili dall’utente legati alle varie entità applicative.
Contesto applicativo
Prima di approfondire come ho strutturato il motore che gestiva le proprietà custom è necessario capire quali sono le entità che ruotavano attorno a questo prodotto.
L’applicazione è nata nel periodo COVID, per permettere di gestire video appuntamenti digitali in modo più complesso rispetto ad un normale strumento di videochiamata come Teams, Zoom o altro. Attorno all’appuntamento troviamo l’anagrafica, i documenti (che potevano essere firmati digitalmente in un contratto), i contratti e dei workflow (una serie di azioni configurabili da fare per sbloccare alcuni passaggi).
Questi sono alcuni casi d’uso di utilizzo dell’applicazione
- HR: censimento delle informazioni di un candidato (anagrafica), pianificazione di uno o più colloqui (appuntamento), raccolta documentale e firma digitale del contratto (documento).
- Finance: Ingaggio di un potenziale cliente (anagrafica), pianificazione di uno o più incontri virtuali (appuntamento), raccolta documentale e firma digitale di un contratto.
- Medical: Un paziente (anagrafica) contattava un’azienda per ricevere assistenza su un dispositivo medicale. L’azienda pianificava un appuntamento (anche immediato) per fornire assistenza e risolvere il problema.
Alcune delle entità che abbiamo nominato necessitano di una struttura che permetta loro di adattarsi ai vari scenari di utilizzo. I campi che definiscono le entità anagrafica, appuntamento e contratto necessitano di essere definiti per ogni installazione.
Dovevo quindi pensare ad un sistema dinamico per permettere di definire i campi di un’entità, mettere mano al codice per ogni contesto era ovviamente un’opzione da non prendere neanche in considerazione.
Obiettivi
- Gestione dinamica delle proprietà delle entità (gestibili anche dal cliente stesso tramite sezione di amministrazione)
- Gestione resiliente dei vari casi d’uso, comprese le richieste custom dei vari clienti
- Rispettare i principi SOLID per permettere di far evolvere il software in modo agile
Architettura dell’applicazione
L’applicazione era divisa tra backend (in .NET Core) e Frontend (Angular).
Il backend era strutturato con un monolite modulare centrale e una serie di microservizi attorno a esso, servizi che già venivano utilizzato in altri contesti aziendali. I moduli applicativi erano costituiti dal modulo dei contratti e dal modulo degli appuntamenti.

Il rilascio era ovviamente automatizzato e seguiva questo schema:

Analisi dei requisiti
Iniziamo a scendere più nel dettaglio rispetto al progetto. Ho sviluppato questo software per diversi anni, nel corso del tempo si sono aggiunti moduli e nuove funzionalità, quello che descriverò qui sarà il riassunto di quello che sono arrivato a sviluppare.
La prima fase è sicuramente stata un’analisi rispetto a cosa mi sarebbe servito per gestire un campo dinamico, individuando le caratteristiche che lo descrivono. Un elenco potrebbe essere il seguente:
- Tipologia del campo (stringa, data, numerico, email, telefono,..)
- Label
- Ordine di rappresentazione
- Gruppo (property con lo stesso nome gruppo venivano raggruppate lato UI)
- Readonly (utile per gestire dei campi calcolati)
- Entità di appartenenza (anagrafica, contratto, appuntamento,..)
- Obbligatorietà
- Visibilità in lista (con questo flag potevo gestire la creazione delle liste in modo dinamico, scegliendo quali campi mostrare. Un campo visibile in lista era automaticamente anche filtrabile)
- Univocità(vincolo software, veniva controllato che il valore univoco fosse unico in tutta la tabella, es. codice fiscale nelle anagrafiche)
- Creazione veloce (es: quando si aggiungeva un nuovo cliente si apriva una modal con i campi obbligatori e con i campi di creazione veloce, in modo tale da inserire solo i dati principali e rimandare in un secondo momento la profilazione più completa del cliente
Oltre a tutto questo c’era anche un concetto di visibilità e gestione dei campi in funzione del ruolo dell’utente. Non mi soffermerò molto su questo punto, ma concettualmente avevo previsto che ad un ruolo corrispondeva un permesso di lettura/scrittura/modifica/eliminazione per ogni entità, questi permessi venivano poi ereditati dalla maschera di visualizzazione in modo tale da permettere o meno le operazioni sui dati. Ovviamente esisteva anche un controllo lato backend per gestire la sicurezza anche in questo livello.
Architettura
Database
Lato database avevo previsto una tabella di configurazione delle entità, in questa tabella venivano tracciati tutti i campi dinamici con tutte le loro caratteristiche.
Il sistema l’avevo pensato per essere dinamico e “adattabile” a qualsiasi delle entità dell’applicazione, esisteva quindi una tabella “base” per ogni entità che si doveva gestire con questo meccanismo: anagrafiche, appuntamenti, contratti,..
Applicazione – Backend
Nell’applicazione avevo sviluppato una semplice classe per gestire le proprietà custom, ogni volta che da UI veniva aggiunto un nuovo campo si creava una nuova colonna sul database, nella tabella dell’entità selezionata. Con questo approccio potevo agganciare qualsiasi tabella ad uno strumento di reportistica già utilizzato in azienda.
Dall’altra parte, avendo campi dinamici, non potevo di certo definire una classe con una property per ogni campo. Tutte le classi che rappresentavano le entità con proprietà dinamiche ereditavano quindi una classe base, la quale aveva una lista di oggetti che rappresentava le proprietà ed il loro eventuale valore. Con questo approccio avevo un unico modo per rappresentare le proprietà, sia quando avevo una oggetto “vuoto” sia quando l’oggetto rappresentava un elemento salvato su database.
Per evitare di avere problemi di performance, alcune strutture dati erano inserite in cache, in modo tale da essere molto più performante ed evitare accessi inutili al database per leggere sempre gli stessi dati.
Applicazione – Frontend
Al frontend arrivava un oggetto molto simile a quello descritto precedentemente e le proprietà che l’utente non poteva vedere venivano rimosse. Una proprietà conteneva anche tutte le sue informazioni utili per il render del componente. Ogni proprietà veniva passata ad un componente Angular che si occupava di gestire la sua rappresentazione. L’insieme di componenti erano raggruppati all’interno di una reactive form di Angular, la quale si occupava anche di gestire i dati inseriti/aggiornati dall’utente.
Infrastruttura
Purtroppo l’azienda dove ho sviluppato questa applicazione possedeva un’infrastruttura vetusta e poco aggiornata, l’applicazione girava all’interno di IIS ( 😭 ), escludendo la possibilità di usare container e scalare in funzione del carico.
Resilienza: gestire le personalizzazioni dei clienti
Il vero e proprio test di resilienza arriva quando il cliente chiede una personalizzazione ad un processo “standard”. Questa applicazione ha superato questa prova, vediamo come!
Uno dei clienti voleva visualizzare (all’interno di alcuni campi in sola lettura) il risultato di un calcolo specifico. Come fare a gestirlo in un sistema che doveva adattarsi a tante realtà?
Le opzioni sul tavolo erano come le due pillole offerte a Neo in Matrix:

(Pillola blu 🔵) Gestire all’interno del sorgente delle personalizzazioni custom in base a determinati parametri
PRO
- Implementazione più veloce
CONTRO
- Pulizia del codice necessaria ogni volta che si perdeva un cliente
- Complessità a lungo andare
- Rischio di rompere il workflow dinamico per gestire casi specifici
(Pillola rossa 🔴) Gestire le personalizzazioni in moduli separati
PRO
- Tutte le personalizzazioni sono esterne al progetto principale, mantenendo “pulito” il codice applicativo base
- Il modulo è caricato solamente in funzione dell’installazione e del cliente, non c’è il rischio che le personalizzazioni si “mischino”
CONTRO
- Non mi sono ancora venuti in mente 😅
Ovviamente ho scelto la pillola rossa 🔴😉 . Come prima cosa sono andato ad individuare alcuni punti dove poter “inserire” eventuali personalizzazioni, per esempio: prima e dopo del salvataggio di un’entità, durante una validazione, durante un task temporale,..
Il passaggio successivo è stato quello di definire un’interfaccia che esponesse i metodi da estendere per poter aggiungere funzionalità custom. Fatto questo ho creato un nuovo progetto nella solution con all’interno dei “moduli”: in ogni modulo si poteva implementare l’interfaccia generica con le dinamiche specifiche del cliente e si potevano anche gestire dei job automatici che eseguissero automatismi. Tramite questo meccanismo abbiamo gestito la sincronizzazione dei dati tra la nostra applicazione ed il CRM del cliente.
Come veniva attivato tutto questo? Tramite la dependency Injection venivano registrate nel progetto le implementazioni corrette in base all’installazione del progetto.

Conclusioni
La vera domanda è: ma tutto questo ha funzionato? La risposta è ASSOLUTAMENTE SI!
L’applicazione permetteva di gestire in pochi minuti l’aggiunta, l’eliminazione o la modifica di proprietà custom legate alle anagrafiche. Una volta conclusa l’analisi di startup con il cliente era possibile installare in poche ore un’applicazione nuova e già configurata (nei casi in cui era necessario implementare delle logiche custom i tempi si allungavano ovviamente).
I clienti che chiedevano eventuali personalizzazioni venivano gestiti in modo agile, senza toccare il codice centrale ma sviluppando su moduli esterni, iniettati specificatamente per l’installazione dedicata.
I bug&fix e le evolutive erano agilmente rilasciati a tutti tramite il sistema di rilascio automatico, permettendoci di tenere sempre tutte le istanze aggiornate alle ultime versioni di prodotto.
Infine le istanze erano monitorate da ELK, uno stack per monitorare i log applicativi in modo centralizzato (qui un mio articolo sull’argomento).
Grazie per aver letto l’articolo, alla prossima!