Uno dei requisiti più comuni nell'ambito delle applicazioni enterprise è quello di mantenere un audit delle chiamate effettuate dagli utenti, così che si possa eventualmente risalire a chi ha effettuato una determinata operazione, o monitorare accessi illeciti.
Si tratta di un problema in apparenza banale, basterebbe infatti creare un filter o un middleware che memorizzi gli estremi della richiesta su un database. Tuttavia, in produzione, effettuare una chiamata in scrittura su un database a ogni richiesta potrebbe sia causare tempi d'attesa non trascurabili, e addirittura causare dei problemi di scalabilità sul database se il traffico dovesse essere elevato.
Una possibile soluzione, allora, è quella di accodare queste istanze di log in memoria e poi procedere alla loro memorizzazione in batch, dopo che ne abbiamo accumulato un certo numero. Anche questo sembra un task in apparenza banale, ma che diventa tutt'altro che scontato in un contesto multi-thread come quello delle web application. Per fortuna c'è una serie di classi all'interno della Task Parallel Library che vengono in nostro aiuto: DataFlow.
Il primo passo è quello di creare un oggetto AuditTrace, che modellerà l'insieme di informazioni che vogliamo tracciare. Per esempio, possiamo usare qualcosa di simile al codice in basso:
public class AuditTrace { public string Url { get; set; } public string HttpVerb { get; set; } public ClaimsPrincipal Principal { get; set; } public DateTime TimeStamp { get; set; } }
Poi dobbiamo costruire la coda che useremo per accumulare un batch di trace da memorizzare, tramite la classe AuditQueue:
internal class AuditQueue { private BatchBlock<AuditTrace> _queue; public AuditQueue(IAuditRepository auditRepository) { var batchSize = 50; _queue = new BatchBlock<AuditTrace>(batchSize); var actionBlock = new ActionBlock<AuditTrace[]>(async traces => { await auditRepository.SaveTracesAsync(traces); }); _queue.LinkTo(actionBlock); } public Task SendAsync(AuditTrace trace) { return _queue.SendAsync(trace); } }
Questo codice incapsula interamente la nostra logica di spooling tramite una semplice pipeline costituita da due oggetti della DataFlow library: BatchBlock e ActionBlock.
BatchBlock
A questo punto, BatchBlock invierà l'intero batch di trace al secondo elemento della pipeline, ossia ActionBlock, che eseguirà l'azione che abbiamo specificato, ossia memorizzarli tramite una classe AuditRepository.
Le due classi sono thread safe, e pertanto sono adatte a essere utilizzato in un contesto web; inoltre il fatto di effettuare una chiamata a database ogni 50 richieste è molto più efficiente di 50 chiamate individuali, magari in parallelo, e farà sì che il carico su quest'ultimo sia assolutamente trascurabile.
L'ultimo passaggio è quello di registrare AuditQueue nell'IoC container, insieme al middleware che lo invochi a ogni richiesta. Come immaginiamo, possiamo farlo nella classe Startup:
public void ConfigureServices(IServiceCollection services) { // ..altro codice qui.. services.AddSingleton<AuditQueue>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ..altro codice qui.. app.Use(async (ctx, next) => { var trace = new AuditTrace() { Url = ctx.Request.GetDisplayUrl() ... }; var queue = ctx.RequestServices.GetService<AuditQueue>(); await queue.SendAsync(trace); await next(); }); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
Come possiamo notare, abbiamo registrato AuditQueue come singleton, perchè vogliamo che tutte le richieste accedano alla medesima coda. Il middleware, poi, è di per sé piuttosto semplice, e si limita a recuperare l'istanza di AuditQueue e inviare il trace della richiesta corrente.
Resta solo un nodo da sciogliere: come gestiamo il caso dello shutdown dell'applicazione, per assicurare di non perdere trace parzialmente accumulati? sarà l'argomento del prossimo script.
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Generare velocemente pagine CRUD in Blazor con QuickGrid
Modificare i metadati nell'head dell'HTML di una Blazor Web App
Effettuare il binding di date in Blazor
Eseguire query per recuperare il padre di un record che sfrutta il tipo HierarchyID in Entity Framework
Utilizzare database e servizi con gli add-on di Container App
Utilizzare Tailwind CSS all'interno di React: installazione
Popolare una classe a partire dal testo, con Semantic Kernel e ASP.NET Core Web API
Sfruttare i KeyedService in un'applicazione Blazor in .NET 8
Usare le collection expression per inizializzare una lista di oggetti in C#
Come EF 8 ha ottimizzato le query che usano il metodo Contains
Esporre i propri servizi applicativi con Semantic Kernel e ASP.NET Web API
Eseguire script pre e post esecuzione di un workflow di GitHub