# Побудова інструменту отримання віддалених відміток для Markdig

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

# Вступ

Однією з проблем, з якими я зіткнувся під час створення цього блогу, було те, як ефективно включати зовнішній зміст без копіювання та вставлення його всюди.

Я хотів отримати файли README з моїх сховищ GitHub, включити документацію з інших проектів і тримати все синхронізованим автоматично.`Mostlylucid.Markdig.FetchExtension`Що ж тоді робити?

> Нетиповий додаток Markdig, який отримуватиме віддалене звантаження під час відтворення і кешує його у кмітливий спосіб.

> На цьому посту я проведу вас крізь те, як я побудував

> **- повне розв' язання для отримання і кешування віддаленого запису з підтримкою декількох серверів зберігання, автоматичного опитування і невикористаного шаблону кешування.**ЗАУВАЖЕННЯ.`disable="true"`Розважайтесь, але це може ще не спрацювати.

> **Ця стаття - штучний код комп'ютера, який також допоміг мені створити цю рубрику.**UDATE`[TOC]`: Додано

Параметр, щоб ми могли демонструвати мітки належним чином без обробки![UPDATE (Nov 7, 2025)](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. **на GitHub для цього сайту**NuGet
2. **NuGet**Чому слід будувати це?
3. **Перш ніж поринути в технічні деталі, дозвольте мені пояснити цю проблему.**У мене є декілька сценаріїв, у яких мені слід включити зовнішній вміст розмітки:
4. **Пакунок READMEs**: Коли я пишу про пакунок NuGe, який я опублікував, я хочу включити його README безпосередньо з GitHub

Документація з 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. Це важливо, тому що це означає, що ви отримуєте вміст, який проходить через всю вашу трубку Markigu, отримуєте всі ваші нетипові розширення, підсвічування синтаксису та стилізування.

Основне розуміння полягає в тому, що

# попередньої обробки



```markdown
# My Documentation

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

До того, как ты попадешь в трубопровод "Маркинг," мы:

- Шукати на
- мітки
- Визначити вміст (з кешу або віддаленого)
- Замінити мітки поточною розміткою

# Тоді нехай Маркдіг все об'єднує.

Це забезпечує послідовність - всі відмітки отримують однаковий метод лікування, незалежно від його джерела.**Основний синтаксис**Використання суфікса є простим.

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

Розширення буде:

## Отримати README з GitHub

Кешувати його протягом 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 і ідентифікатора допису блогу
3. Перевіряє, чи є у нас кешований вміст і чи є він свіжим
4. Якщо кеш є свіжим, повертає його негайно
5. Якщо застарілий, намагається отримати свіжий вміст

## При успіху, оновлює кеш

Під час спроби кешування вмісту буде повернуто застарілий кеш (відновлення кешу на час!)

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

```

Смерточасне виправлення@ info: tooltip

# - це вирішальне для надійності.

Навіть якщо 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 + блогу, щоб створити безпечні назви файлів

1. Використовує час модифікації файлів для визначення свіжості`<fetch>`Наполегливі через програму перезапуск
2. Той самий застарілий шаблон перевірки на час@ info: whatsthis
3. Сховище бази даних: Production- Ready
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"]
```

Інтеграція з вашим відтворенням

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

**Ключем є крок попередньої обробки.**

1. **Ось як я інтегрую його у своєму блозі:**Потік:

2. **Ваше повідомлення містить**мітки
   
   - Попередньопроцесор вирішує, що їх слід позначити`id="getting-started"`
   - Об'єднане повідомлення проходить через Markdig.`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 для стилізації:**Це передає з вашим нетиповим класом:

- Як це працює:
- Автоматичне визначення
- : Програма TOC автоматично визначає мінімальний рівень заголовка у вашому документі і відповідним чином корегує його.

**Якщо ваш документ починається з H2, TOC вважатиме H2 найвищим рівнем.**Створення ІД`[TOC]`: Вказівки автоматично надаються ідентифікаторами для прив' язки якорів:`.UseToc()`"Початок" →

## "API Reference" →

Вкладених структур

```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`
- структура, що відображає ієрархію документа.

Увімкнення підтримки TOC:

```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}`Розширення TOC повністю не залежить від розширення звантаження.
- `{url}`Вони просто упаковуються разом для зручності.
- `{nextrefresh:format}`Ви можете:
- `{pollfrequency}`Використовувати TOC без звантаження
- `{status}`Використовувати звантаження без TOC

## Використовувати обидва разом

Примітка:`disable="true"`Позначка TOC працює як у ваших основних файлах markdown, так і у зв' язуванні віддаленого вмісту.

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

Якщо ви отримаєте файл README з GitHub, який містить

- , вона автоматично створить зміст з заголовків цього документа (якщо ви додали
- до вашого трубопроводу).
- Перетворення посилань

Коли ви отримуєте віддалену позначку вниз (особливо від GitHub), відносні посилання розриваються.`<fetch>`Суфікс може автоматично переписати їх:`<fetch-summary>`Це трансформує:

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

## Зберігати абсолютні адреси URL і якір

Реалізація використовує AST Markdig для перезапису посилань:

```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. **https: // api. example.com/status. md**6 січня 2025 року (2 години тому)
2. **Або налаштуйте шаблон:**Вивід: ~~~~ ~
3. **Останнє оновлення: 06 січень 2025 14: 001 Стан: cacheed * Next repeat: через 22 години**Доступні заповнювачі:
4. **- Остання дата/ час звантаження**- Час читання, який можна прочитати з часу отримання

- Адреса джерела**- Коли зміст буде оновлено**- Тривалість кешу у годинах

# - Стан кешу (свіжий/ запланований/ задалений)

Вимикання обробки документації

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

# Використовувати

attribute:

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

# Непрацездатний теґ залишається у списку з міткою markdown як- is, він досконалий для:

Запис документації про сам суфікс

1. **Створення прикладів у підручниках**Показ синтаксису міток без вмикання отримання`ConcurrentDictionary`Це працює для обох
2. **і**Мітки:
3. **Система подій для спостереження**Додаток оприлюднює події для всіх отриманих операцій:
4. **Це полегшує інтеграцію з " Прометей ," " Прометей ," або з вашою лісозаготівельною інфраструктурою:**Докладно про Стратегію кешування`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
```

Цей шаблон називається

# Смерточасне виправлення@ info: tooltip

і це чудово для надійності.

```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. **Або за допомогою веб- гачків, якщо змінюється вміст:**Перевірка суфікса
2. **Розширення включає в себе комплексні тести.**Ось як я будую їх:
3. **Обмірковування швидкодії**Розширення розроблено для швидкодії:
4. **З' єднаний словник**- Вдосконалення пам' яті
5. **для безпечного доступу до гілки**SemaphoreSlim
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)
- [Синхронізувати весь шлях](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.SqlServer)

- Без дзвінків блокування, все синхронно[Типовий номер швидкодії на моєму домашньому сервері:](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension)

Проходження кешу: < 1 мс