# 为 Markdig 建立一个远程的 Markdown 抓取器

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

# 一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

我在建立这个博客时遇到的挑战之一是, 如何有效地包括外部标记内容,

我想从我的 GitHub 库中获取 README 文件, 包括来自其他项目的文件, 并自动保持一切同步 。`Mostlylucid.Markdig.FetchExtension`解决办法?

> 自定义的 Markdig 扩展名, 该扩展名在转换时间获取远程标记并智能缓存 。

> 在这个岗位上,我会给你介绍我是如何建造的

> **- 完全解决取取和缓存远程记分数内容,支持多个存储后端、自动投票以及一个停滞但重置的缓存模式。**注意:这仍然是提前释放的, 但我想把它弄出去。`disable="true"`玩得开心点,但可能还不行

> **这篇文章是由人工智能生成的, 使用 cluude 代码, 也帮助我建立这个功能。**最新更新`[TOC]`:增加数

参数,所以我们现在可以正确演示标记,而不处理它们![更新(2025年11月7日)](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension).

[![添加目录(TOC)生成功能!](https://img.shields.io/nuget/v/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
[![使用使用](https://img.shields.io/nuget/dt/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)

[TOC]

# 在标记下自动从文档标题中生成可点击的目录。

找找源头

1. **在吉特胡卜山上为这个站点**N Nuget 元数
2. **N Nuget 元数**为什么要建这个?
3. **在潜入技术细节之前 让我解释一下问题**我有一些需要包括外部减价内容的假想:
4. **软件包 REEADME 软件包**: 当我写一个我出版的NuGet包包时, 我想直接从 GitHub 中加入它的 README

API 文件

- :经常变化的外部 API 文档需要保持同步
- 共享内容
- : 生活在一个存储库中但需要出现在多个地方的文件
- 业绩 业绩业绩 业绩业绩

:我不想在每页的页数中 获取这些内容 - 那会慢而浪费

# 最天真的做法是使用 HTTP 客户端在需要时获得标记。

但这有问题:

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

```

每个请求都点击远程服务器**网络延迟影响页面负载时间**没有离线支持

1. 没有处理短暂故障`<fetch>`我需要更聪明的东西: 去拿一次, 智能缓存, 自动刷新, 并优雅地处理失败。
2. 建筑结构概览
3. 延长期采用预处理办法,而不是作为Markdig挖井管道的一部分。
4. 这至关重要,因为它意味着通过你整个马克迪的管道 获取内容流, 获取你所有的定制扩展, 语法加亮, 和外观。

这里的关键见解是

# 预处理预处理



```markdown
# My Documentation

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

在你到达马克迪输油管之前,我们:

- 扫描扫描
- 标签标签
- 解析内容( 从缓存或远程)
- 将标记替换为实际标记

# 然后让Markdig 一起处理一切

这确保了一致性 -- -- 不论来源如何,所有记分都得到同样的待遇。**基本语法**使用扩展名很简单 。

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

```

## 在您的标记中 :

就是这样!`IMarkdownFetchService`:

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

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

延期将:

## 从 GitHub 获取 README

堆积了24小时`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}";
}
```

返回后请求的缓存内容

1. 缓存过期时自动更新
2. 存储提供者建筑
3. 我遵循的设计原理之一是
4. 灵活度
5. 
6. 不同的应用有不同的需要。
7. 一个小型的演示应用程序不需要PostgreSQL, 但多服务器的生产部署是需要的。

于是我建造了一个可插入的存储结构:**核心界面**每件每件东西都执行

## 简单而干净。

每个执行项目都以自己的方式储存,但接口始终一致。

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

模拟存储:适合演示

1. 最简单的执行用途`SemaphoreSlim`正如你们所看到的,这可以做到如下:
2. 从 URL 和博客 post ID 创建缓存密钥
3. 检查我们是否存有缓存内容,是否是新鲜内容
4. 如果缓存是新鲜的, 请立即返回
5. 如果淡化, 尝试获取新内容

## 成功时, 更新缓存

以缓存内容失败时, 返回过期缓存( stale- while- revalidate! ) !

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

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

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

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

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

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

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

        return fetchResult;
    }
}
```

没有缓存失败时, 返回错误

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

这种模式 -

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

```

静态时的重新校验

# - 对可靠性至关重要。

即使GitHub倒闭了,你的网站 仍然用隐藏的内容工作。

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

基于文件的存储库:简单持久性

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

## 对于单一服务器的部署,基于文件的储存工作非常出色:

这里的要点 :`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();
```

## 用途

用于线索安全文件访问

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

隐藏 URL + 博客 的 URL + 博客 post ID 以创建安全文件名

1. 使用文件修改时间确定新鲜度`<fetch>`在整个应用程序重新启动中保持永久状态
2. 相同平淡的值重现模式
3. 数据库储存:生产-储存
4. 对于生产部署, 特别是多服务器设置, 您想要共享缓存 。

# 这就是数据库提供者来的地方:

## 数据库模式很简单:

在多服务器的部署中,这可以让您在所有情况中隐藏一致性:**所有服务器共享相同的缓存 。**当服务器 1 获取 README 时, 服务器 2 和 3 立即从缓存内容中受益 。

**设置扩展 设置扩展**

开始是直截了当的。`[TOC]`首先,安装基件包 :

```markdown
# My Document

[TOC]

# Introduction
Content here...

# Getting Started
More content...

## Installation
Details...
```

然后选择您的存储提供者 :

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

**ASP. NET 核心的配置**

在你的

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

与您的 Markdown 设计整合

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

**关键是预处理步骤。**

1. **以下是我如何把它融入我的博客:**流动是:

2. **您的标记包含**标签标签
   
   - 预处理器将预处理器溶解到实际标记`id="getting-started"`
   - 合并计分通过马克迪`id="api-reference"`

3. **每样东西都有你的定制扩展、外观等**高级特征`ul/li`内容生成表

**包包现在包括**

单独

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

**目录( TOC) 扩展 !**虽然它被包装 与取回扩展, 它是完全独立的, 并且可以自己使用。**您可以从文档的标题中自动生成可点击目录。**基本用法:

- 简单添加
- 在您的标记中的任意位置 :

这将生成包含锁定链接的所有标题的嵌套列表 :`.UseToc()`自定义 CSS 类 :

**您可以指定一个自定义的 CSS 类,用于打字 :**这与您的自定义类有关 :

- 如何运作:
- 自动检测
- :技选委员会自动检测到文档中的最低标题水平,并相应调整。

**如果您的文档以 H2 开头, TOC 将 H2 视为顶级 。**ID代代人`[TOC]`: 锁定连接时标题会自动给定 ID :`.UseToc()`"开始"

## “API参考”____________________________________________________________________________________________________________________________________________________________________________________

嵌套结构

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

:造物主建造一个合适的巢穴

- `./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`
- 反映文档等级结构的文档结构。

启用技术中心支持 :

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

## 当配置您的 Markdig 管道时, 添加 TOC 扩展 :

管道位置 :

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

不同于某些 Markdig 扩展, TOC 扩展

> _不在乎你把它加到哪里[正在输油管中。](https://api.example.com/status.md)自动扩展名 :_

在分析器列表( 位置 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"/>
```

在所有解析完成后重新调试器, 从整个文档中收集标题

> 所以你可以添加

- 管道配置的开始、中间或结束。

- `{retrieved:format}`重要:
- `{age}`技选委员会的延期与调阅延期完全无关。
- `{url}`他们只是为了方便才包装在一起的
- `{nextrefresh:format}`你可以:
- `{pollfrequency}`使用 TOC 而不获取
- `{status}`使用不使用 TOC 的抓取

## 两者同时使用

注:`disable="true"`TOC标记在您的主标记文件中和远程内容中都起作用。

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

如果您从 GitHub 中获取包含

- ,它将自动从文档的标题标题中生成目录(假设您添加了
- 在输油管上)。
- 链接转换

当获取远程标记时( 特别是从 GitHub) , 相对链接会断开 。`<fetch>`扩展名可自动重写 :`<fetch-summary>`此变换 :

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

## 保存绝对 URL 和锁定

执行时使用 Markdig AST 重写链接 :

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

获取元数据摘要

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

# 您可以在上次获取内容时显示读者 :

以页脚制成 :

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

内容从

1. **网站:http://api.example.com/status.md。**2025年1月6日(2小时前)
2. **或者自定义模板 :**产出:__________________________________________________________________________
3. **最新更新时间:2025年1月6日 2025年1月6日 2.30 状态:缓存 下次更新:22小时后**可用的占位符 :
4. **- 最后获取日期/时间**- 取回后人类可读时间

- 来源 URL**- 内容何时更新**- 时数缓存持续时间

# - 缓存状态(重置/缓存/缓存)

禁用文件处理

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

写入获取扩展名的文档( 如此文章 !) 时, 您需要一种方法来显示标签, 而不进行处理 。

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

# 使用

属性 :

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

# 禁用标签仍保留在按原样标记的标记中, 完美适用于 :

写入关于扩展名本身的文档

1. **在教义中创建示例**显示标签语法而不触发抓取`ConcurrentDictionary`这对两者都管用
2. **和**标签 :
3. **监测事件系统**扩展号发布所有抓取操作的事件 :
4. **这样很容易与应用程序透视、 Prometheus 或您的伐木基础设施整合:**详细缓存战略`IHttpClientFactory`缓冲行为遵循国家机器模式:
5. **这里的关键洞察力是:**新鲜缓存新缓存

- 立即返回,没有网络被击中

- Stale 缓存缓存
- - 尝试获取新鲜, 但如果抓取失败, 倒回到老旧
- 缺少缓存
- - 必须获取或返回错误

# 零彩票频率

- 总是获取新鲜(可用于测试)

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

此模式被称为

# 静态时的重新校验

这对可靠性来说是极好的。

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

即使你的线人倒下,你的网站 仍然提供隐藏的内容。

# 缓存清除和管理

有时您需要手动手动无效缓存 :

1. **或者在内容改变时通过 Webhooks :**延长期测试
2. **延长期包括全面测试。**这是我如何构建它们的:
3. **绩效考量**扩展部分的设计是为了达到以下性能:
4. **同时词典**- 初步实施用途
5. **用于线索安全访问**夏马磷Slim
6. **- 防止种族条件的基于文件的使用同步锁定以防止种族条件**数据库指数

- 所有数据库提供者都有关于缓存密钥的适当索引

- [HTTP 客户共享](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
- [- 用途](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Postgres)
- [高效连接再利用](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Sqlite)
- [Async Async 一路前进](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.SqlServer)

- 没有阻断电话,一切都是静音[我家里服务器上的典型性能数字:](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension)

缓存命中: < 1ms