La creazione di API con ASP.NET Core 9 può sembrare complessa, ma il framework offre diversi approcci per gestire le richieste HTTP in modo semplice ed efficace. Uno degli approcci principali è l'uso delle "minimal APIs", che permettono di creare rapidamente delle API con un numero minimo di righe di codice. Tuttavia, quando la complessità del progetto cresce, l'uso delle API basate su controller diventa una scelta ideale per mantenere la struttura del codice pulita e facilmente manutenibile. In questa sezione, esploreremo come implementare e gestire una semplice API per la gestione dei prodotti, sia con l'approccio minimal che con quello basato su controller, evidenziando i vantaggi e le differenze di ciascun metodo.
Iniziamo con un esempio di una minimal API per la gestione di un registro di prodotti. Il codice di base per una minimal API è incredibilmente semplice: si definisce una lista di prodotti in memoria, e si utilizzano i metodi MapGet, MapPost, MapPut e MapDelete per gestire le richieste HTTP. Ad esempio, per eliminare un prodotto, si utilizza il seguente codice:
Questo frammento di codice definisce una route che gestisce la richiesta HTTP DELETE per eliminare un prodotto dalla lista in memoria. Se il prodotto non viene trovato, viene restituito un errore 404 (Not Found), altrimenti il prodotto viene rimosso e viene restituito un codice di stato 204 (No Content).
Un altro aspetto importante è l'uso della classe Results, che incapsula le funzionalità di restituzione della risposta. Results definisce i codici di stato delle risposte HTTP, semplificando il processo di gestione delle risposte.
Nel caso di una richiesta POST per aggiungere un nuovo prodotto, il codice si compone come segue:
Questa API accetta un oggetto Product in formato JSON, lo mappa automaticamente e lo aggiunge alla lista di prodotti. Il risultato dell'operazione è un oggetto Product restituito con il codice di stato 200 (OK). La bellezza di ASP.NET Core 9 sta nel fatto che grazie al concetto di "binding", il framework si occupa di mappare automaticamente l'oggetto JSON inviato dal client sull'oggetto Product in memoria, semplificando enormemente la gestione delle richieste.
Per testare questa API, è possibile utilizzare strumenti come Postman per inviare una richiesta POST. Basta configurare il corpo della richiesta come segue:
Questa richiesta JSON rappresenta un prodotto con un nome e un prezzo, e quando viene inviata, il prodotto viene registrato nella lista di prodotti in memoria. Una volta inviata la richiesta, il sistema risponde con lo stesso oggetto prodotto, serializzato in formato JSON, insieme al codice di stato HTTP 200.
Tuttavia, quando il progetto cresce e si rendono necessarie funzionalità più complesse, l'approccio basato sui controller diventa inevitabile. L'approccio basato sui controller segue il pattern Model-View-Controller (MVC) e offre una struttura più robusta e organizzata. In questo caso, ogni risorsa (come i prodotti) viene gestita da un controller dedicato. Creando un progetto web API basato su controller, si ottiene una struttura simile a quella di un progetto MVC, ma senza la parte di visualizzazione.
Per creare un progetto basato su controller, basta eseguire il comando:
Questo comando genera un progetto che utilizza l'approccio controller, con una struttura chiara e ben organizzata. La configurazione principale si trova nel file Program.cs, dove vengono aggiunti i servizi necessari, come AddControllers per registrare i controller e AddSwaggerGen per la generazione automatica della documentazione dell'API tramite Swagger.
Il codice di configurazione di base in Program.cs appare come segue:
La configurazione delle rotte per i controller avviene tramite il metodo MapControllers(), che automaticamente mappa le azioni dei controller alle rispettive rotte. Ogni controller è responsabile per la gestione di una risorsa specifica, come nel caso del controller ProductController, che gestisce tutte le operazioni CRUD (Create, Read, Update, Delete) sui prodotti.
Ecco come potrebbe essere il controller per gestire i prodotti:
Questo controller gestisce le richieste per ottenere tutti i prodotti, ottenere un prodotto specifico tramite ID, aggiungere un nuovo prodotto e eliminare un prodotto. Ogni azione è decorata con gli attributi appropriati, come [HttpGet], [HttpPost], e [HttpDelete], che associano ciascun metodo HTTP alla corrispondente azione del controller.
L'approccio basato su controller è particolarmente utile quando si lavora su progetti di grandi dimensioni, poiché consente una gestione più pulita e modulare delle API. Ogni controller può essere dedicato a una specifica risorsa, e la configurazione delle rotte e dei servizi è centralizzata e facilmente modificabile.
Importante è comprendere che, sebbene l'approccio delle minimal APIs sia adatto per progetti più piccoli e con meno complessità, l'approccio controller-based fornisce una maggiore flessibilità e modularità quando il numero di rotte e la logica dell'applicazione aumentano. La scelta tra questi due
Come funziona il binding e la validazione nelle API con ASP.NET Core?
Il binding nelle richieste HTTP rappresenta un aspetto fondamentale per la costruzione di API robuste e flessibili in ASP.NET Core. Una richiesta può includere molteplici componenti: il corpo (body), l’URL, i parametri nella query string e quelli inviati tramite form. In ASP.NET Core, queste informazioni sono astratte nell’oggetto HttpRequest, accessibile tramite la proprietà Request all’interno di ControllerBase, semplificando l’interazione con i dati della richiesta.
Per esempio, ottenere un valore dalla query string può avvenire direttamente tramite Request.QueryString o Request, ma è molto più elegante e sicuro utilizzare l’attributo [FromQuery] per associare automaticamente il parametro del metodo al valore della query string. Questo permette una gestione pulita e tipizzata dei dati in ingresso, rispettando i contratti stabiliti dall’API. È possibile anche rinominare il parametro legato, qualora il nome della query string fosse diverso da quello del parametro nel metodo.
ASP.NET Core offre una serie di attributi per il binding dei dati a seconda della natura della richiesta: [FromBody] per i dati JSON o XML nel corpo della richiesta, utilizzabile una sola volta per metodo; [FromForm] per i dati provenienti da form HTML; [FromServices] per l’iniezione di servizi direttamente nei metodi d’azione; [FromHeader] per leggere valori dagli header HTTP, utile per token o versionamento API; [FromRoute] per parametri incorporati nell’URL. Questa flessibilità consente di adattare il binding alle necessità specifiche di ogni endpoint, migliorando la chiarezza e la manutenzione del codice.
Quando le esigenze dell’applicazione superano le capacità del binding standard, è possibile implementare binding personalizzati. Questi permettono di gestire casi complessi, trasformando dati in strutture più articolate prima che arrivino all’azione, anche se richiedono una conoscenza più approfondita e vanno oltre gli esempi di base.
Il binding rappresenta solo una parte della corretta gestione delle richieste; fondamentale è anche la validazione dei dati. ASP.NET Core introduce il concetto di ModelState, un sistema che verifica che i dati in ingresso rispettino le regole definite nei modelli. ModelState agisce come un filtro che blocca dati non validi prima che influenzino la logica dell’applicazione.
Ad esempio, in un metodo che registra un prodotto, si verifica se l’oggetto prodotto è nullo o se ModelState indica errori di validazione. In questi casi, si ritorna una risposta HTTP 400 con i dettagli degli errori. ModelState organizza gli errori in un dizionario che, convertito in JSON, mostra per ogni proprietà gli errori riscontrati.
Per abilitare la validazione automatica, si usano attributi come [Required] e [MinLength] sui modelli, specificando regole e messaggi personalizzati. È possibile combinare più attributi su una stessa proprietà per definire vincoli più stringenti. Inoltre, la validazione può essere estesa manualmente all’interno delle azioni, aggiungendo errori al ModelState se si riscontrano condizioni particolari, come un prezzo negativo.
Oltre a questi, ASP.NET Core mette a disposizione molte altre annotazioni di validazione pronte all’uso, consentendo una gestione esaustiva e modulare delle regole di business. Questa infrastruttura rende le API più affidabili, riducendo la possibilità di dati inconsistenti o errati.
Il processo di binding e validazione è alla base della qualità e della robustezza delle API. Senza un controllo rigoroso e chiaro dei dati in ingresso, si rischiano errori difficili da diagnosticare o problemi di sicurezza. Inoltre, l’uso combinato di attributi di binding e di validazione permette di mantenere un codice pulito, leggibile e facilmente manutenibile.
La gestione delle validazioni non si esaurisce con ModelState: è consigliabile integrare la documentazione delle API per chiarire ai consumatori quali dati sono richiesti e quali vincoli sono applicati. Strumenti di documentazione e formattazione delle risposte, insieme a una gestione centralizzata degli errori, contribuiscono a rendere le API più trasparenti e facili da usare.
Infine, è importante comprendere che il binding e la validazione rappresentano due facce della stessa medaglia: il primo permette di raccogliere e trasformare i dati da varie fonti in parametri fortemente tipizzati, mentre la seconda garantisce che tali dati rispettino le regole necessarie per il corretto funzionamento dell’applicazione. Questo approccio combinato è essenziale per sviluppare API affidabili, scalabili e sicure.
Qual è la differenza tra autenticazione e autorizzazione nei sistemi moderni di gestione delle identità?
Nei sistemi moderni, la gestione delle identità è fondamentale per garantire la sicurezza e la protezione delle informazioni. Come discusso nei capitoli precedenti, le applicazioni web possono effettuare richieste verso diverse API, che consentono alle aziende di fornire i propri servizi, creando così un ampio ventaglio di integrazioni tra applicazioni diverse. Grazie a queste integrazioni, possiamo disporre di applicazioni che offrono funzionalità varie, come le mappe, i gateway di pagamento e perfino le API che forniscono capacità legate all’intelligenza artificiale. Per garantire che le applicazioni e le API comunichino in modo sicuro, è necessario implementare un meccanismo di sicurezza basato sull'identità, che ci consenta di sapere chi sta richiedendo determinate informazioni e perché. Questo meccanismo di sicurezza si articola in due concetti fondamentali: autenticazione e autorizzazione.
In generale, siamo abituati a pensare che il flusso di autenticazione e autorizzazione ruoti attorno al processo di login, ma è essenziale comprendere la differenza tra i due.
Autenticazione
L’autenticazione si propone di rispondere alla domanda: “Chi sei tu?”. Quando un utente si autentica in un’applicazione, fornisce delle credenziali, come email e password. Una volta inviate queste informazioni al server tramite il form di login, l’applicazione inizia a identificare l'utente attraverso le credenziali fornite. Se l’utente viene trovato nel database con quelle credenziali, l’applicazione riconosce chi sta cercando di accedere al sistema. Ma questa è solo una parte del processo. Ora che l’applicazione sa chi è l’utente, è necessario comprendere cosa questo utente può fare, ed è qui che entra in gioco il processo di autorizzazione.
Autorizzazione
L’autorizzazione, invece, ha come obiettivo rispondere alla domanda: “Cosa può fare questo utente?”. Dopo aver identificato l’utente, l’applicazione inizia a determinare i permessi dell’utente, come mostrato nel flusso di autorizzazione. L’autorizzazione definisce l’ambito in cui l’utente può operare: se può gestire informazioni, se può accedere a determinati dati e via dicendo. Il flusso di autorizzazione di solito identifica l’utente tramite ruoli, che raggruppano diversi livelli di accesso all’interno dell’applicazione. È molto comune l’uso dei ruoli, poiché consentono di definire facilmente i permessi che un utente può avere in una data applicazione.
In generale, i processi di autenticazione e autorizzazione sono concetti piuttosto semplici. Tuttavia, per implementarli in modo sicuro, bisogna considerare alcuni standard di riferimento: i protocolli OAuth 2.0 e OpenID Connect (OIDC).
OAuth 2.0 e OIDC: Comprensione dei protocolli
Immagina un edificio ad alta sicurezza: l’autorizzazione può essere vista come l’ottenimento del permesso di entrare, mentre l’autenticazione verifica la tua identità. OAuth 2.0 si concentra sull’autorizzazione, permettendo agli utenti di concedere l’accesso ai propri dati su una piattaforma (come un account social) a un’altra, senza dover condividere le proprie credenziali. In altre parole, permette ad altre applicazioni di accedere a determinate informazioni senza dover fornire la password, come accade quando ci si logga in una piattaforma utilizzando le credenziali di Microsoft, Google, Facebook e simili.
Il flusso base di OAuth 2.0 si può definire come segue:
-
L’utente effettua il login in una nuova applicazione usando il proprio account di social media.
-
L’applicazione reindirizza l’utente alla piattaforma di social media (il server di autorizzazione).
-
Dopo aver effettuato l’accesso all’account social, l’utente concede il permesso all’applicazione di utilizzare i propri dati (come nome, email, foto del profilo, ecc.).
-
Il server di autorizzazione genera dei token speciali per l’applicazione. I token sono utilizzati per ottenere l’accesso ai dati dell’utente (access token). In alcuni casi, vengono utilizzati anche i refresh token per fornire nuovi token.
-
L’applicazione usa l’access token per recuperare i dati dell’utente dalla piattaforma social in modo sicuro.
Questo processo consente a due applicazioni di condividere informazioni sull'utente senza che questi debba inserire le credenziali per ogni nuova applicazione, aumentando così sia la sicurezza che la convenienza.
D’altro canto, OpenID Connect (OIDC) si basa su OAuth 2.0, aggiungendo un ulteriore strato di autenticazione. Utilizza il framework di autorizzazione di OAuth per verificare l’identità dell’utente tramite fornitori di identità di fiducia, come Google o Facebook. Vediamo come OIDC completa OAuth 2.0:
-
Alcune applicazioni offrono la possibilità di accedere utilizzando altri account social. In questo caso, durante il flusso di OAuth 2.0, invece di effettuare il login nell’applicazione, l’utente viene reindirizzato alla pagina di login del social media (il fornitore di OpenID).
-
L’utente si autentica con le credenziali del social media, confermando la propria identità al fornitore di OpenID.
-
Con il consenso dell’utente, il fornitore di OpenID condivide le informazioni di base del profilo (come nome ed email) con la nuova applicazione tramite un ID token.
OIDC consente funzionalità come il single sign-on (SSO), che permette di accedere a più applicazioni utilizzando le stesse credenziali di login (pensiamo ad esempio a quando ci si logga su diversi siti con l’account Google).
Pur essendo simili e interconnessi, OAuth 2.0 e OIDC servono scopi differenti:
-
Focus: OAuth 2.0 riguarda l’autorizzazione (accesso ai dati), mentre OIDC si occupa dell’autenticazione (verifica dell’identità dell’utente).
-
Condivisione delle informazioni: OAuth 2.0 gestisce principalmente gli access token, mentre OIDC introduce gli ID token contenenti le informazioni del profilo utente.
Possiamo pensare a OAuth 2.0 come una chiave che apre la porta di una casa, mentre OIDC fornisce la verifica dell’identità per permettere di ricevere questa chiave. Questo flusso è molto comune nelle applicazioni che usiamo quotidianamente.
Nonostante la semplicità del flusso di OAuth 2.0 e OIDC, l’implementazione di questi meccanismi non è banale. Essa dipende da importanti accorgimenti per garantire che le funzionalità siano eseguite correttamente. A tal proposito, ASP.NET Core 9 offre astrazioni che supportano lo sviluppo della gestione delle identità rispettando gli standard descritti finora. L'astrazione che implementa queste risorse è nota come ASP.NET Core Identity. Questo sistema è in continuo miglioramento, integrando le migliori pratiche di sicurezza per i flussi di autorizzazione e autenticazione, permettendo al contempo l'integrazione con altri fornitori di identità e offrendo la possibilità di personalizzazioni.
Endtext
Come migliorare le prestazioni delle applicazioni con strategie di caching e resilienza
La compressione, il caching delle informazioni e il modello di programmazione asincrona sono elementi imprescindibili nello sviluppo di applicazioni moderne. Essi garantiscono un’esperienza utente fluida e consentono ai sistemi integrati di funzionare senza intoppi anche sotto carichi elevati. Tra queste tecniche, il caching si distingue come una delle più potenti per ottimizzare le prestazioni, riducendo il tempo di accesso ai dati più frequentemente richiesti.
Il caching consiste nell’immagazzinare temporaneamente i dati in una posizione facilmente accessibile, evitando così di dover ripetere operazioni costose o accedere ripetutamente alla fonte originaria. Un approccio comune in ambito ASP.NET Core è l’uso della cache in-memory, che mantiene i dati nella memoria del server, risultando molto veloce ma adatto principalmente a dataset di dimensioni medio-piccole e ambienti con un solo nodo di esecuzione.
Per applicazioni più robuste e distribuite, è necessario adottare strategie di caching distribuito. Redis, acronimo di Remote DIctionary Server, è una soluzione open source di riferimento in questo ambito. Redis è un archivio in-memory ad alte prestazioni, capace di gestire strutture dati complesse (stringhe, hash, liste, set ordinati e altri) e dotato di un modello di persistenza su disco che assicura la durabilità dei dati. La sua integrazione con ASP.NET Core 9 è ormai consolidata e permette di realizzare sistemi resilienti e scalabili.
L’implementazione di Redis come cache distribuita in un progetto ASP.NET Core si basa su alcune configurazioni chiave: la stringa di connessione al server Redis e l’eventuale prefisso per le chiavi di cache. Grazie alla libreria Microsoft.Extensions.Caching.StackExchangeRedis, è possibile aggiungere rapidamente al container di Dependency Injection i servizi necessari per gestire la cache in modo trasparente e efficiente.
Dal punto di vista pratico, un controller API dedicato può gestire le operazioni di memorizzazione e recupero dati dalla cache Redis. Utilizzando i metodi asincroni GetStringAsync e SetStringAsync, si possono serializzare oggetti complessi in JSON e conservarli con opzioni di scadenza configurabili, quali l’espirazione assoluta e quella scorrevole. Tali meccanismi assicurano che i dati in cache rimangano aggiornati e si liberino automaticamente dopo un periodo stabilito, mantenendo il sistema reattivo e ottimizzato.
Il concetto di resilienza in questo contesto è strettamente connesso alla capacità dell’applicazione di continuare a funzionare efficacemente anche in presenza di problemi temporanei nei servizi esterni, come Redis. L’adozione di strategie di caching distribuito contribuisce a questa resilienza, perché riduce la dipendenza da risorse esterne e minimizza i tempi di risposta, mantenendo l’esperienza utente coerente.
È fondamentale comprendere che il caching non è una panacea: deve essere utilizzato con cognizione di causa, valutando attentamente quali dati possono essere memorizzati e per quanto tempo. Inoltre, la gestione della coerenza tra cache e sorgenti dati è un aspetto cruciale per evitare la restituzione di informazioni obsolete o errate.
Oltre alla pura implementazione tecnica, occorre anche considerare l’integrazione del caching all’interno di un’architettura più ampia di sistemi distribuiti, tenendo conto della scalabilità, della disponibilità e della tolleranza ai guasti. L’uso di Redis permette di affrontare molte di queste sfide grazie alla sua flessibilità e alle funzionalità avanzate.
Infine, un’attenzione particolare va riservata al monitoraggio delle prestazioni e all’analisi dei pattern di accesso ai dati: solo così è possibile ottimizzare costantemente la strategia di caching, migliorando l’efficienza generale dell’applicazione senza compromettere la qualità del servizio offerto agli utenti.
Come garantire scalabilità, coerenza e gestione efficiente dei log nelle applicazioni cloud-native
Quando si parla di scalabilità in ambienti containerizzati, è fondamentale comprendere il processo che avviene sia nella fase di incremento (scale-out) sia in quella di decremento (scale-in) delle istanze. Al momento dello scale-out, viene creata una nuova istanza del container, la quale si avvia rapidamente e diventa operativa in tempi minimi per gestire le richieste. Solo quando l’istanza è pronta, il bilanciatore di carico comincia a indirizzare su di essa le richieste, garantendo così un assorbimento graduale e senza interruzioni del traffico. Al contrario, durante lo scale-in o l’aggiornamento della versione dell’applicazione, il bilanciatore di carico smette di inviare nuove richieste alla vecchia istanza, la quale completa con cura tutte le richieste ancora in corso prima di chiudere definitivamente, evitando così qualsiasi interruzione brusca del servizio. Questo meccanismo di spegnimento graduale (graceful shutdown) è essenziale per mantenere l’affidabilità e la robustezza dell’applicazione, come dimostra l’implementazione possibile in ASP.NET Core 9 attraverso la gestione dei token di cancellazione, che consente di sincronizzare la chiusura dell’applicazione con il completamento delle richieste attive.
Altro aspetto cruciale riguarda la parità tra ambienti di sviluppo e produzione, noto come dev/prod parity. La celebre affermazione “Funziona sulla mia macchina” mette in luce una realtà ben nota: il comportamento delle applicazioni può variare significativamente tra diversi ambienti di esecuzione a causa di variabili quali risorse hardware, configurazioni di sistema o permessi di accesso. Per minimizzare queste discrepanze, è imprescindibile garantire la massima uniformità tra i diversi ambienti, ottenuta tramite l’uso di infrastrutture come codice (Infrastructure as Code) e strumenti come Terraform o Bicep. Questo approccio non solo agevola l’agilità nello sviluppo e nell’operatività, ma rafforza anche la governance, la sicurezza e la compliance dell’intero ecosistema.
Il trattamento dei log rappresenta un ulteriore elemento cardine nell’architettura cloud-native. Tradizionalmente, i log venivano considerati semplici registri testuali consultati solo in caso di problemi, ma in un contesto dinamico e distribuito come quello del cloud, tale pratica si rivela inadeguata. I log devono essere intesi come flussi di eventi, raccolti e analizzati in tempo reale da sistemi dedicati al monitoraggio, come Elasticsearch, Azure Monitor o soluzioni open source quali Prometheus e Grafana. Questi flussi consentono di ottenere una visione dettagliata e aggiornata del funzionamento delle applicazioni, facilitando l’analisi delle performance, il rilevamento tempestivo di anomalie e la pianificazione di strategie di scaling automatico.
La gestione della telemetria e dei log, tuttavia, introduce una dipendenza da specifici SDK e strumenti di raccolta, che può complicare il cambio di tecnologie o la migrazione verso altri sistemi di monitoraggio. Per mitigare questo vincolo, soluzioni come OpenTelemetry offrono un approccio vendor-agnostic, permettendo una raccolta dei dati indipendente dal fornitore e la distribuzione simultanea su più piattaforme di analisi. Questa strategia di isolamento dei meccanismi di logging riduce il coupling con l’applicazione, favorendo flessibilità e manutenzione a lungo termine.
L’importanza dei log si estende anche ai modelli architetturali basati su eventi e microservizi, dove la tracciabilità dell’intero flusso di esecuzione risulta indispensabile per audit, ottimizzazioni e risoluzione dei bug. In queste architetture, l’isolamento delle responsabilità, la decoupling e la separazione delle preoccupazioni diventano fattori fondamentali per garantire sicurezza, scalabilità e manutenzione efficace.
Infine, anche i processi amministrativi, come le migrazioni di database, si inseriscono nel contesto delle applicazioni cloud-native. Nonostante la complessità, è importante che tali operazioni vengano eseguite in modo indipendente e coordinato, mantenendo il più possibile l’isolamento e la modularità dell’applicazione.
Per comprendere a fondo questi aspetti, è necessario riconoscere che la resilienza e la robustezza delle applicazioni cloud-native non derivano solo da singole tecnologie o pattern, ma dall’insieme coerente di pratiche, strumenti e principi che permettono di affrontare le sfide della complessità distribuita, della variabilità degli ambienti e della continua evoluzione delle infrastrutture. La gestione attenta della scalabilità, della parità degli ambienti, del logging e dei processi amministrativi contribuisce a costruire sistemi affidabili, adattabili e facilmente manutenibili, elementi imprescindibili in un mondo dominato dall’adozione massiva del cloud e dei container.
Come l'esercizio fisico stimola il corpo e la consapevolezza del movimento
Perché il Big Bang è il punto di partenza per comprendere l'Universo?
Cosa Rende Un Film o Una Performance Unica? Un'Analisi del Successo Nella Cultura Contemporanea

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский