# Fuoco e non sparare *Abbastanza* Dimentica - esecuzione effimera senza Stato

<!--category-- ASP.NET, Architecture, CQRS, Systems Design, Async, Privacy-Safe Design -->
<datetime class="hidden">2025-12-12T12:00</datetime>

La maggior parte dei sistemi async o ricordano troppo (log, code, spazzatura persistente che non hai mai voluto)... o non ricordano nulla (buchi neri incendiati e dimenticati che svaniscono nel momento in cui qualcosa va storto).

Questo articolo introduce qualcosa di diverso:

> Uno schema in cui ogni operazione asincrona diventa una piccola **tracciabile**, **ispezionabile**, **ri-recuperabile**, **Unità di attività autopulente** -e l'intero sistema rimane privato, limitato, veloce e deterministico.

E' il compagno del mio precedente articolo, **[Imparare le LRU - Quando l'eccesso di capacità rende il vostro sistema migliore](/blog/learning-lrus-when-capacity-makes-systems-better)**. Che uno ha esplorato come limitata memoria + scadenza scorrevole diventa un meccanismo di sopravvivenza.

Questo esplora l'altra metà: **esecuzione limitata, effimera** -come un piccolo rolling buffer di attività attive diventa un debugger, un log di eventi e un motore di riproduzione, senza mantenere alcun dato utente.

L'esempio viene dal widget di traduzione del mio blog - una piccola UI per la traduzione di contenuti markdown on-the-fly. Sembra banale. Sotto il cofano, implementa i modelli è possibile rubare per qualsiasi flusso di lavoro async.

Questo e' **Parte 1** di una serie in due parti:

- **Parte 1 (questo articolo)**: La teoria, il modello, e un esempio del mondo reale
- **[Parte 2: Costruire una biblioteca di esecuzione effimera riutilizzabile](/blog/ephemeral-execution-library)**: La piena implementazione con l'integrazione DI

## NUGET!!!

[Questo è ora nel pacchetto per lo piùlucid.efemerals Nuget anche più di 20 modelli per lo piùlucid.efemerals e 'atomi'](https://www.nuget.org/packages?q=mostlylucid&includeComputedFrameworks=true&prerel=true&sortby=created-desc).

[![NuGetCity name (optional, probably does not need a translation)](https://img.shields.io/nuget/v/mostlylucid.ephemeral.svg)](https://www.nuget.org/packages/mostlylucid.ephemeral)
[![Licenza](https://img.shields.io/badge/license-Unlicense-blue.svg)](../../UNLICENSE)

[TOC]

---


## Il problema - flussi di lavoro sincronizzati Crea stato nascosto

Gli sviluppatori ASP.NET tendono a scegliere tra tre opzioni sbagliate:

### Fuoco e dimentico

Tu `Task.Run()` Qualcosa, sperare che finisca, e quando esplode non ottieni niente. Nessuna correlazione, nessuna ricostruzione, nessuna idea di cosa abbia fallito.

### Fuoco e attesa

Blocchi i fili che non dovresti bloccare, il flusso muore, la latenza esplode, i tuoi utenti ti odiano.

### Coda/canali ovunque

Ora hai:

- code senza limiti
- stato persistente che non intendevi mantenere
- PII accidentalmente memorizzato da qualche parte
- un sistema distribuito in cui si desiderava solo una semplice operazione

### Tracciamento distribuito

Utile... ma esterno. Inoltre: non può riprodurre nulla e spesso trapela dati che non hai mai pensato di memorizzare.

Cio' che vogliamo in realta' e':

- **di breve durata**
- **privato**
- **debuggabile**
- **ispezionabile**
- **limitato**
- **recuperabile se necessario**
- **e poi se n'è andato**

Una memoria "appena abbastanza lunga" di ciò che il sistema sta facendo - e niente di più.

---


## Il modello - Fuoco e *Non proprio.* Dimentica

Ecco l'idea in una frase:

> **Preleva immediatamente un'operazione asincrona, rintracciala esplicitamente tramite un TaskCompletionSource, ripuliscilo deterministicamente e mantieni un piccolo buffer di rotolamento degli ultimi compiti in modo da poterli ispezionare o recuperarli - senza mantenere alcun contenuto utente.**

Si trova a metà strada tra:

- approvvigionamento di eventi
- tracciatura
- code di lavoro
- futures/promesse
- sessioni effimere

...senza diventare nessuno di loro.

```mermaid
flowchart TB
    subgraph Client["Client Request"]
        R[Start Translation]
    end

    subgraph API["API Layer"]
        A[Create TaskId] --> B[Create TaskCompletionSource]
        B --> C[Queue to Channel]
        C --> D[Return TaskId Immediately]
    end

    subgraph Cache["Ephemeral Cache (Bounded)"]
        E[Store TranslateTask]
        F[Max 5 per user]
        G[6hr absolute / 1hr sliding expiry]
    end

    subgraph Worker["Background Worker"]
        H[Read from Channel]
        H --> I[Execute Translation]
        I --> J[Complete TCS]
    end

    R --> A
    B --> E
    E --> F --> G
    C --> H

    style Cache fill:none,stroke:#10b981,stroke-width:2px
    style Worker fill:none,stroke:#6366f1,stroke-width:2px
```

---


## Anatomia di un compito effimero

La struttura dei dati di base è un `TranslateTask` - una piccola unità di esecuzione atomica sostenuta da un `Task<TaskCompletion>`:

```csharp
// From TranslateTask.cs
public class TranslateTask(
    string taskId,
    DateTime startTime,
    string language,
    Task<TaskCompletion>? task)
    : TranslateResultTask(taskId, startTime, language)
{
    public Task<TaskCompletion>? Task { get; init; } = task;
}

public record TaskCompletion(
    string? TranslatedMarkdown,
    string OriginalMarkdown,
    string Language,
    bool Complete,
    DateTime? EndTime);
```

Contiene:

- Un ID unico (corrispondenti a tutto il flusso)
- Un timestamp (quando è iniziato)
- Metadati (lingua -non contenuto utente!)
- Un riferimento all'attuale `Task<TaskCompletion>`
- Durata (computata all'atto dell'accesso)
- Stato errore (derivato dallo stato attività)
- Risultato finale (solo se completato con successo)

Niente di persistente, niente di scritto su disco, niente di memorizzato al di là di un rolling window.

AWS ha funzioni passo. Azure ha funzioni durabili. Hai... 30 linee di codice che non richiedono una nuvola.

---


## Il modello di risorsa del completamento del task -Bridging Fire-and-Forget to Fire-and-Track

La magia accade nel `BackgroundTranslateService`. Invece di fuoco-e-dimenticare, usiamo un [TaskCuplementFonte](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) -un costrutto simile a una promessa che ci permette di tornare immediatamente mentre il lavoro avviene in background.

```csharp
// From BackgroundTranslateService.cs
private readonly Channel<(PageTranslationModel, TaskCompletionSource<TaskCompletion>)>
    _translations = Channel.CreateUnbounded<(PageTranslationModel, TaskCompletionSource<TaskCompletion>)>();

private async Task<Task<TaskCompletion>> Translate(PageTranslationModel message)
{
    // Create a TaskCompletionSource that will eventually hold the result
    var tcs = new TaskCompletionSource<TaskCompletion>();

    // Send the translation request along with the TCS to be processed
    await _translations.Writer.WriteAsync((message, tcs));

    // Return the Task immediately -caller can await it or check status later
    return tcs.Task;
}
```

### Cos'è un TaskCompletionFonte?

Se non l'hai usato `TaskCompletionSource<T>` prima, pensatelo come un **prometti di controllare manualmente**. A differenza di un regolare `Task` che completa quando il suo lavoro termina, un TCS completa quando *tu* chiamata `SetResult()`, `SetException()`, oppure `SetCanceled()`.

Questo ti permette di:

1. Restituisce a `Task` al chiamante immediatamente
2. Fare il lavoro reale da qualche altra parte (filo diverso, servizio di sfondo, ecc.)
3. Completa l'attività quando *tu* decidere che è fatto

E' il ponte tra "fuoco-e-dimenticato" e "fuoco-e-traccia."

---


## Il livello API -Iniziare una traduzione

Quando un utente fa clic su "Tradurre," il frontend chiama l'API:

```javascript
// From translations.js
fetch('/api/translate/start-translation', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        Language: shortCode,
        OriginalMarkdown: markdown
    })
})
.then(response => response.json())
.then(taskId => {
    // Got a taskId immediately -translation is running in background
    console.log("Task ID:", taskId);

    // Poll for updates via HTMX
    htmx.ajax('get', "/editor/get-translations", {
        target: '#translations',
        swap: 'innerHTML'
    });
});
```

L'API restituisce immediatamente con solo un ID attività:

```csharp
// From TranslateAPI.cs
[HttpPost("start-translation")]
public async Task<Results<Ok<string>, BadRequest<string>>> StartTranslation(
    [FromBody] MarkdownTranslationModel model)
{
    if (!backgroundTranslateService.TranslationServiceUp)
        return TypedResults.BadRequest("Translation service is down");

    // Create a unique identifier for this translation task
    var taskId = Guid.NewGuid().ToString("N");
    var userId = Request.GetUserId(Response);

    // Trigger translation -returns Task<TaskCompletion> immediately
    var translationTask = await backgroundTranslateService.Translate(model);

    // Wrap it in our trackable TranslateTask
    var translateTask = new TranslateTask(taskId, DateTime.Now, model.Language, translationTask);

    // Store in the ephemeral cache (bounded, self-cleaning)
    translateCacheService.AddTask(userId, translateTask);

    // Return the task ID to the client -they can poll for status
    return TypedResults.Ok(taskId);
}
```

La risposta è immediata. La traduzione effettiva viene eseguita in background. L'utente può interrogare o semplicemente guardare l'aggiornamento dell'interfaccia utente.

---


## La finestra di funzionamento del rotolamento - un buffer autopulente

L'intuizione chiave dal [Articolo LRU](/blog/learning-lrus-when-capacity-makes-systems-better) si applica anche in questo caso:

> **Mantieni abbastanza storia da debug e ragionare sul sistema... e lasciare che il decadimento naturale cancelli tutto il resto.**

```csharp
// From TranslateCacheService.cs
public class TranslateCacheService(IMemoryCache memoryCache)
{
    public void AddTask(string userId, TranslateTask task)
    {
        if (memoryCache.TryGetValue(userId, out CachedTasks? tasks))
        {
            var currentTasks = tasks?.Tasks ?? new List<TranslateTask>();
            currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();

            // Keep only the 5 most recent tasks -bounded window
            if (currentTasks.Count >= 5)
            {
                var lastTask = currentTasks.Last();
                currentTasks.Remove(lastTask);
            }

            currentTasks.Add(task);
            currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();
            tasks!.Tasks = currentTasks;

            memoryCache.Set(userId, tasks, new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = tasks.AbsoluteExpiration,
                SlidingExpiration = TimeSpan.FromHours(1)
            });
        }
        else
        {
            // First task for this user
            var cachedTasks = new CachedTasks
            {
                Tasks = new List<TranslateTask> { task },
                AbsoluteExpiration = DateTime.Now.AddHours(6)
            };
            memoryCache.Set(userId, cachedTasks, new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = cachedTasks.AbsoluteExpiration,
                SlidingExpiration = TimeSpan.FromHours(1)
            });
        }
    }

    public List<TranslateTask> GetTasks(string userId)
    {
        if (memoryCache.TryGetValue(userId, out CachedTasks? tasks))
            return tasks?.Tasks ?? new List<TranslateTask>();
        return new List<TranslateTask>();
    }

    private class CachedTasks
    {
        public List<TranslateTask> Tasks { get; set; } = new();
        public DateTime AbsoluteExpiration { get; set; }
    }
}
```

Ogni nuovo compito:

- Viene aggiunto al rolling window dell'utente
- Spinge fuori il più vecchio se siamo alla capacità (5 max)
- Evapori completamente dopo 6 ore (assoluto) o 1 ora di inattività (scivolo)

```mermaid
flowchart LR
    subgraph Window["Per-User Task Window (Max 5)"]
        T1[Oldest Task] --- T2[Older] --- T3[Recent] --- T4[Newer] --- T5[Newest]
    end

    New[New Task] -->|Enqueue| Window
    T1 -->|Evicted| Gone[(Expired)]

    style Window fill:none,stroke:#10b981,stroke-width:2px
    style Gone fill:none,stroke:#ef4444,stroke-width:2px
```

Nessun rischio di ritenzione. Nessun PII nella cache (solo ID attività, timestamp e codici linguistici). Nessun mal di testa GDPR.

---


## Concorrenza legata - Il governatore Loop

Il servizio di background implementa un **Concorrenze fisse** loop utilizzando un lettore di canali e `Task.WhenAny`:

```csharp
// From BackgroundTranslateService.cs
private async Task TranslateFilesAsync(CancellationToken cancellationToken)
{
    var processingTasks = new List<Task>();

    while (!cancellationToken.IsCancellationRequested)
    {
        // Fill up to IPCount concurrent tasks (e.g., 4 parallel translations)
        while (processingTasks.Count < markdownTranslatorService.IPCount &&
               !cancellationToken.IsCancellationRequested)
        {
            var item = await _translations.Reader.ReadAsync(cancellationToken);
            var translateModel = item.Item1;
            var tcs = item.Item2;

            // Start the task and add it to the list
            var task = TranslateTask(cancellationToken, translateModel, item, tcs);
            processingTasks.Add(task);
        }

        // Wait for ANY of the tasks to complete
        var completedTask = await Task.WhenAny(processingTasks);
        processingTasks.Remove(completedTask);

        // Handle exceptions if needed
        try
        {
            await completedTask;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error translating markdown");
        }
    }
}
```

E' come se **regolatore su un motore a vapore**: più carico → contropressione costruisce → strozzatura naturale → stabilità.

Il modello ti dà:

- **Nessuna spirale di sovraccarico** - non si possono deporre compiti illimitati
- **Comportamento morbido in tempo reale** - latenza legata
- **Levigatura naturale delle esplosioni** -canale buffers i picchi
- **Stabilità sotto carico** -degrada con grazia

---


## Completare laFonte del Completamento del TaskCource

Quando il lavoro di traduzione effettiva finisce, completiamo il TCS:

```csharp
// From BackgroundTranslateService.cs
private async Task TranslateTask(
    CancellationToken cancellationToken,
    PageTranslationModel translateModel,
    (PageTranslationModel, TaskCompletionSource<TaskCompletion>) item,
    TaskCompletionSource<TaskCompletion> tcs)
{
    try
    {
        await retryPolicy.ExecuteAsync(async () =>
        {
            // Do the actual translation work
            var translatedMarkdown = await markdownTranslatorService.TranslateMarkdown(
                translateModel.OriginalMarkdown,
                translateModel.Language,
                cancellationToken);

            // SUCCESS: Complete the TCS with the result
            tcs.SetResult(new TaskCompletion(
                translatedMarkdown,
                translateModel.OriginalMarkdown,
                translateModel.Language,
                true,
                DateTime.Now));
        });
    }
    catch (TranslateException e)
    {
        // FAILURE: Complete the TCS with an exception
        tcs.SetException(new Exception($"Translation failed after 3 retries: {e.Message}"));
    }
    catch (Exception e)
    {
        // UNEXPECTED: Complete the TCS with the exception
        tcs.SetException(e);
    }
}
```

Il chiamante e' `Task<TaskCompletion>` - che hanno ricevuto subito quando hanno chiamato `Translate()` - Ora completano.

- `await` se vogliono bloccare
- Controlla `IsCompleted` Sondaggio
- Controlla `IsFaulted` per vedere se è fallito
- Controlla `Result` per ottenere il contenuto tradotto

---


## La proiezione dello stato - Deriving State da Task State

Quando mostriamo le attività all'utente, proiettiamo lo stato delle attività live in un modello di visualizzazione:

```csharp
// From TranslateTask.cs
public TranslateResultTask(TranslateTask task, bool includeMarkdown = false)
{
    TaskId = task.TaskId;
    StartTime = task.StartTime;
    Language = task.Language;

    // Check for faulted state first -a faulted task is also "completed" in .NET terms
    if (task.Task?.IsFaulted == true)
    {
        Failed = true;
        Completed = false;
        TotalMilliseconds = (int)(DateTime.Now - task.StartTime).TotalMilliseconds;
    }
    else if (task.Task?.IsCompletedSuccessfully == true)
    {
        Completed = true;
        Failed = false;
        var endTime = task.Task.Result.EndTime;
        TotalMilliseconds = (int)((endTime - task.StartTime)!).Value.TotalMilliseconds;
        EndTime = endTime;
    }
    else
    {
        // Still in progress
        Completed = false;
        Failed = false;
        TotalMilliseconds = (int)(DateTime.Now - task.StartTime).TotalMilliseconds;
    }

    if (Completed && includeMarkdown)
    {
        var result = task.Task?.Result;
        if (result == null) return;
        OriginalMarkdown = result.OriginalMarkdown;
        TranslatedMarkdown = result.TranslatedMarkdown;
    }
}
```

### Una nota sullo Stato dei compiti

.NET's `Task` ha alcune strane combinazioni di stato:

- `IsCompleted` è vero per **entrambi** Completamento riuscito E compiti difettosi
- `IsCompletedSuccessfully` è vero solo per il successo
- `IsFaulted` significa che ha lanciato un'eccezione
- `IsCanceled` significa che è stato cancellato

Quindi devi controllare. `IsFaulted` prima del controllo `IsCompleted`, o tratterai i fallimenti come successi.

---


## Il Polling UI -HTMX

I sondaggi di visualizzazione per gli aggiornamenti utilizzando HTMX `hx-trigger`:

```html
@* From _GetTranslations.cshtml *@
@{
    var allCompleted = Model.All(x => x.Completed);
    var trigger = allCompleted ? "none" : "every 5s";
}

<div class="translationpoller"
     hx-get="/editor/get-translations"
     hx-swap="outerHTML"
     hx-trigger="@trigger">
    <table class="table">
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @if (item.Completed)
                    {
                        <a href="#" x-on:click.prevent="viewTranslation('@item.TaskId')">View</a>
                    }
                    else if (item.Failed)
                    {
                        <text>Failed</text>
                    }
                    else
                    {
                        <text>Processing</text>
                    }
                </td>
                <td>
                    @if (item.Completed)
                    {
                        <i class='bx bx-check text-green'></i>
                    }
                    else if (item.Failed)
                    {
                        <i class='bx bx-x text-red'></i>
                    }
                    else
                    {
                        <img src="~/img/3-dots-bounce.svg" />
                    }
                </td>
                <td>@item.Language</td>
                <td>@TimeSpan.FromMilliseconds(item.TotalMilliseconds).Humanize()</td>
            </tr>
        }
    </table>
</div>
```

La parte intelligente: `hx-trigger="@trigger"` cambiamenti basati sullo stato:

- Se le attività sono ancora in esecuzione: sondaggio ogni 5 secondi
- Se tutti i compiti sono eseguiti: fermare il sondaggio (`"none"`)

Questo e' **Autoregolazione dei sondaggi** - si ferma automaticamente quando non c'è niente da guardare.

---


## Recupero del risultato

Quando l'utente fa clic su "Visualizza," prendiamo la traduzione completata:

```csharp
// From TranslateAPI.cs
[HttpGet("get-translation/{taskId}")]
public async Task<Results<JsonHttpResult<TranslateResultTask>, BadRequest<string>>> GetTranslation(
    string taskId)
{
    var userId = Request.GetUserId(Response);
    var tasks = translateCacheService.GetTasks(userId);

    var translationTask = tasks.FirstOrDefault(t => t.TaskId == taskId);
    if (translationTask == null)
        return TypedResults.BadRequest("Task not found");

    // Include the markdown content in the response
    var result = new TranslateResultTask(translationTask, includeMarkdown: true);
    return TypedResults.Json(result);
}
```

Il JavaScript poi popola l'editor:

```javascript
// From translations.js
export function viewTranslation(taskId) {
    fetch(`/api/translate/get-translation/${taskId}`)
        .then(response => response.json())
        .then(data => {
            // Show the translated content area
            document.getElementById("translatedcontent").classList.remove("hidden");

            // Populate the editors
            var originalMde = window.mostlylucid.simplemde.getinstance('translatedcontentarea');
            originalMde.value(data.originalMarkdown);

            var mde = window.mostlylucid.simplemde.getinstance('markdowneditor');
            mde.value(data.translatedMarkdown);
        });
}
```

---


## Perché questo è Privacy-Safe

Perché:

- **Nessun contenuto utente è memorizzato nella cache** -solo metadati delle attività (ID, timestamp, lingua)
- **Il contenuto esiste solo nel risultato dell'attività** -che è in memoria, allegato al Task
- **La finestra è delimitata** -max 5 attività per utente, max 6 ore di ritenzione
- **Tutto decade naturalmente** -la scadenza scorrevole pulisce gli utenti inattivi
- **Niente tocca il disco** -Nessun registro, nessuna coda, nessun database
- **Non è possibile mantenere accidentalmente PII** - Non c'è posto dove possa andare.

Questa è l'architettura *Vorrei che browser e frontend framework avessero utilizzato per le operazioni di sessione*.

---


## Il comportamento emergente

Cosa ottieni senza pensare troppo:

| Benefit | How |
|---------|-----|
| **Stabilità** | Concurrency delimitata via canale + motivo semaforo |
| **Loop di feedback negativo** | Più carico → contropressione → memoria delimitata → comportamento coerente |
| **Debug di visibilità** | Le operazioni esatte sono presenti appena il tempo necessario per ispezionare |
| **Ripristino** | I risultati completati sono accessibili fino alla loro scadenza |
| **Conservazione zero dei dati** | Privacy e semplicità allineate per una volta |
| **Auto-ottimizzazione dell'esecuzione** | Le vecchie mansioni si allontanano, quelle rilevanti rimangono |

Proprio come la cache LRU ha affilato la memoria comportamentale, la finestra di funzionamento a rotolamento affila la vista di esecuzione.

---


## Quando usare questo modello

Usalo quando:

- I dati dell'utente non devono persistere
- Le operazioni sono di breve durata (dai secondi ai minuti)
- Debug
- Questioni relative alla privacy
- Questioni relative alla stabilità del carico
- Vuoi risposta istantanea + elaborazione dello sfondo

Non usarlo quando:

- Hai bisogno di una vera durata (usare una vera coda)
- Le operazioni richiedono ore (usare Hangfire o simili)
- È necessario esattamente-una volta semantica (usare una transazione distribuita)
- È necessario coordinare più server (usare Redis o un broker di messaggi)

---


## Conclusione -Effimera esecuzione come filosofia del design

Se [Articolo LRU](/blog/learning-lrus-when-capacity-makes-systems-better) era circa **imparare dimenticando**, questo è circa **esecuzione mediante evaporazione**.

Il sistema ricorda esattamente abbastanza per essere utile - e niente di più.

E'

- limitato
- deterministico
- privato
- debuggabile
- recuperabile
- autopulente
- robusto
- e architettura-first

La cosa migliore? Sai già come implementarlo: un `TaskCompletionSource`, a `Channel`, a `IMemoryCache` con scadenza scorrevole, e un lavoratore di background.

**Fuoco... e non dimenticarti.**

---


## Cosa c'e' dopo?

Dentro **[Parte 2: Costruire una biblioteca di esecuzione effimera riutilizzabile](/blog/ephemeral-execution-library)**Trasformeremo questo schema in un aiutante drop-in:

- `EphemeralForEachAsync<T>` - come `Parallel.ForEachAsync` ma con monitoraggio delle operazioni
- Gasdotti a chiave per l'esecuzione sequenziale per entità
- `EphemeralWorkCoordinator<T>` - una coda di lavoro osservabile a lunga durata
- Coordinatori nominati/digitati con `AddEphemeralWorkCoordinator<TCoordinator>` (come `AddHttpClient`)
- Full DI integrazione con una durata di vita determinata e singleton
- Confronto con altri approcci (flusso dati TPL, canali, servizi di background)

---


## Collegamenti

- [Imparare le LRU - Quando l'eccesso di capacità rende il vostro sistema migliore](/blog/learning-lrus-when-capacity-makes-systems-better) - l'articolo di accompagnamento sulla memoria limitata
- [Parte 2: Costruire una biblioteca di esecuzione effimera riutilizzabile](/blog/ephemeral-execution-library) - la piena attuazione
- [Documentazione TaskCuplementSource](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) - Documenti di Microsoft sul modello TCS
- [System.Threading.Channels](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) - il produttore-consumatore primitivo che usiamo
- [Documentazione IMemoryCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory) -ASP.NET built-in cache