# Aufbau eines Remote Markdown Fetchers für Markdig

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

# Einleitung

Eine der Herausforderungen, vor denen ich beim Aufbau dieses Blogs stand, war, wie man effizient externe Markdown-Inhalte ohne manuelles Kopieren und Einfügen überall einschließt.

Ich wollte README-Dateien aus meinen GitHub-Repositories holen, Dokumentationen aus anderen Projekten enthalten und alles automatisch synchronisiert halten.`Mostlylucid.Markdig.FetchExtension`Die Lösung?

> Eine benutzerdefinierte Markdig-Erweiterung, die Remote-Markdown zur Renderzeit holt und sie intelligent zwischenspeichert.

> In diesem Beitrag begleite ich dich, wie ich gebaut habe.

> **- eine Komplettlösung zum Abrufen und Caching von Remote-Markdown-Inhalten mit Unterstützung für mehrere Speicher-Backends, automatisches Polling und einem veralteten Caching-Muster.**HINWEIS: Das ist immer noch Vorveröffentlichung, aber ich wollte es da raus bekommen.`disable="true"`Viel Spaß, aber es könnte noch nicht funktionieren.

> **Dieser Artikel ist KI generiert - mit claude Code, der auch mir half, die Funktion zu bauen.**AKTUELLE ENTWICKLUNG`[TOC]`: Hinzugefügt

Parameter so können wir nun die Tags richtig vorführen, ohne dass sie verarbeitet werden![UPDATE (7. November 2025)](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension).

[![: Inhaltsverzeichnis (TOC)-Generierung hinzugefügt!](https://img.shields.io/nuget/v/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
[![Verwendung](https://img.shields.io/nuget/dt/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)

[TOC]

# in Ihrem Markdown, um automatisch ein anklickbares Inhaltsverzeichnis aus Dokumentüberschriften zu generieren.

Sehen Sie die Quelle dafür hier

1. **auf der GitHub für diese Seite**NuGet
2. **NuGet**Warum das bauen?
3. **Bevor ich in die technischen Details eintauche, möchte ich das Problem erklären.**Ich habe mehrere Szenarien, in denen ich externe Markdown-Inhalte hinzufügen muss:
4. **README-Pakete**: Wenn ich über ein NuGet-Paket schreibe, das ich veröffentlicht habe, möchte ich seine README direkt von GitHub enthalten

API-Dokumentation

- : Externe API-Dokumente, die sich häufig ändern müssen, um synchron zu bleiben
- Gemeinsame Inhalte
- : Dokumentation, die in einem Repository lebt, aber an mehreren Orten erscheinen muss
- Leistung

: Ich möchte diesen Inhalt nicht auf jeder Seite laden - das wäre langsam und verschwenderisch

# Der naive Ansatz wäre, einen HTTP-Client zu verwenden, um Markdown zu holen, wann immer Sie ihn brauchen.

Aber das ist 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]

```

Jede Anfrage trifft den entfernten Server**Netzwerk Latenz Auswirkungen Seite Ladezeiten**Keine Offline-Unterstützung

1. Kein Umgang mit vorübergehenden Ausfällen`<fetch>`Ich brauchte etwas Klügeres: Einmal holen, intelligent zwischenspeichern, automatisch aktualisieren und Fehler anmutig handhaben.
2. Überblick über die Architektur
3. Die Erweiterung folgt einem Vorverarbeitungsansatz, anstatt Teil der Markdig-Parsing-Pipeline zu sein.
4. Dies ist entscheidend, weil es bedeutet, gesammelte Inhalte fließt durch Ihre gesamte Markdig-Pipeline, immer alle Ihre kundenspezifischen Erweiterungen, Syntax Hervorhebung und Styling.

Die wichtigste Einsicht hier ist:

# Vorverarbeitung



```markdown
# My Documentation

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

Bevor Ihr Abschlag auf die Markdig-Pipeline trifft, haben wir:

- Suchen nach
- Tags
- Lösen Sie den Inhalt (aus Cache oder Remote)
- Ersetzen Sie die Tags durch tatsächliche Markierung

# Dann lass Markdig alles zusammen verarbeiten

Dies sorgt für Konsistenz - alle Markdown bekommt die gleiche Behandlung unabhängig von ihrer Quelle.**Die grundlegende Syntax**Die Verwendung der Erweiterung ist einfach.

```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 deinem Abspann:

Das war's!`IMarkdownFetchService`:

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

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

Die Erweiterung wird

## Holen Sie die README von GitHub

Halten Sie es 24 Stunden lang fest.`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}";
}
```

Rückgabe zwischengespeicherter Inhalte bei nachfolgenden Anfragen

1. Automatisch aktualisieren, wenn der Cache abläuft
2. Architektur des Storage Providers
3. Eines der Design-Prinzipien, denen ich folgte, war
4. Flexibilität
5. 
6. Unterschiedliche Anwendungen haben unterschiedliche Bedürfnisse.
7. Eine kleine Demo-App braucht PostgreSQL nicht, aber eine Multiserver-Produktions-Deployment tut es.

Also baute ich eine steckbare Speicherarchitektur:**Die Kernschnittstelle**Alles implementiert

## Einfach und sauber.

Jede Implementierung übernimmt die Speicherung auf eigene Weise, aber die Schnittstelle bleibt konstant.

```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-Memory Storage: Perfekt für Demos

1. Die einfachste Implementierung nutzt`SemaphoreSlim`Wie Sie sehen können, macht dies folgendes:
2. Erstellt einen Cache-Schlüssel aus der URL und Blog-Post-ID
3. Prüft, ob wir Inhalte zwischengespeichert haben und ob es frisch ist
4. Wenn der Cache frisch ist, gibt er sofort zurück
5. Versucht, frischen Inhalt zu holen.

## Beim Erfolg aktualisiert der Cache

Beim Ausfall mit zwischengespeicherten Inhalten, gibt der veraltete Cache zurück (stale-wile-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;
    }
}
```

Bei Ausfall ohne Cache, gibt Fehler zurück

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

Dieses Muster -

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

```

abgestanden-während-revalidiert

# - ist entscheidend für die Zuverlässigkeit.

Auch wenn GitHub unten ist, arbeitet Ihre Website immer mit zwischengespeicherten Inhalten.

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

Dateibasierte Speicherung: Einfache Persistenz

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

## Für Single-Server-Bereitstellungen funktioniert der dateibasierte Speicher hervorragend:

Wesentliche Punkte:`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();
```

## Verwendung

für threadsicheren Dateizugriff

```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 die URL + Blog-Post-ID, um sichere Dateinamen zu erstellen

1. Verwendet Datei Änderung Zeit, um Frische zu bestimmen`<fetch>`Persistiert über Anwendungs-Neustarts
2. Gleiches Stil-während-revalidiertes Muster
3. Datenbankspeicher: Produktionsbereit
4. Für Produktions-Deployments, insbesondere Multiserver-Setups, möchten Sie einen gemeinsamen Cache.

# Hier kommen die Datenbankanbieter ins Spiel:

## Das Datenbankschema ist einfach:

In einer Multi-Server-Bereitstellung gibt Ihnen dies die Konsistenz des Caches über alle Instanzen hinweg:**Alle Server teilen den gleichen Cache.**Wenn Server 1 eine README holt, profitieren Server 2 und 3 sofort von dem zwischengespeicherten Inhalt.

**Einrichtung der Erweiterung**

Der Einstieg ist einfach.`[TOC]`Installieren Sie zunächst das Basispaket:

```markdown
# My Document

[TOC]

# Introduction
Content here...

# Getting Started
More content...

## Installation
Details...
```

Dann wählen Sie Ihren Speicheranbieter:

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

**Konfiguration in ASP.NET Core**

In Ihrem

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

Integration mit Ihrem Markdown Rendering

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

**Der Schlüssel ist der Vorverarbeitungsschritt.**

1. **Hier ist, wie ich es in meinem Blog integrieren:**Der Fluss ist:

2. **Ihr Markdown enthält**Tags
   
   - Preprocessor löst sie auf tatsächliche Markdown`id="getting-started"`
   - Der kombinierte Abschlag geht durch Markdig`id="api-reference"`

3. **Alles bekommt Ihre kundenspezifischen Erweiterungen, Styling, etc.**Erweiterte Funktionen`ul/li`Inhaltsverzeichnis Erzeugung

**Das Paket enthält jetzt eine**

getrennt

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

**Inhaltsverzeichnis (TOC) Erweiterung!**Während es neben der fetch-Erweiterung verpackt ist, ist es völlig unabhängig und kann auf eigene Faust verwendet werden.**Sie können automatisch ein anklickbares Inhaltsverzeichnis aus den Überschriften Ihres Dokuments generieren.**Grundlegende Verwendung:

- Einfach hinzufügen
- überall in Ihrem Markdown:

Dies erzeugt eine verschachtelte Liste aller Überschriften mit Anker-Links:`.UseToc()`Benutzerdefinierte CSS-Klassen:

**Sie können eine benutzerdefinierte CSS-Klasse für Styling angeben:**Dies wird mit Ihrer benutzerdefinierten Klasse wiedergegeben:

- Wie es funktioniert:
- Automatische Erkennung
- : Der TOC erkennt automatisch die minimale Überschrift in Ihrem Dokument und passt sich entsprechend an.

**Wenn Ihr Dokument mit H2 beginnt, behandelt das TOC H2 als oberstes Level.**ID-Erstellung`[TOC]`: Überschriften werden automatisch IDs für die Ankerverknüpfung gegeben:`.UseToc()`"Erster Anfang" →

## „API-Referenz" →

Verschachtelte Struktur

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

: Der Renderer baut eine richtig verschachtelte

- `./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`
- Struktur, die Ihre Dokumenthierarchie widerspiegelt.

TOC-Unterstützung aktivieren:

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

## Wenn Sie Ihre Markdig-Pipeline konfigurieren, fügen Sie die TOC-Erweiterung hinzu:

Rohrleitungsposition:

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

Im Gegensatz zu einigen Markdig-Erweiterungen, die TOC-Erweiterung

> _ist egal, wo Sie es hinzufügen[in der Pipeline.](https://api.example.com/status.md)Die Erweiterung automatisch:_

Fügt seinen Parser am Anfang der Parserliste ein (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"/>
```

Renders, nachdem das Parsen abgeschlossen ist, sammeln Überschriften aus dem gesamten Dokument

> So können Sie hinzufügen

überall - Anfang, Mitte oder Ende Ihrer Pipeline-Konfiguration.

- `{retrieved:format}`Wichtig:
- `{age}`Die TOC-Erweiterung ist völlig unabhängig von der fetch-Erweiterung.
- `{url}`Sie sind nur zur Bequemlichkeit zusammengepackt.
- `{nextrefresh:format}`Sie können:
- `{pollfrequency}`TOC ohne Abruf verwenden
- `{status}`Holen ohne TOC verwenden

## Beides zusammen verwenden

Anmerkung:`disable="true"`Der TOC-Marker funktioniert sowohl in Ihren Haupt-Markierungsdateien als auch in heruntergeladenen Remote-Inhalten.

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

Wenn Sie eine README von GitHub erhalten, die

- , es erzeugt automatisch ein Inhaltsverzeichnis aus den Überschriften dieses Dokuments (angenommen, Sie haben hinzugefügt
- zu Ihrer Pipeline).
- Verknüpfungstransformation

Beim Abrufen von Remote-Markdown (besonders von GitHub) brechen relative Links ab.`<fetch>`Die Erweiterung kann sie automatisch umschreiben:`<fetch-summary>`Dies verwandelt sich:

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

## Bewahrt absolute URLs und Anker

Die Implementierung nutzt den Markdig AST, um Links neu zu schreiben:

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

Metadaten-Zusammenfassung abrufen

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

# Sie können den Lesern zeigen, wann der Inhalt zuletzt abgeholt wurde:

Das ergibt mit einer Fußzeile:

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

Inhalt ausgelesen

1. **https://api.example.com/status.md**am 06 Jan 2025 (vor 2 Stunden)
2. **Oder passen Sie die Vorlage an:**Ausgabe:~~~~~
3. **Letzte Aktualisierung: 06.Januar 2025 14:30-19 Status: cached-tend-Nächste Aktualisierung: in 22 Stunden**Verfügbare Platzhalter:
4. **- Letztes Abrufdatum/Zeit**- Menschlich lesbare Zeit seit dem Abrufen

- Quell-URL**- Wenn der Inhalt aktualisiert wird**- Dauer des Caches in Stunden

# - Cache-Status (frisch/cached/stale)

Deaktivieren der Verarbeitung für Dokumentation

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

Wenn Sie Dokumentation über die fetch-Erweiterung schreiben (wie dieser Artikel!), benötigen Sie einen Weg, um die Tags anzuzeigen, ohne dass sie verarbeitet werden.

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

# Verwenden Sie die

Attribut:

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

# Das deaktivierte Tag bleibt im Markdown as-is, perfekt für:

Dokumentation über die Erweiterung selbst schreiben

1. **Erstellen von Beispielen in Tutorials**Anzeige der Tag-Syntax ohne Auslösung von fetches`ConcurrentDictionary`Dies funktioniert für beide
2. **und**Tags:
3. **Veranstaltungssystem für die Überwachung**Die Erweiterung veröffentlicht Ereignisse für alle Abrufoperationen:
4. **Dadurch ist es einfach, sich in Application Insights, Prometheus oder Ihre Logging-Infrastruktur zu integrieren:**Caching-Strategie im Detail`IHttpClientFactory`Das Caching-Verhalten folgt einem Zustands-Maschinenmuster:
5. **Die wichtigsten Erkenntnisse hier:**Frischer Cache

- Sofort zurück, kein Netzwerk-Hit

- Zwischenspeicher
- - Versuchen Sie, frisch zu holen, aber fallen Sie zurück in den Stillstand, wenn holen fehlschlägt
- Fehlender Cache
- - Muss Fehler holen oder zurückgeben

# 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

- Immer frisch holen (nützlich zum 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
```

Dieses Muster wird als

# abgestanden-während-revalidiert

und es ist ausgezeichnet für Zuverlässigkeit.

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

Auch wenn Ihre Quelle nach unten geht, Ihre Website dient immer zwischengespeicherte Inhalte.

# Entfernung und Verwaltung von Caches

Manchmal müssen Sie den Cache manuell ungültig machen:

1. **Oder über Webhooks, wenn sich der Inhalt ändert:**Prüfung der Erweiterung
2. **Die Erweiterung umfasst umfassende Tests.**So strukturiere ich sie:
3. **Leistungserwägungen**Die Erweiterung ist für die Leistung ausgelegt:
4. **Gleichzeitiges Wörterbuch**- Einsatz von In-Memory-Implementierung
5. **für fadensicheren Zugriff**SemaphoreSlim
6. **- File-based verwendet async Sperrung, um Wettlaufbedingungen zu verhindern**Datenbankverzeichnisse

- Alle Datenbankanbieter haben korrekte Indexe auf Cache-Schlüsseln

- [HTTP-Client-Pooling](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
- [- Verwendung](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Postgres)
- [für effiziente Wiederverwendung der Verbindung](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Sqlite)
- [Den ganzen Weg async](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.SqlServer)

- Keine Anrufe blockieren, alles ist async[Typische Leistungszahlen auf meinem Home-Server:](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension)

Treffer im Cache: < 1ms