# Bâtir un Fetcher de marquage à distance pour Markdig

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

# Présentation

L'un des défis auxquels j'ai dû faire face lors de la construction de ce blog était de savoir comment inclure efficacement le contenu de balisage externe sans le copier et le coller manuellement partout.

Je voulais récupérer les fichiers README de mes dépôts GitHub, inclure la documentation d'autres projets, et garder tout automatiquement synchronisé.`Mostlylucid.Markdig.FetchExtension`La solution ?

> Une extension Markdig personnalisée qui récupère le balisage à distance au moment du rendu et le cache intelligemment.

> Dans ce post, je vais vous guider dans la façon dont j'ai construit

> **- une solution complète pour récupérer et mettre en cache le contenu de balisage à distance avec prise en charge de plusieurs backends de stockage, sondage automatique, et un motif de mise en cache à répétition.**REMARQUE: C'est toujours prélibéré, mais je voulais l'obtenir là-bas.`disable="true"`Amuse-toi bien, mais ça ne marchera peut-être pas encore.

> **Cet article est généré par l'IA - en utilisant le code claude qui m'a également aidé à construire la fonctionnalité.**MISE À JOUR`[TOC]`: Ajouté

paramètre donc nous pouvons maintenant démouler les tags correctement sans qu'ils soient traités![MISE À JOUR (7 novembre 2025)](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension).

[![: Ajout de la fonction de génération de la Table des matières (TOC) !](https://img.shields.io/nuget/v/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
[![Utilisation](https://img.shields.io/nuget/dt/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)

[TOC]

# dans votre balisage pour générer automatiquement une table des matières cliquable à partir des en-têtes du document.

Voir la source pour ça ici

1. **sur le GitHub pour ce site**NuGet
2. **NuGet**Pourquoi construire ça ?
3. **Avant de plonger dans les détails techniques, permettez-moi d'expliquer le problème.**J'ai plusieurs scénarios où j'ai besoin d'inclure le contenu de balisage externe:
4. **RÉPERTOIRES DES Paquets**: Quand j'écris sur un paquet NuGet que j'ai publié, je veux inclure son README directement de GitHub

Documentation de l'API

- : Les documents d'API externes qui changent fréquemment doivent rester synchronisés
- Contenu partagé
- : Documentation qui vit dans un dépôt mais doit apparaître en plusieurs endroits
- Résultats

: Je ne veux pas récupérer ce contenu sur chaque charge de page - ce serait lent et gaspillé

# L'approche naïve serait d'utiliser un client HTTP pour récupérer le balisage chaque fois que vous en avez besoin.

Mais c'est problématique.

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

```

Chaque requête touche le serveur distant**Impacts de la latence sur le réseau temps de chargement des pages**Pas de prise en charge hors ligne

1. Pas de manipulation des défaillances transitoires`<fetch>`J'avais besoin de quelque chose de plus intelligent: récupérer une fois, cacher intelligemment, rafraîchir automatiquement, et gérer les échecs gracieusement.
2. Vue d'ensemble de l'architecture
3. L'extension suit une approche de prétraitement plutôt que de faire partie du pipeline d'analyse Markdig.
4. Ceci est crucial parce que cela signifie que le contenu récupéré circule à travers tout votre pipeline Markdig, obtenant toutes vos extensions personnalisées, la mise en valeur syntaxique et le style.

Le point de vue clé ici est:

# prétraitement



```markdown
# My Documentation

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

Avant que votre balisage ne touche le pipeline Markdig, nous:

- Analyser pour
- tags
- Résoudre le contenu (à partir du cache ou de la télécommande)
- Remplacer les balises par le marquage réel

# Alors laissez Markdig tout traiter ensemble

Cela assure la cohérence - tout balisage obtient le même traitement quelle que soit sa source.**La Syntaxe de base**L'utilisation de l'extension est simple.

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

```

## Dans votre balisage :

C'est ça !`IMarkdownFetchService`:

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

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

L'extension:

## Renseignez-vous auprès de GitHub

Cache-le pendant 24 heures.`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}";
}
```

Retourner le contenu mis en cache sur les requêtes suivantes

1. Rafraîchir automatiquement lorsque le cache expire
2. Architecture du fournisseur de stockage
3. L'un des principes de conception que j'ai suivis était
4. flexibilité
5. 
6. Différentes applications ont des besoins différents.
7. Une petite application de démonstration n'a pas besoin de PostgreSQLTM, mais un déploiement de production multi-serveurs le fait.

J'ai donc construit une architecture de stockage rechargeable :**L'interface de base**Tout implémente

## Simple et propre.

Chaque implémentation gère le stockage à sa manière, mais l'interface reste cohérente.

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

Stockage en mémoire : parfait pour les démos

1. La mise en œuvre la plus simple utilise`SemaphoreSlim`Comme vous pouvez le voir, cela fait ce qui suit :
2. Crée une clé de cache à partir de l'URL et de l'ID du poste de blog
3. Vérifie si nous avons mis en cache du contenu et si c'est frais
4. Si le cache est frais, le retourne immédiatement
5. S'il reste, essayez de récupérer du contenu frais

## Sur le succès, met à jour le cache

En cas d'échec avec le contenu mis en cache, retourne le cache stale (stale-wilen-validate!)

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

En cas d'échec sans cache, retourne l'erreur

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

Ce modèle -

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

```

stale-temps-revalidate

# - est crucial pour la fiabilité.

Même si GitHub est en panne, votre site continue de travailler avec du contenu mis en cache.

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

Stockage basé sur des fichiers: Persistance simple

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

## Pour les déploiements d'un seul serveur, le stockage basé sur des fichiers fonctionne très bien:

Points clés ici:`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();
```

## Utilisations

pour l'accès aux fichiers thread-safe

```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 l'ID de poste URL + blog pour créer des noms de fichiers sûrs

1. Utilise le temps de modification du fichier pour déterminer la fraîcheur`<fetch>`Persiste à travers les redémarrages d'applications
2. Même motif de récupération de l'antécédent
3. Stockage de la base de données: Production-Ready
4. Pour les déploiements de production, en particulier les configurations multi-serveurs, vous voulez un cache partagé.

# C'est là que les fournisseurs de bases de données entrent en jeu :

## Le schéma de base de données est simple:

Dans un déploiement multi-serveurs, cela vous donne la cohérence du cache dans toutes les instances:**Tous les serveurs partagent le même cache.**Lorsque le serveur 1 récupère un README, les serveurs 2 et 3 bénéficient immédiatement de ce contenu mis en cache.

**Configuration de l'extension**

Commencer est simple.`[TOC]`D'abord, installez le paquet de base :

```markdown
# My Document

[TOC]

# Introduction
Content here...

# Getting Started
More content...

## Installation
Details...
```

Choisissez ensuite votre fournisseur de stockage:

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

**Configuration dans ASP.NET Core**

Dans votre

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

Intégration avec votre rendu Markdown

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

**La clé est l'étape du prétraitement.**

1. **Voici comment je l'intègre dans mon blog:**Le débit est:

2. **Votre balisage contient**tags
   
   - Le préprocesseur les résout au balisage réel`id="getting-started"`
   - Le balisage combiné passe par Markdig`id="api-reference"`

3. **Tout obtient vos extensions personnalisées, style, etc.**Caractéristiques avancées`ul/li`Table des matières Génération

**Le paquet comprend désormais:**

séparés

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

**Table des matières (TOC) extension!**Bien qu'il soit emballé à côté de l'extension de récupération, il est complètement indépendant et peut être utilisé seul.**Vous pouvez automatiquement générer une table des matières cliquable à partir des rubriques de votre document.**Utilisation de base :

- Ajouter simplement
- n'importe où dans votre balisage:

Cela génère une liste imbriquée de toutes les rubriques avec des liens d'ancrage:`.UseToc()`Classes CSS personnalisées :

**Vous pouvez spécifier une classe CSS personnalisée pour le style :**Cela rend avec votre classe personnalisée:

- Comment ça marche :
- Détection automatique
- : Le TOC détecte automatiquement le niveau de cap minimal dans votre document et s'ajuste en conséquence.

**Si votre document commence avec H2, le TOC traite H2 comme le niveau supérieur.**Génération d'ID`[TOC]`: Des identifiants sont automatiquement donnés pour la liaison de l'ancre :`.UseToc()`"Démarrer" →

## "Référence API" →

Structure imbriquée

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

: Le rendeur construit un correctement niché

- `./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`
- structure qui reflète la hiérarchie de votre document.

Soutien aux COT :

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

## Lors de la configuration de votre pipeline Markdig, ajoutez l'extension TOC :

Position du pipeline :

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

Contrairement à certaines extensions Markdig, l'extension TOC

> _ne se soucie pas où vous l'ajoutez[dans l'oléoduc.](https://api.example.com/status.md)L'extension automatiquement:_

Insère son analyseur au début de la liste d'analyse (position 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"/>
```

Les rendez-vous après tout l'analyse est terminée, recueillant les titres de l'ensemble du document

> Pour que vous puissiez ajouter

n'importe où - début, milieu, ou fin de votre configuration de pipeline.

- `{retrieved:format}`Important:
- `{age}`L'extension TOC est totalement indépendante de l'extension fetch.
- `{url}`Ils sont emballés ensemble pour plus de commodité.
- `{nextrefresh:format}`Vous pouvez :
- `{pollfrequency}`Utiliser TOC sans récupérer
- `{status}`Utiliser fetch sans TOC

## Utiliser les deux ensemble

Remarque:`disable="true"`Le marqueur TOC fonctionne à la fois dans vos fichiers de balisage principaux et dans le contenu distant récupéré.

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

Si vous obtenez un README de GitHub qui contient

- , il générera automatiquement une table des matières à partir des rubriques de ce document (en supposant que vous ayez ajouté
- à votre pipeline).
- Transformation des liens

Lors de la récupération du pointage à distance (surtout de GitHub), les liens relatifs se brisent.`<fetch>`L'extension peut automatiquement les réécrire :`<fetch-summary>`Cela transforme :

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

## Préserve les URLs et les ancres absolues

L'implémentation utilise le Markdig AST pour réécrire les liens :

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

Récapitulatif des métadonnées

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

# Vous pouvez montrer le lecteur lorsque le contenu a été récupéré pour la dernière fois :

Cela rend avec un pied de page:

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

Contenu récupéré à partir de

1. **https://api.example.com/status.md**le 06 janvier 2025 (il y a 2 heures)
2. **Ou personnalisez le modèle :**Sortie :~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3. **Dernière mise à jour: 06 janvier 2025 14:30.Statut: mis en cache. Prochain rafraîchissement: en 22 heures**Titulaires de places disponibles:
4. **- Date/heure de la dernière récupération**- Temps lisible par l'homme depuis la récupération

- URL source**- Quand le contenu sera rafraîchi**- Durée du cache en heures

# - Statut de cache (fresh/calcated/stale)

Désactivation du traitement pour la documentation

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

Lors de l'écriture de la documentation sur l'extension fetch (comme cet article!), vous avez besoin d'un moyen de montrer les tags sans qu'ils soient traités.

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

# Utilisez la

attribut & #160;:

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

# Le tag désactivé reste dans le balisage tel quel, parfait pour :

Ecrire la documentation sur l'extension elle-même

1. **Création d'exemples dans des tutoriels**Affichage de la syntaxe des tags sans déclencher des récupérations`ConcurrentDictionary`Cela fonctionne pour les deux
2. **et**tags & #160;:
3. **Système de surveillance des événements**L'extension publie des événements pour toutes les opérations de récupération:
4. **Il est donc facile de s'intégrer à Application Insights, Prométhée ou à votre infrastructure d'enregistrement :**Stratégie de mise en cache en détail`IHttpClientFactory`Le comportement de mise en cache suit un modèle de machine d'état:
5. **Les principaux points de vue sont les suivants :**Cache fraîche

- Retournez immédiatement, pas de contact réseau

- Cache pour stales
- - Essayez de récupérer frais, mais retombez dans l'impasse si la récupération échoue
- Cache manquante
- - Doit récupérer ou retourner une erreur

# Fréquence zéro du scrutin

- Toujours chercher frais (utile pour les tests)

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

Ce modèle s'appelle

# stale-temps-revalidate

et c'est excellent pour la fiabilité.

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

Même si votre source tombe en panne, votre site continue de servir de contenu mis en cache.

# Enlèvement et gestion des caches

Parfois, vous avez besoin d'invalider manuellement le cache :

1. **Ou via webhooks lorsque le contenu change:**Essai de l'extension
2. **L'extension comprend des tests complets.**Voici comment je les structure :
3. **Considérations de performance**L'extension est conçue pour la performance:
4. **Dictionnaire concomitant**- Utilisations d'implémentation en mémoire
5. **pour un accès sans fil**SémaphoreSlim
6. **- Utilisation de fichiers de verrouillage async pour prévenir les conditions de course**Index des bases de données

- Tous les fournisseurs de bases de données ont des index appropriés sur les clés cache

- [Mise en commun des clients HTTP](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
- [- Utilisations](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Postgres)
- [pour une réutilisation efficace de la connexion](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Sqlite)
- [Async tout le chemin](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.SqlServer)

- Pas d'appels de blocage, tout est async[Nombres de performances typiques sur mon serveur d'accueil:](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension)

Cache atteinte: < 1ms