# Att bygga en fjärrmarkering Fetcher för Markdig

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

# Inledning

En av de utmaningar jag stod inför när jag byggde den här bloggen var hur man effektivt inkluderar externt markdown-innehåll utan att manuellt kopiera och klistra in det överallt.

Jag ville hämta README-filer från mina GitHub-arkiv, inkludera dokumentation från andra projekt och hålla allt automatiskt synkroniserat.`Mostlylucid.Markdig.FetchExtension`Vad är lösningen?

> En anpassad Markdig förlängning som hämtar fjärrmarkering vid renderingstid och caches det intelligent.

> I det här inlägget går jag igenom hur jag byggde

> **- en komplett lösning för att hämta och cacha fjärrmarkdown-innehåll med stöd för flera lagringsbackends, automatiska röstningar och ett gammalt cachemönster.**OBS: Detta är fortfarande prerelease men jag ville få ut det där.`disable="true"`Ha kul, men det kanske inte fungerar än.

> **Denna artikel är AI genererad - med hjälp av klaude kod som också hjälpte mig att bygga funktionen.**UPPDATERING`[TOC]`Tillagd:

parameter så att vi nu kan demo taggarna ordentligt utan att de bearbetas![UPPDATERING (nov 7, 2025)](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension).

[![: Tillagd innehållsförteckning (TOC) generation funktion!](https://img.shields.io/nuget/v/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
[![Användning](https://img.shields.io/nuget/dt/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)

[TOC]

# i din markering för att automatiskt generera en klickbar innehållsförteckning från dokumentrubriker.

Se källan till den här

1. **på GitHub för denna webbplats**Hämta
2. **Hämta**Varför bygga upp detta?
3. **Innan du dyker in i de tekniska detaljerna, låt mig förklara problemet.**Jag har flera scenarier där jag behöver inkludera externt påslagsinnehåll:
4. **Förpackningsredaktioner**: När jag skriver om ett NuGet paket Jag har publicerat, Jag vill inkludera sin README direkt från GitHub

API-dokumentation

- : Externa API-dokument som ändras ofta behöver stanna i synk
- Delat innehåll
- : Dokumentation som bor i ett arkiv men som behöver visas på flera platser
- Prestanda

: Jag vill inte hämta detta innehåll på varje sida ladda - som skulle vara långsam och slösaktig

# Det naiva tillvägagångssättet skulle vara att använda en HTTP-klient för att hämta markdown när du behöver det.

Men det är problematiskt:

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

```

Varje begäran träffar fjärrservern**Nätverkslatens påverkar sidbelastningstider**Inget stöd offline

1. Ingen hantering av övergående fel`<fetch>`Jag behövde något smartare: hämta en gång, cache intelligent, uppdatera automatiskt, och hantera misslyckanden graciöst.
2. Översikt över arkitekturen
3. Förlängningen följer en förbearbetningsmetod snarare än att vara en del av Markdig-parseringsledningen.
4. Detta är avgörande eftersom det innebär hämtade innehåll strömmar genom hela Markdig pipeline, få alla dina anpassade tillägg, syntax highlighting och styling.

Den viktigaste insikten här är

# Förbehandling



```markdown
# My Documentation

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

Innan ni slår till mot Markdig pipeline, vi:

- Sök efter
- taggar
- Lös upp innehållet (från cache eller fjärr)
- Ersätt taggarna med faktisk markering

# Låt då Markdig bearbeta allt tillsammans

Detta säkerställer konsekvens - alla markeringar får samma behandling oavsett källa.**Den grundläggande syntaxen**Att använda förlängningen är enkelt.

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

```

## I din markering:

Nu räcker det!`IMarkdownFetchService`:

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

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

Förlängningen kommer att

## Hämta README från GitHub

Cache det i 24 timmar`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}";
}
```

Returnera innehåll i cache vid efterföljande förfrågningar

1. Uppdatera automatiskt när cachetiden går ut
2. Arkitektur för lagringsleverantör
3. En av de designprinciper jag följde var
4. Flexibilitet
5. 
6. Olika tillämpningar har olika behov.
7. En liten demoapp behöver inte PostgreSQL, men en produktion med flera servrar gör det.

Så jag byggde en insticksbar lagringsarkitektur:**Kärngränssnittet**Allt implementeras

## Enkelt och rent.

Varje genomförande hanterar lagring på sitt eget sätt, men gränssnittet förblir konsekvent.

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

Lagring i minne: Perfekt för demos

1. Den enklaste användningen av genomförandet`SemaphoreSlim`Som du kan se gör detta följande:
2. Skapar en cache- nyckel från webbadressen och blogginlägget ID
3. Kontrollerar om vi har cachet innehåll och om det är färskt
4. Om cache är färsk, returnerar den omedelbart
5. Om det är gammalt, försök att hämta färskt innehåll

## Vid framgång, uppdaterar cache

Vid fel med cachet innehåll, returnerar gamla cache (tale-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;
    }
}
```

Vid fel utan cache, returnerar fel

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

Detta mönster -

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

```

Slumpmässig förlängning

# - är avgörande för pålitligheten.

Även om GitHub är nere, din webbplats fortsätter att arbeta med cachad innehåll.

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

Filbaserad lagring: Enkel varaktighet

```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 distributioner av en enda server fungerar filbaserad lagring bra:

Viktiga punkter här:`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();
```

## Användning

för trådsäker filåtkomst

```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 URL + blogg inlägg ID för att skapa säkra filnamn

1. Använder filändringstid för att bestämma färskhet`<fetch>`Persisterar över program omstarter
2. Samma gamla och förnyade mönster
3. Databaslagring: Produktionsklar
4. För produktionsinstallationer, särskilt konfigurationer med flera servrar, vill du ha en delad cache.

# Det är där databasleverantörerna kommer in:

## Databasschemat är enkelt:

I en multi-server distribution, ger detta dig cachekonsistens över alla instanser:**Alla servrar delar samma cache.**När Server 1 hämtar en README, Servers 2 och 3 omedelbart dra nytta av det cachade innehållet.

**Ställa in utbyggnaden**

Det är enkelt att komma igång.`[TOC]`Installera först baspaketet:

```markdown
# My Document

[TOC]

# Introduction
Content here...

# Getting Started
More content...

## Installation
Details...
```

Välj sedan din lagringsleverantör:

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

**Inställning i ASP.NET Core**

I din

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

Integration med din markeringsåtergivning

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

**Nyckeln är förbehandlingssteget.**

1. **Så här integrerar jag det i min blogg:**Flödet är:

2. **Din markering innehåller**taggar
   
   - Preprocessorn löser dem till faktisk markering`id="getting-started"`
   - Den kombinerade markeringen går genom Markdig`id="api-reference"`

3. **Allt får dina egna tillägg, styling, etc.**Avancerade funktioner`ul/li`Innehållsförteckning Generering

**Paketet innehåller nu en**

separat

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

**Innehållsförteckning (TOC) förlängning!**Medan den är förpackad vid sidan av hämta förlängningen, är den helt oberoende och kan användas på egen hand.**Du kan automatiskt generera en klickbar innehållsförteckning från dokumentets rubriker.**Grundläggande användning:

- Lägg helt enkelt till
- Var som helst i din markering:

Detta ger upphov till en lista över alla rubriker med ankarlänkar:`.UseToc()`Anpassade CSS-klasser:

**Du kan ange en anpassad CSS-klass för styling:**Detta gör med din anpassade klass:

- Hur det fungerar:
- Automatisk upptäckt
- : TOC upptäcker automatiskt den lägsta kursnivån i ditt dokument och justerar därefter.

**Om ditt dokument börjar med H2 behandlar TOC H2 som den översta nivån.**ID-generering`[TOC]`: Rubriker ges automatiskt ID för länkning av ankar:`.UseToc()`"Komma igång" →

## "API-referens" →

Inhägnad struktur

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

: Rendern bygger ett ordentligt näste

- `./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 som återspeglar din dokumenthierarki.

Aktivera TOC-stöd:

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

## När du konfigurerar din Markdig-rörledning, lägg till TOC-tillägget:

Rörledningsläge:

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

Till skillnad från vissa Markdig tillägg, TOC förlängning

> _bryr sig inte om var du lägger till det[- Jag är på gång.](https://api.example.com/status.md)Förlängningen automatiskt:_

Lägger till sin parser i början av parserlistan (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 efter all tolkning är komplett, samla rubriker från hela dokumentet

> Så du kan lägga till

var som helst - början, mitten, eller slutet av din rörledning konfiguration.

- `{retrieved:format}`Viktigt:
- `{age}`TOC-tillägget är helt oberoende av hämta tillägg.
- `{url}`De är packade ihop för bekvämlighetens skull.
- `{nextrefresh:format}`Du kan:
- `{pollfrequency}`Använd TOC utan att hämta
- `{status}`Använd hämta utan TOC

## Använd båda tillsammans

Anmärkning:`disable="true"`TOC-markören fungerar både i dina huvudfiler och i hämtat fjärrinnehåll.

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

Om du hämtar en README från GitHub som innehåller

- , Det kommer automatiskt att generera en innehållsförteckning från dokumentets rubriker (förutsatt att du har lagt till
- till din pipeline).
- Länkomvandling

När du hämtar fjärrmarkering (särskilt från GitHub), relativa länkar bryta.`<fetch>`Förlängningen kan automatiskt skriva om dem:`<fetch-summary>`Detta förändrar:

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

## Bevarar absoluta webbadresser och ankare

Implementeringen använder Markdig AST för att skriva om länkar:

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

Hämta metadatasammanfattning

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

# Du kan visa läsarna när innehållet senast hämtades:

Detta gör med en sidfot:

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

Innehåll hämtat från

1. **I enlighet med artikel 108.3 i EUF-fördraget ska medlemsstaterna säkerställa att kommissionen, i enlighet med artikel 108.3 i fördraget om Europeiska unionens funktionssätt, har befogenhet att anta delegerade akter i enlighet med artikel 290 i EUF-fördraget för att se till att bestämmelserna i denna förordning efterlevs.**den 06 jan 2025 (2 timmar sedan)
2. **Eller anpassa mallen:**utmatning: ~ ~ ~ ~ ~
3. **Senast uppdaterad: 06 januari 2025 14:30 .. Status: cachad .. Nästa uppdatering: på 22 timmar**Tillgängliga platshållare:
4. **- Senaste hämta datum / tid**- Mänsklig läsbar tid sedan hämta

- Källans webbadress**- När innehållet kommer att uppdateras**- Cache varaktighet i timmar

# - Cache-status (färsk/kaklad/stav)

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

När du skriver dokumentation om hämta tillägg (som den här artikeln!), behöver du ett sätt att visa taggarna utan att de behandlas.

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

# Använd

egenskap:

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

# Den inaktiverade taggen finns kvar i markeringen som är, perfekt för:

Skriva dokumentation om själva förlängningen

1. **Skapa exempel i handledningar**Visar taggsyntax utan att starta hämtare`ConcurrentDictionary`Detta fungerar för båda
2. **och**Taggar:
3. **Händelsesystem för övervakning**Förlängningen publicerar händelser för alla hämta åtgärder:
4. **Detta gör det enkelt att integrera med Application Insights, Prometheus eller din logginfrastruktur:**Caching strategi i detalj`IHttpClientFactory`Caching beteende följer en tillståndsmaskin mönster:
5. **De viktigaste insikterna här:**Färsk cache

- Återvänd omedelbart, ingen nätverksträff

- Stales kackerlacka
- - Försök att hämta färska, men falla tillbaka till gamla om hämta misslyckas
- Saknar cache
- - Måste hämta eller returnera fel

# Nollpoleringsfrekvens

- Hämta alltid färska (användbart för testning)

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

Detta mönster kallas

# Slumpmässig förlängning

Och det är utmärkt för pålitlighet.

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

Även om din källa går ner, din webbplats fortsätter att tjäna cachad innehåll.

# Borttagning och hantering av cache

Ibland måste du manuellt ogiltigförklara cache:

1. **Eller via webhooks när innehållet ändras:**Pröva utbyggnaden
2. **Tillägget omfattar omfattande tester.**Så här strukturerar jag dem:
3. **Prestandaöverväganden**Förlängningen är utformad för prestanda:
4. **Samtidigt uppslagsverk**- Användning inom minnet av genomförandet
5. **för trådsäker åtkomst**SemaforeSlim Ordförande
6. **- Filbaserad använder async-låsning för att förhindra rasförhållanden**Databasindex

- Alla databasleverantörer har korrekta index på cachenycklar

- [Poolning av HTTP- klient](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
- [- Använder](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Postgres)
- [för effektiv återanvändning av anslutning](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Sqlite)
- [Asynkronisera hela vägen](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.SqlServer)

- Inga blockerande samtal, allt är async[Typiska prestandanummer på min hemserver:](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension)

Cache träff: < 1 ms