# Een remote Markdown Fetcher bouwen voor Markdig

<!--category-- Markdown, AI-Article,  MarkDig, ASP.NET Core, C#, API, Nuget, FetchExtension-->
<datetime class="hidden">2025-11-07T10:00</datetime>

# Inleiding

Een van de uitdagingen die ik geconfronteerd tijdens het bouwen van deze blog was hoe efficiënt externe markdown inhoud zonder handmatig kopiëren en plakken overal.

Ik wilde README-bestanden uit mijn GitHub repositories ophalen, documentatie van andere projecten opnemen en alles automatisch synchroniseren.`Mostlylucid.Markdig.FetchExtension`De oplossing?

> Een aangepaste Markdig extensie die remote markdown ophaalt op het moment van weergeven en het intelligent caches.

> In deze post, zal ik u door hoe ik gebouwd

> **- een complete oplossing voor het ophalen en cachen van remote markdown content met ondersteuning voor meerdere opslag backends, automatische polling, en een oud-while-revalidate caching patroon.**OPMERKING: Dit is nog steeds prerelease, maar ik wilde het naar buiten krijgen.`disable="true"`Veel plezier, maar het werkt misschien nog niet.

> **Dit artikel is AI gegenereerd - met behulp van claude code die ook hielp me bouwen van de functie.**UPDAAT`[TOC]`: Toegevoegd

parameter zodat we nu de tags goed kunnen demograferen zonder dat ze verwerkt worden![UPDATE (nov 7, 2025)](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension).

[![: Toegevoegd Inhoudsopgave (TOC) generatie functie!](https://img.shields.io/nuget/v/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
[![Gebruik](https://img.shields.io/nuget/dt/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)

[TOC]

# in uw markdown om automatisch een klikbare inhoudsopgave uit documentkoppen te genereren.

Zie de bron ervoor hier

1. **op de GitHub voor deze site**NuGet
2. **NuGet**Waarom dit bouwen?
3. **Voordat ik in de technische details duik, leg ik het probleem uit.**Ik heb verschillende scenario's waarin ik externe markdown content moet opnemen:
4. **README's van het pakket**: Wanneer ik schrijf over een NuGet pakket dat ik heb gepubliceerd, wil ik de README direct van GitHub

API-documentatie

- : Externe API-docs die vaak moeten blijven synchroniseren
- Gedeelde inhoud
- : Documentatie die in één repository leeft maar op meerdere plaatsen moet verschijnen
- Prestaties

: Ik wil deze inhoud niet ophalen op elke pagina lading - dat zou langzaam en verspillend zijn

# De naïeve benadering zou zijn om een HTTP client te gebruiken om markdown op te halen wanneer je het nodig hebt.

Maar dat is problematisch:

```mermaid
graph TD
    A[Markdown with fetch tags] --> B[MarkdownFetchPreprocessor]
    B --> C{Check Cache}
    C -->|Fresh| D[Return Cached Content]
    C -->|Stale/Missing| E[Fetch from Remote URL]
    E -->|Success| F[Update Cache]
    E -->|Failure| G{Has Cached?}
    G -->|Yes| H[Return Stale Cache]
    G -->|No| I[Return Error Comment]
    F --> J[Replace fetch with Content]
    D --> J
    H --> J
    I --> J
    J --> K[Processed Markdown]
    K --> L[Your Markdig Pipeline]
    L --> M[Final HTML]

```

Elk verzoek raakt de externe server**Netwerk latency impact pagina laadtijden**Geen offline ondersteuning

1. Geen behandeling van voorbijgaande storingen`<fetch>`Ik had iets slimmer nodig: één keer ophalen, intelligent cachen, automatisch opfrissen en fouten op een elegante manier afhandelen.
2. Overzicht architectuur
3. De uitbreiding volgt eerder een voorbewerkingsaanpak dan deel uit te maken van de Markdig-ontleedpijplijn.
4. Dit is cruciaal omdat het betekent opgehaalde inhoud stroomt door uw hele Markdig pijplijn, het krijgen van al uw aangepaste extensies, syntax highlighting, en styling.

Het belangrijkste inzicht hier is

# voorbewerking



```markdown
# My Documentation

<fetch markdownurl="https://raw.githubusercontent.com/user/repo/main/README.md"
       pollfrequency="24" disable="true"/>
```

Voordat je de Markdig-pijpleiding raakt, zijn we:

- Scannen op
- tags
- De inhoud op te lossen (van cache of remote)
- De tags vervangen door echte markdown

# Laat Markdig dan alles samen verwerken.

Dit zorgt voor consistentie - alle markdown krijgt dezelfde behandeling ongeacht de bron.**De basissyntaxis**Het gebruik van de extensie is eenvoudig.

```mermaid
graph LR
    A[IMarkdownFetchService Interface] --> B[InMemoryMarkdownFetchService]
    A --> C[FileBasedMarkdownFetchService]
    A --> D[PostgresMarkdownFetchService]
    A --> E[SqliteMarkdownFetchService]
    A --> F[SqlServerMarkdownFetchService]
    A --> G[YourCustomService]

    B --> H[ConcurrentDictionary]
    C --> I[File System + SemaphoreSlim]
    D --> J[PostgreSQL Database]
    E --> K[SQLite Database]
    F --> L[SQL Server Database]
    G --> M[Your Storage Backend]

```

## In je afdruk:

Dat is het!`IMarkdownFetchService`:

```csharp
public interface IMarkdownFetchService
{
    Task<MarkdownFetchResult> FetchMarkdownAsync(
        string url,
        int pollFrequencyHours,
        int blogPostId = 0);

    Task<bool> RemoveCachedMarkdownAsync(
        string url,
        int blogPostId = 0);
}
```

De uitbreiding zal:

## Haal de README van GitHub

Cache het voor 24 uur`ConcurrentDictionary`:

```csharp
public class InMemoryMarkdownFetchService : IMarkdownFetchService
{
    private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<InMemoryMarkdownFetchService> _logger;

    public async Task<MarkdownFetchResult> FetchMarkdownAsync(
        string url,
        int pollFrequencyHours,
        int blogPostId)
    {
        var cacheKey = GetCacheKey(url, blogPostId);

        // Check cache
        if (_cache.TryGetValue(cacheKey, out var cached))
        {
            var age = DateTimeOffset.UtcNow - cached.FetchedAt;
            if (age.TotalHours < pollFrequencyHours)
            {
                _logger.LogDebug("Returning cached content for {Url}", url);
                return new MarkdownFetchResult
                {
                    Success = true,
                    Content = cached.Content
                };
            }
        }

        // Fetch fresh content
        var fetchResult = await FetchFromUrlAsync(url);

        if (fetchResult.Success)
        {
            _cache[cacheKey] = new CacheEntry
            {
                Content = fetchResult.Content,
                FetchedAt = DateTimeOffset.UtcNow
            };
        }
        else if (cached != null)
        {
            // Fetch failed, return stale cache
            _logger.LogWarning("Fetch failed, returning stale cache for {Url}", url);
            return new MarkdownFetchResult
            {
                Success = true,
                Content = cached.Content
            };
        }

        return fetchResult;
    }

    private static string GetCacheKey(string url, int blogPostId)
        => $"{url}_{blogPostId}";
}
```

Teruggeven van gecachede inhoud op volgende verzoeken

1. Automatisch vernieuwen wanneer de cache verloopt
2. Architectuur van de opslagprovider
3. Een van de ontwerpprincipes die ik volgde was
4. flexibiliteit
5. 
6. Verschillende toepassingen hebben verschillende behoeften.
7. Een kleine demo app heeft PostgreSQL niet nodig, maar een multi-server productie implementatie wel.

Dus bouwde ik een pluggable opslag architectuur:**De kerninterface**Alles implementeert

## Eenvoudig en schoon.

Elke implementatie behandelt opslag op zijn eigen manier, maar de interface blijft consistent.

```csharp
public class FileBasedMarkdownFetchService : IMarkdownFetchService
{
    private readonly string _cacheDirectory;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<FileBasedMarkdownFetchService> _logger;
    private readonly SemaphoreSlim _fileLock = new(1, 1);

    public async Task<MarkdownFetchResult> FetchMarkdownAsync(
        string url,
        int pollFrequencyHours,
        int blogPostId)
    {
        var cacheKey = ComputeCacheKey(url, blogPostId);
        var cacheFile = GetCacheFilePath(cacheKey);

        await _fileLock.WaitAsync();
        try
        {
            // Check if file exists and is fresh
            if (File.Exists(cacheFile))
            {
                var fileInfo = new FileInfo(cacheFile);
                var age = DateTimeOffset.UtcNow - fileInfo.LastWriteTimeUtc;

                if (age.TotalHours < pollFrequencyHours)
                {
                    var cached = await File.ReadAllTextAsync(cacheFile);
                    return new MarkdownFetchResult
                    {
                        Success = true,
                        Content = cached
                    };
                }
            }

            // Fetch fresh
            var fetchResult = await FetchFromUrlAsync(url);

            if (fetchResult.Success)
            {
                await File.WriteAllTextAsync(cacheFile, fetchResult.Content);
            }
            else if (File.Exists(cacheFile))
            {
                // Return stale on fetch failure
                var stale = await File.ReadAllTextAsync(cacheFile);
                return new MarkdownFetchResult
                {
                    Success = true,
                    Content = stale
                };
            }

            return fetchResult;
        }
        finally
        {
            _fileLock.Release();
        }
    }

    private string GetCacheFilePath(string cacheKey)
        => Path.Combine(_cacheDirectory, $"{cacheKey}.md");

    private static string ComputeCacheKey(string url, int blogPostId)
    {
        var combined = $"{url}_{blogPostId}";
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(combined);
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToHexString(hash);
    }
}
```

In-Geheugen opslag: Perfect voor demo's

1. De eenvoudigste implementatie maakt gebruik van`SemaphoreSlim`Zoals je kunt zien doet dit het volgende:
2. Maakt een cache sleutel van de URL en blog post ID
3. Controleer of we gecached inhoud hebben en of het vers is
4. Als cache vers is, geeft deze onmiddellijk terug
5. Als oud, probeert om verse inhoud op te halen

## Bij succes, update de cache

Bij fout met gecachede inhoud, geeft oude cache terug (stale-while-revalidate!)

```csharp
public class PostgresMarkdownFetchService : IMarkdownFetchService
{
    private readonly MarkdownCacheDbContext _dbContext;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<PostgresMarkdownFetchService> _logger;

    public async Task<MarkdownFetchResult> FetchMarkdownAsync(
        string url,
        int pollFrequencyHours,
        int blogPostId)
    {
        var cacheKey = GetCacheKey(url, blogPostId);

        // Query cache
        var cached = await _dbContext.MarkdownCache
            .FirstOrDefaultAsync(c => c.CacheKey == cacheKey);

        if (cached != null)
        {
            var age = DateTimeOffset.UtcNow - cached.LastFetchedAt;
            if (age.TotalHours < pollFrequencyHours)
            {
                return new MarkdownFetchResult
                {
                    Success = true,
                    Content = cached.Content
                };
            }
        }

        // Fetch fresh
        var fetchResult = await FetchFromUrlAsync(url);

        if (fetchResult.Success)
        {
            if (cached == null)
            {
                cached = new MarkdownCacheEntry
                {
                    CacheKey = cacheKey,
                    Url = url,
                    BlogPostId = blogPostId
                };
                _dbContext.MarkdownCache.Add(cached);
            }

            cached.Content = fetchResult.Content;
            cached.LastFetchedAt = DateTimeOffset.UtcNow;
            await _dbContext.SaveChangesAsync();
        }
        else if (cached != null)
        {
            // Return stale
            return new MarkdownFetchResult
            {
                Success = true,
                Content = cached.Content
            };
        }

        return fetchResult;
    }
}
```

Bij fout zonder cache, geeft fout terug

```sql
CREATE TABLE markdown_cache (
    id SERIAL PRIMARY KEY,
    cache_key VARCHAR(128) NOT NULL UNIQUE,
    url VARCHAR(2048) NOT NULL,
    blog_post_id INTEGER NOT NULL,
    content TEXT NOT NULL,
    last_fetched_at TIMESTAMP WITH TIME ZONE NOT NULL,
    CONSTRAINT ix_markdown_cache_cache_key UNIQUE (cache_key)
);

CREATE INDEX ix_markdown_cache_url_blog_post_id
    ON markdown_cache(url, blog_post_id);
```

Dit patroon -

```mermaid
graph TB
    subgraph "Load Balancer"
        LB[Load Balancer]
    end

    subgraph "Application Servers"
        A1[App Server 1<br/>FetchExtension]
        A2[App Server 2<br/>FetchExtension]
        A3[App Server 3<br/>FetchExtension]
    end

    subgraph "Shared Cache"
        PG[(PostgreSQL<br/>markdown_cache table)]
    end

    subgraph "External Content"
        R1[Remote URL 1]
        R2[Remote URL 2]
        R3[Remote URL 3]
    end

    LB --> A1
    LB --> A2
    LB --> A3

    A1 <-->|Read/Write Cache| PG
    A2 <-->|Read/Write Cache| PG
    A3 <-->|Read/Write Cache| PG

    A1 -.->|Fetch if cache miss| R1
    A2 -.->|Fetch if cache miss| R2
    A3 -.->|Fetch if cache miss| R3

```

oud-while-revalidate

# - is cruciaal voor betrouwbaarheid.

Zelfs als GitHub is neer, uw site blijft werken met gecached inhoud.

```bash
dotnet add package mostlylucid.Markdig.FetchExtension
```

Bestandsgestuurde opslag: Eenvoudige Persistentie

```bash
# For in-memory (demos/testing)
# Already included in base package

# For file-based storage
# Already included in base package

# For PostgreSQL
dotnet add package mostlylucid.Markdig.FetchExtension.Postgres

# For SQLite
dotnet add package mostlylucid.Markdig.FetchExtension.Sqlite

# For SQL Server
dotnet add package mostlylucid.Markdig.FetchExtension.SqlServer
```

## Voor single-server implementaties, file-based storage werkt geweldig:

Belangrijkste punten:`Program.cs`:

```csharp
using Mostlylucid.Markdig.FetchExtension;

var builder = WebApplication.CreateBuilder(args);

// Option 1: In-Memory (simplest)
builder.Services.AddInMemoryMarkdownFetch();

// Option 2: File-Based (persists across restarts)
builder.Services.AddFileBasedMarkdownFetch("./markdown-cache");

// Option 3: PostgreSQL (multi-server)
builder.Services.AddPostgresMarkdownFetch(
    builder.Configuration.GetConnectionString("MarkdownCache"));

// Option 4: SQLite (single server with DB)
builder.Services.AddSqliteMarkdownFetch("Data Source=markdown-cache.db");

// Option 5: SQL Server (enterprise)
builder.Services.AddSqlServerMarkdownFetch(
    builder.Configuration.GetConnectionString("MarkdownCache"));

var app = builder.Build();

// If using database storage, ensure schema exists
if (app.Environment.IsDevelopment())
{
    app.Services.EnsureMarkdownCacheDatabase();
}

// Configure the extension with your service provider
FetchMarkdownExtension.ConfigureServiceProvider(app.Services);

app.Run();
```

## Gebruik

voor thread-veilige bestandstoegang

```csharp
public class MarkdownRenderingService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly MarkdownFetchPreprocessor _preprocessor;
    private readonly MarkdownPipeline _pipeline;

    public MarkdownRenderingService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _preprocessor = new MarkdownFetchPreprocessor(serviceProvider);

        _pipeline = new MarkdownPipelineBuilder()
            .UseAdvancedExtensions()
            .UseSyntaxHighlighting()
            .UseToc()  // Add TOC support for [TOC] markers
            .UseYourCustomExtensions()
            .Build();
    }

    public string RenderMarkdown(string markdown)
    {
        // Step 1: Preprocess to handle fetch tags
        var processed = _preprocessor.Preprocess(markdown);

        // Step 2: Run through your normal Markdig pipeline
        return Markdown.ToHtml(processed, _pipeline);
    }
}
```

Hashes de URL + blog post ID om veilige bestandsnamen te maken

1. Gebruikt bestandsmodificatie tijd om versheid te bepalen`<fetch>`Persistent over toepassing herstarten
2. Zelfde oud-while-revalidate patroon
3. Database-opslag: Productie-klaar
4. Voor productie-implementaties, met name multi-server opstellingen, wilt u een gedeelde cache.

# Dat is waar de database providers komen in:

## Het databaseschema is eenvoudig:

In een multi-server implementatie, dit geeft je cache consistentie over alle instanties:**Alle servers delen dezelfde cache.**Wanneer Server 1 een README haalt, profiteren Servers 2 en 3 onmiddellijk van die gecachede inhoud.

**De extensie instellen**

Aan de slag gaan is eenvoudig.`[TOC]`Installeer eerst het basispakket:

```markdown
# My Document

[TOC]

# Introduction
Content here...

# Getting Started
More content...

## Installation
Details...
```

Kies vervolgens uw opslagprovider:

```html
<nav class="ml_toc" aria-label="Table of Contents">
  <ul>
    <li><a href="#introduction">Introduction</a></li>
    <li><a href="#getting-started">Getting Started</a>
      <ul>
        <li><a href="#installation">Installation</a></li>
      </ul>
    </li>
  </ul>
</nav>
```

**Configuratie in ASP.NET Core**

In uw

```markdown
[TOC cssclass="my-custom-toc"]
```

Integratie met uw Markdown Rendering

```html
<nav class="my-custom-toc" aria-label="Table of Contents">
  <!-- TOC content -->
</nav>
```

**De sleutel is de voorbewerkingsstap.**

1. **Zo integreer ik het in mijn blog:**De stroom is:

2. **Uw markdown bevat**tags
   
   - Preprocessor lost ze op tot werkelijke markdown`id="getting-started"`
   - De gecombineerde afwaardering gaat via Markdig`id="api-reference"`

3. **Alles krijgt uw aangepaste extensies, styling, enz.**Geavanceerde functies`ul/li`Inhoudsopgave

**Het pakket bevat nu een**

apart

```csharp
var pipeline = new MarkdownPipelineBuilder()
    .UseAdvancedExtensions()
    .UseToc()  // Add TOC support - position in pipeline doesn't matter!
    .Use<YourOtherExtensions>()
    .Build();
```

**Inhoudsopgave (TOC) uitbreiding!**Terwijl het is verpakt naast de fetch extensie, het is volledig onafhankelijk en kan worden gebruikt op zijn eigen.**U kunt automatisch een klikbare inhoudsopgave genereren uit de rubrieken van uw document.**Basisgebruik:

- Simpelweg toevoegen
- overal in uw markdown:

Dit genereert een geneste lijst van alle rubrieken met ankerverbindingen:`.UseToc()`Aangepaste CSS-klassen:

**U kunt een aangepaste CSS-klasse opgeven voor styling:**Dit rendert met uw aangepaste klasse:

- Hoe het werkt:
- Autodetectie
- : De TOC detecteert automatisch het minimale koersniveau in uw document en past dienovereenkomstig aan.

**Als uw document begint met H2, behandelt de TOC H2 als het hoogste niveau.**ID-generatie`[TOC]`: Headings worden automatisch ID's gegeven voor het verankeren van:`.UseToc()`"Aan de slag" →

## "API-referentie" →

Geneste structuur

```markdown
<fetch markdownurl="https://raw.githubusercontent.com/user/repo/main/docs/README.md"
       pollfrequency="24"
       transformlinks="true" disable="true"/>
```

: De renderer bouwt een goed geneste

- `./CONTRIBUTING.md` → `https://github.com/user/repo/blob/main/docs/CONTRIBUTING.md`
- `../images/logo.png` → `https://github.com/user/repo/blob/main/images/logo.png`
- structuur die uw documenthiërarchie weerspiegelt.

TOC-ondersteuning inschakelen:

```csharp
public class MarkdownLinkRewriter
{
    public static string RewriteLinks(string markdown, string sourceUrl)
    {
        var document = Markdown.Parse(markdown);
        var baseUri = GetBaseUri(sourceUrl);

        foreach (var link in document.Descendants<LinkInline>())
        {
            if (IsRelativeLink(link.Url))
            {
                link.Url = ResolveRelativeLink(baseUri, link.Url);
            }
        }

        using var writer = new StringWriter();
        var renderer = new NormalizeRenderer(writer);
        renderer.Render(document);
        return writer.ToString();
    }

    private static bool IsRelativeLink(string url)
    {
        if (string.IsNullOrEmpty(url)) return false;
        if (url.StartsWith("http://") || url.StartsWith("https://")) return false;
        if (url.StartsWith("#")) return false;  // Anchor
        if (url.StartsWith("mailto:")) return false;
        return true;
    }
}
```

## Voeg bij het configureren van uw Markdig-pijpleiding de TOC-extensie toe:

Pijplijnpositie:

```markdown
<fetch markdownurl="https://api.example.com/status.md"
       pollfrequency="1"
       showsummary="true" disable="true"/>
```

In tegenstelling tot sommige Markdig extensies, de TOC extensie

> _maakt het niet uit waar je het toevoegt[in de pijplijn.](https://api.example.com/status.md)De extensie automatisch:_

Voegt zijn parser toe aan het begin van de parserlijst (positie 0)

```markdown
<fetch markdownurl="https://example.com/docs.md"
       pollfrequency="24"
       showsummary="true"
       summarytemplate="Last updated: {retrieved:long} | Status: {status} | Next refresh: {nextrefresh:relative}" disable="true"/>
```

Renders nadat alle ontleden is voltooid, het verzamelen van rubrieken uit het hele document

> Dus je kunt toevoegen

overal - begin, midden, of einde van uw pijpleiding configuratie.

- `{retrieved:format}`Belangrijk:
- `{age}`De TOC extensie is volledig onafhankelijk van de fetch extensie.
- `{url}`Ze zijn gewoon samen verpakt voor het gemak.
- `{nextrefresh:format}`U kunt:
- `{pollfrequency}`TOC gebruiken zonder ophalen
- `{status}`Ophalen zonder TOC gebruiken

## Gebruik beide samen

Opmerking:`disable="true"`De TOC-markering werkt zowel in uw belangrijkste markdown-bestanden als in opgehaalde inhoud op afstand.

```markdown
<!-- This will be processed and fetch content -->
<fetch markdownurl="https://example.com/README.md" pollfrequency="24"/>

<!-- This will NOT be processed - useful for documentation -->
<fetch markdownurl="https://example.com/README.md" pollfrequency="24" disable="true"/>
```

Als u een README van GitHub die bevat ophalen

- , het zal automatisch een inhoudsopgave genereren uit de rubrieken van dat document (aangenomen dat u hebt toegevoegd
- naar uw pijpleiding).
- Transformatie koppelen

Bij het ophalen van remote markdown (vooral van GitHub), relatieve links breken.`<fetch>`De extensie kan ze automatisch herschrijven:`<fetch-summary>`Dit transformeert:

```markdown
<fetch-summary url="https://example.com/api/status.md" disable="true"/>
```

## Bewaart absolute URL's en ankers

De implementatie maakt gebruik van de Markdig AST om links te herschrijven:

```csharp
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddPostgresMarkdownFetch(connectionString);

        var sp = services.BuildServiceProvider();
        var eventPublisher = sp.GetRequiredService<IMarkdownFetchEventPublisher>();

        // Subscribe to events
        eventPublisher.FetchBeginning += (sender, args) =>
        {
            Console.WriteLine($"Fetching {args.Url}...");
        };

        eventPublisher.FetchCompleted += (sender, args) =>
        {
            var source = args.WasCached ? "cache" : "remote";
            Console.WriteLine($"Fetched {args.Url} from {source} in {args.Duration.TotalMilliseconds}ms");
        };

        eventPublisher.FetchFailed += (sender, args) =>
        {
            Console.WriteLine($"Failed to fetch {args.Url}: {args.ErrorMessage}");
        };
    }
}
```

Overzicht van metadata ophalen

```mermaid
sequenceDiagram
    participant MD as Markdown Processor
    participant EP as Event Publisher
    participant FS as Fetch Service
    participant ST as Storage Backend
    participant L as Your Listeners

    MD->>EP: FetchBeginning
    EP->>L: Notify FetchBeginning
    EP->>FS: FetchMarkdownAsync(url)
    FS->>ST: Check Cache
    alt Cache Fresh
        ST-->>FS: Cached Content
        FS->>EP: FetchCompleted (cached=true)
    else Cache Stale/Missing
        FS->>FS: HTTP GET
        alt Success
            FS->>ST: Update Cache
            ST-->>FS: OK
            FS->>EP: FetchCompleted (cached=false)
        else Failure
            FS->>EP: FetchFailed
            EP->>L: Notify FetchFailed
        end
    end
    EP->>L: Notify FetchCompleted
    EP->>L: Notify ContentUpdated
    FS-->>MD: MarkdownFetchResult

    Note over L: Listeners can be: Logging, Metrics, Telemetry, Webhooks
```

# U kunt de lezers tonen wanneer de inhoud voor het laatst is opgehaald:

Dit rendert met een voettekst:

```mermaid
stateDiagram-v2
    [*] --> CheckCache: Fetch Request

    CheckCache --> Fresh: Cache exists & age < pollFrequency
    CheckCache --> Stale: Cache exists & age >= pollFrequency
    CheckCache --> Missing: No cache entry

    Fresh --> ReturnCached: Return cached content
    ReturnCached --> [*]

    Stale --> FetchRemote: Attempt HTTP GET
    Missing --> FetchRemote: Attempt HTTP GET

    FetchRemote --> UpdateCache: Success
    FetchRemote --> HasStale: Failure

    UpdateCache --> ReturnFresh: Return new content
    ReturnFresh --> [*]

    HasStale --> ReturnStale: Return stale cache
    HasStale --> ReturnError: No cache available

    ReturnStale --> [*]
    ReturnError --> [*]

    note right of Fresh
        pollFrequency = 0
        means always stale
    end note

    note right of HasStale
        Stale-while-revalidate
        pattern ensures uptime
    end note
```

Inhoud opgehaald van

1. **https://api.example.com/status.md**op 06 jan 2025 (2 uur geleden)
2. **Of pas het sjabloon aan:**Uitvoer:~~~~
3. **Laatst bijgewerkt: 06 januari 2025 14:30**Beschikbare plaatshouders:
4. **- Laatste ophalen datum/tijd**- Menselijk leesbare tijd sinds het halen

- Bron URL**- Wanneer de inhoud wordt ververst**- Cache duur in uren

# - Cachestatus (vers/gecached/verhaal)

Verwerking voor documentatie uitschakelen

```csharp
public class MarkdownController : Controller
{
    private readonly IMarkdownFetchService _fetchService;

    public async Task<IActionResult> InvalidateCache(string url)
    {
        var removed = await _fetchService.RemoveCachedMarkdownAsync(url);

        if (removed)
        {
            return Ok(new { message = "Cache invalidated" });
        }

        return NotFound(new { message = "No cache entry found" });
    }
}
```

Bij het schrijven van documentatie over de fetch extensie (zoals dit artikel!), heb je een manier nodig om de tags te tonen zonder dat ze worden verwerkt.

```csharp
// GitHub webhook notifies of README update
app.MapPost("/webhooks/github", async (
    GitHubWebhookPayload payload,
    IMarkdownFetchService fetchService) =>
{
    if (payload.Repository?.FullName == "user/repo" &&
        payload.Commits?.Any(c => c.Modified?.Contains("README.md") == true) == true)
    {
        var url = "https://raw.githubusercontent.com/user/repo/main/README.md";
        await fetchService.RemoveCachedMarkdownAsync(url);

        return Results.Ok(new { message = "Cache invalidated" });
    }

    return Results.Ok(new { message = "No action needed" });
});
```

# Gebruik de

attribuut:

```csharp
public class MarkdownFetchServiceTests
{
    [Fact]
    public async Task FetchMarkdownAsync_CachesContent()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddInMemoryMarkdownFetch();
        var sp = services.BuildServiceProvider();

        var fetchService = sp.GetRequiredService<IMarkdownFetchService>();
        var url = "https://raw.githubusercontent.com/user/repo/main/README.md";

        // Act - First fetch (from network)
        var result1 = await fetchService.FetchMarkdownAsync(url, 24, 0);

        // Act - Second fetch (from cache)
        var result2 = await fetchService.FetchMarkdownAsync(url, 24, 0);

        // Assert
        Assert.True(result1.Success);
        Assert.True(result2.Success);
        Assert.Equal(result1.Content, result2.Content);
    }

    [Fact]
    public async Task FetchMarkdownAsync_ReturnsStaleOnFailure()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddInMemoryMarkdownFetch();
        var sp = services.BuildServiceProvider();

        var fetchService = sp.GetRequiredService<IMarkdownFetchService>();
        var url = "https://httpstat.us/200?sleep=100";

        // Act - First fetch succeeds
        var result1 = await fetchService.FetchMarkdownAsync(url, 0, 0);

        // Change URL to fail
        var badUrl = "https://httpstat.us/500";

        // Act - Second fetch fails, should return stale
        var result2 = await fetchService.FetchMarkdownAsync(badUrl, 0, 0);

        // Assert
        Assert.True(result1.Success);
        // Even though fetch failed, we return success with stale content
        Assert.True(result2.Success);
    }
}
```

# De uitgeschakelde tag blijft in de markdown as-is, perfect voor:

Documenten schrijven over de extensie zelf

1. **Voorbeelden maken in tutorials**Syntaxis van tag tonen zonder ophalen te activeren`ConcurrentDictionary`Dit werkt voor beide
2. **en**tags:
3. **Gebeurtenissysteem voor monitoring**De extensie publiceert evenementen voor alle ophalen operaties:
4. **Dit maakt het eenvoudig om te integreren met Application Insights, Prometheus, of uw logging infrastructuur:**Caching Strategie in detail`IHttpClientFactory`Het caching gedrag volgt een state machine patroon:
5. **De belangrijkste inzichten hier:**Verse cache

- Return onmiddelijk, geen netwerk hit

- Stamcache
- - Probeer fris te halen, maar val terug naar oud als apporteren mislukt
- Ontbrekende cache
- - Moet een foutmelding ophalen of retourneren

# Nul Pollfrequentie

- Altijd vers ophalen (handig voor het testen)

```yaml
name: Publish Markdig.FetchExtension

on:
  push:
    tags:
      - 'fetchextension-v*.*.*'

permissions:
  id-token: write
  contents: read

jobs:
  build-and-publish:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '9.0.x'

    - name: Extract version from tag
      id: get_version
      run: |
        TAG=${GITHUB_REF#refs/tags/fetchextension-v}
        echo "VERSION=$TAG" >> $GITHUB_OUTPUT

    - name: Build
      run: dotnet build Mostlylucid.Markdig.FetchExtension/Mostlylucid.Markdig.FetchExtension.csproj --configuration Release -p:Version=${{ steps.get_version.outputs.VERSION }}

    - name: Pack
      run: dotnet pack Mostlylucid.Markdig.FetchExtension/Mostlylucid.Markdig.FetchExtension.csproj --configuration Release --no-build -p:PackageVersion=${{ steps.get_version.outputs.VERSION }} --output ./artifacts

    - name: Login to NuGet (OIDC)
      id: nuget_login
      uses: NuGet/login@v1
      with:
        user: 'mostlylucid'

    - name: Publish to NuGet
      run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ steps.nuget_login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
```

Dit patroon heet

# oud-while-revalidate

En het is uitstekend voor betrouwbaarheid.

```markdown
# My NuGet Package Documentation

Here's the official README from GitHub:

<fetch markdownurl="https://raw.githubusercontent.com/scottgal/mostlylucidweb/main/Umami.Net/README.md"
       pollfrequency="24"
       transformlinks="true"
       showsummary="true"
       summarytemplate="*Fetched {age} from GitHub*" disable="true"/>

# Installation

The package is available on NuGet...
```

Zelfs als je bron naar beneden gaat, blijft je site cache inhoud serveren.

# Cache verwijderen en beheren

Soms moet je de cache handmatig ongeldig maken:

1. **Of via webhooks wanneer de inhoud verandert:**Testen van de uitbreiding
2. **De uitbreiding omvat uitgebreide tests.**Hier is hoe ik ze structureer:
3. **Prestatieoverwegingen**De uitbreiding is ontworpen voor prestaties:
4. **Gelijktijdig woordenboek**- In-geheugen implementatie toepassingen
5. **voor draadveilige toegang**SemaforeSlim
6. **- File-based maakt gebruik van async vergrendeling om racevoorwaarden te voorkomen**Database-indexen

- Alle database providers hebben goede indexen op cache sleutels

- [HTTP-client pooling](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
- [- Gebruikt](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Postgres)
- [voor efficiënt hergebruik van verbindingen](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Sqlite)
- [Async all the way](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.SqlServer)

- Geen telefoontjes blokkeren, alles is async.[Typische prestatienummers op mijn thuisserver:](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension)

Cache hit: < 1ms