# Eld och gör det inte *Ganska* Oförgätentlig avrättning utan tillstånd

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

De flesta async-system kommer antingen ihåg för mycket (loggar, köer, ihållande skräp du aldrig ville ha)... eller så minns de ingenting alls (eld-och-glömma svarta hål som försvinner när något går fel).

Denna artikel introducerar något annat:

> Ett mönster där varje async-operation blir en liten **Spårbar**, **inspekteras**, **återinsättningsbar**, **självrengörande arbetsenhet** -och hela systemet förblir privat, avgränsat, snabbt och deterministiskt.

Det är följeslagaren till min tidigare artikel, **[Lära LRU - När överträffa kapacitet gör ditt system bättre](/blog/learning-lrus-when-capacity-makes-systems-better)**. Att man utforskade hur avgränsat minne + glidande utgång blir en överlevnadsmekanism.

Den här utforskar den andra halvan: **avgränsad, flyktig avrättning** -hur en liten rullande buffert av aktiva uppgifter blir en avlusare, en händelselogg, och en replay motor, utan att behålla några användardata.

Exemplet kommer från min bloggs översättning widget - en liten UI för att översätta markdown innehåll i farten. Det ser trivialt ut. Under huven, det implementerar mönster som du kan stjäla för alla async arbetsflöden.

Det här är **Häfte 1** i en serie med två delar:

- **Del 1 (denna artikel)**: Teorin, mönstret och ett verkligt exempel
- **[Del 2: Att bygga ett återanvändbart efemärt avrättningsbibliotek](/blog/ephemeral-execution-library)**: Fullständigt genomförande med integrering av DI

## NUGET!!!

[Detta är nu i de flesta lucid.ephemerals Nuget paket också mer än 20 mestadels lucid.ephemerals mönster och "atomer"](https://www.nuget.org/packages?q=mostlylucid&includeComputedFrameworks=true&prerel=true&sortby=created-desc).

[![Hämta](https://img.shields.io/nuget/v/mostlylucid.ephemeral.svg)](https://www.nuget.org/packages/mostlylucid.ephemeral)
[![Licens](https://img.shields.io/badge/license-Unlicense-blue.svg)](../../UNLICENSE)

[TOC]

---


## Problemet -Async Workflows skapa dolda tillstånd

ASP.NET utvecklare tenderar att välja mellan tre dåliga alternativ:

### Eld-och-glömma

Du där. `Task.Run()` När den sprängs får man inget samband, ingen rekonstruktion, ingen aning om vad som misslyckades.

### Eld-och-vänta

Genomströmning dör, latens exploderar och användarna hatar dig.

### Köer/kanaler överallt

Och nu har du:

- obundna köer
- Ihållande tillstånd du inte menade att hålla
- PII förvaras av misstag någonstans
- ett distribuerat system där du bara ville ha en enkel operation

### Distribuerad spårning

Användbar... men extern. Dessutom: det kan inte spela upp något och ofta läcker data du aldrig tänkt att lagra.

Vad vi egentligen vill ha är:

- **Kortlivad**
- **privat**
- **Avlusningsbar**
- **inspekteras**
- **avgränsad**
- **återanvändbar vid behov**
- **och sedan borta**

Ett "bara tillräckligt långt" minne av vad systemet gör - och inget mer.

---


## Mönstret - Eld och *Inte riktigt* Glöm

Här är idén i en mening:

> **Starta en async-operation omedelbart, spåra den explicit via en TaskCompletionSource, rensa upp den deterministiskt, och hålla en liten rullande buffert av de senaste uppgifterna så att du kan inspektera eller hämta dem - utan att behålla något användarinnehåll.**

Det sitter halvvägs mellan:

- Händelseinköp
- spårning
- jobbköer
- futures/promises
- Efemära sessioner

...utan att bli någon av dem.

```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
```

---


## Anatomi av en efemär uppgift

Den centrala datastrukturen är en `TranslateTask` -en liten atomär avrättningsenhet som backas upp av en `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);
```

Den innehåller:

- Ett unikt ID (korrelerar över hela flödet)
- En tidsstämpel (när det började)
- Metadata (språk - inte användarinnehåll!)
- En hänvisning till det faktiska `Task<TaskCompletion>`
- Varaktighet (beräknad på tillträde)
- Feltillstånd (härlett från uppgiftstillstånd)
- Slutresultat (endast om det har slutförts med lyckat resultat)

Inget kvarstod, inget skrivet till diskett, inget lagrat utanför ett rullande fönster.

AWS har stegfunktioner. Azure har hållbara funktioner. Du har ... 30 rader kod som inte kräver en molnräkning.

---


## AktivitetskompletteringSource Mönster -Bridning Fire-and-Glöm till Fire-and-Track

Magin händer i `BackgroundTranslateService`. I stället för eld-och-glömma, använder vi en [Källa för slutförande av uppgift](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) -en löfteslik konstruktion som låter oss återvända omedelbart medan arbetet sker i bakgrunden.

```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;
}
```

### Vad är en uppgiftskompletteringskälla?

Om du inte har använt `TaskCompletionSource<T>` före, se det som en **lova att du styr manuellt**. Till skillnad från en vanlig `Task` som slutför när dess arbete avslutas, en TCS färdig när *du* samtal `SetResult()`, `SetException()`, eller `SetCanceled()`.

Detta låter dig:

1. Returnera en `Task` till den som ringer omedelbart
2. Gör själva arbetet någon annanstans (olika tråd, bakgrundstjänst, etc.)
3. Fyll i uppgiften när *du* bestämma att det är gjort

Det är bron mellan "eld-och-glöm" och "eld-och-spår".

---


## API-skiktet - Starta en översättning

När en användare klickar på "Translate" anropar gränssnittet 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'
    });
});
```

API:et returneras omedelbart med bara ett aktivitets-ID:

```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);
}
```

Svaret är omedelbart. Den faktiska översättningen körs i bakgrunden. Användaren kan välja eller bara titta på UI- uppdateringen.

---


## Rullande drift fönster - En självrengörande buffer

Den viktigaste insikten från [LRU-artikel](/blog/learning-lrus-when-capacity-makes-systems-better) gäller även här:

> **Håll bara tillräckligt med historia för att felsöka och resonera om systemet... och låta naturligt förfall radera allt annat.**

```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; }
    }
}
```

Varje ny uppgift:

- Läggs till i användarens rullande fönster
- Trycker ut den äldsta om vi har kapacitet (5 max)
- Avdunstar helt efter 6 timmar (absolut) eller 1 timmes inaktivitet (glidning)

```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
```

Ingen risk för retention. Ingen PII i cache (bara uppgifts-ID, tidsstämplar och språkkoder). Ingen GDPR-huvudvärk.

---


## Gränslös konkurs Guvernör Loop

Bakgrundstjänsten implementerar en **Fast överensstämmelse** loop med hjälp av en kanalläsare och `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");
        }
    }
}
```

Det här är som en **Guvernör på en ångmaskin**: mer belastning → mottryck bygger → naturlig strypning → stabilitet.

Mönstret ger dig:

- **Ingen överbelastning spiral** -kan inte skapa obegränsade uppgifter
- **Mjukt beteende i realtid** -begränsad latens
- **Naturlig utjämning av sprickbildningar** -kanal buffertar spikarna
- **Stabilitet under belastning** - Försämrar graciöst.

---


## Fullbordande av aktivitetskompletteringskällan

När det faktiska översättningsarbetet är avslutat, slutför vi 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);
    }
}
```

Den som ringer `Task<TaskCompletion>` -som de fick direkt när de ringde `Translate()` - Nu är de färdiga.

- `await` det om de vill blockera
- Kontrollera `IsCompleted` till undersökning
- Kontrollera `IsFaulted` för att se om den misslyckades
- Kontrollera `Result` för att få det översatta innehållet

---


## Statusprojicering -Deriving stat från Task State

När vi visar uppgifter för användaren projicerar vi liveuppgiftstillståndet i en vymodell:

```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;
    }
}
```

### En anmärkning om uppgiftsstat

.NET:s `Task` har några udda tillståndskombinationer:

- `IsCompleted` är sant för **båda** framgångsrikt slutförande OCH felaktiga uppgifter
- `IsCompletedSuccessfully` gäller endast för framgång
- `IsFaulted` betyder att det kastade ett undantag
- `IsCanceled` betyder att det ställdes in

Så du måste kolla `IsFaulted` Före kontroll `IsCompleted`Annars behandlar du misslyckanden som framgångar.

---


## UI -HTMX-mätningen

Vyundersökningar för uppdateringar med hjälp av HTMX's `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>
```

Den smarta biten: `hx-trigger="@trigger"` Förändringar baserade på tillstånd:

- Om uppgifter fortfarande körs: opinionsundersökning var femte sekund
- Om alla uppgifter är gjorda: stoppa röstningen (`"none"`)

Det här är **Självreglerande röstning** -Det slutar automatiskt när det inte finns något att titta på.

---


## Hämta resultatet

När användaren klickar på "Visa" hämtar vi den färdiga översättningen:

```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);
}
```

JavaScriptet fyller sedan i editorn:

```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);
        });
}
```

---


## Varför detta är integritetssäkert

Därför att:

- **Inget användarinnehåll lagras i cache** -endast aktivitetsmetadata (ID, tidsstämpel, språk)
- **Innehåll finns bara i Aktivitetens resultat** -som är till minne, knuten till uppgiften
- **Fönstret är avgränsat** -max 5 uppgifter per användare, max 6 timmar kvar
- **Allt förfaller naturligt** -glidande utgångsdatum rensar upp inaktiva användare
- **Inget vidrör disken** - Inga loggar, inga köer, ingen databas
- **Du kan inte av misstag behålla PII** - Det finns ingenstans att ta vägen.

Det här är arkitekturen. *Jag önskar att webbläsare och frontendramar hade använts för sessionsoperationer*.

---


## Det nya beteendet

Vad du får utan att tänka för mycket:

VÄLKOMMEN FRÅN FLYGPLATSEN
|---------|-----|
| **Stabilitet** till Gränsad konvergens via kanal + semaformönster
| **Negativ återkopplingsslinga** Mer belastning → mottryck → avgränsat minne → konsekvent beteende
| **Felsökningssynlighet** Den exakta verksamheten är närvarande bara tillräckligt länge för att inspektera
| **Återvinningsförmåga** Slutförda resultat är tillgängliga tills de upphör att gälla
| **Noll lagring av uppgifter** med integritet och enkelhet anpassad för en gångs skull
| **Självoptimerande genomförande** på gamla uppgifter faller bort, relevanta förblir  med

Precis som LRU-cachen skärpte beteendeminnet skärper det rullande driftfönstret körvyn.

---


## När du ska använda detta mönster

Använd den när:

- Användardata får inte kvarstå
- Verksamheten är kortlivad (sekunder till minuter)
- Avlusning av ärenden
- Sekretessfrågor
- Belastningsstabilitetsfrågor
- Du vill ha omedelbar respons + bakgrundsbehandling

Använd den inte när:

- Du behöver sann hållbarhet (använd en riktig kö)
- Verksamheten tar timmar (använd Hangfire eller liknande)
- Du behöver exakt en gång semantik (använd en distribuerad transaktion)
- Flera servrar måste samordna (använd Redis eller en meddelandemäklare)

---


## Slutsats -Ephemeral Execution as a Design Philosophy

Om inte annat anges ska [LRU-artikel](/blog/learning-lrus-when-capacity-makes-systems-better) var ungefär **Att lära sig genom att glömma**, Den här handlar om **Genomförande genom avdunstning**.

Systemet kommer ihåg exakt tillräckligt för att vara till hjälp - och inget mer.

Det är:

- avgränsad
- Deterministisk
- privat
- Avlusningsbar
- återanvändbar
- självrengörande
- robust
- och arkitektur-först

Du vet redan hur man genomför det: en `TaskCompletionSource`, , , , . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . `Channel`, en `IMemoryCache` med glidande utgång, och en bakgrundsarbetare.

**Eld... och glöm inte.**

---


## Vad är nästa?

Till **[Del 2: Att bygga ett återanvändbart efemärt avrättningsbibliotek](/blog/ephemeral-execution-library)**Vi förvandlar mönstret till en drop-in-hjälpare.

- `EphemeralForEachAsync<T>` - Som jag sa. `Parallel.ForEachAsync` men med driftspårning
- Nyckelrörledningar för utförande per enhet i följd
- `EphemeralWorkCoordinator<T>` - en långlivad observerbar arbetskö
- Namngivna/typade samordnare med `AddEphemeralWorkCoordinator<TCoordinator>` (som `AddHttpClient`)
- Fullständig DI-integration med räckvidd och singleton-livslängder
- Jämförelse med andra metoder (TPL Dataflow, Kanaler, Bakgrundstjänster)

---


## Länkar

- [Lära LRU - När överträffa kapacitet gör ditt system bättre](/blog/learning-lrus-when-capacity-makes-systems-better) - följeslagaren artikeln om avgränsat minne
- [Del 2: Att bygga ett återanvändbart efemärt avrättningsbibliotek](/blog/ephemeral-execution-library) -det fullständiga genomförandet
- [Dokumentation om aktivitetskompletteringKälla](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) -Microsofts dokument på TCS-mönstret.
- [System. Threading.Channels](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) -den producent-konsument primitiva vi använder
- [ImoryCache-dokumentation](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory) -ASP.NETs inbyggda cache