第六章:文章管理与 Markdown 支持

博客v1.0系列教程(Csharp)博客 v1.0 系列教程 (C#)

6.1 文章服务

public class ArticleService : IArticleService
{
    private readonly IRepository<ArticleModel> _articleRepo;

    public async Task<PageResult<ArticleModel>> GetArticlesAsync(
        int page, int pageSize, string? category, string? keyword)
    {
        var query = _articleRepo.AsQueryable()
            .WhereIF(!string.IsNullOrEmpty(category),
                a => a.Category == category)
            .WhereIF(!string.IsNullOrEmpty(keyword),
                a => a.Title.Contains(keyword) || a.Content.Contains(keyword))
            .OrderBy(a => a.CreateTime, OrderByType.Descending);

        var total = await query.CountAsync();
        var items = await query.Skip((page - 1) * pageSize)
            .Take(pageSize).ToListAsync();

        return new PageResult<ArticleModel>(items, total, page, pageSize);
    }
}

6.2 Markdown 渲染

使用 HtmlAgilityPack 处理 Markdown 内容:

public class MarkdownHelper
{
    public static string RenderToHtml(string markdown)
    {
        // 使用 Markdown 解析库将 Markdown 转为 HTML
        var pipeline = new MarkdownPipelineBuilder()
            .UseAdvancedExtensions()
            .Build();

        return Markdig.Markdown.ToHtml(markdown, pipeline);
    }

    public static string ExtractAbstract(string markdown, int length = 160)
    {
        var plainText = Regex.Replace(markdown, @"[#*`\[\]]", "");
        return plainText.Length > length
            ? plainText[..length] + "..."
            : plainText;
    }
}

6.3 文章端点

public static void MapArticleEndpoints(this WebApplication app)
{
    var group = app.MapGroup("/api/article");

    // 获取文章列表(分页)
    group.MapGet("/", async (IArticleService service,
        int page = 1, int pageSize = 10,
        string? category = null, string? keyword = null) =>
    {
        return await service.GetArticlesAsync(page, pageSize, category, keyword);
    });

    // 获取文章详情
    group.MapGet("/{id}", async (IArticleService service, long id) =>
    {
        var article = await service.GetArticleByIdAsync(id);
        return article is null ? Results.NotFound() : Results.Ok(article);
    });

    // 创建文章
    group.MapPost("/", async (IArticleService service,
        CreateArticleDto dto) =>
    {
        var id = await service.CreateArticleAsync(dto);
        return Results.Created($"/api/article/{id}", new { Id = id });
    }).AddEndpointFilter<AuthorizeFilter>();

    // 更新文章
    group.MapPut("/", async (IArticleService service,
        UpdateArticleDto dto) =>
    {
        await service.UpdateArticleAsync(dto);
        return Results.Ok();
    }).AddEndpointFilter<AuthorizeFilter>();

    // 删除文章
    group.MapDelete("/{id}", async (IArticleService service, long id) =>
    {
        await service.DeleteArticleAsync(id);
        return Results.Ok();
    }).AddEndpointFilter<AuthorizeFilter>();
}

6.4 双源文章系统

支持从数据库和 Markdown 文件两种来源加载文章:

public interface IArticleProvider
{
    Task<ArticleModel?> GetArticleAsync(long id);
    Task<List<ArticleModel>> GetArticlesAsync();
}

public class DatabaseArticleProvider : IArticleProvider { /* ... */ }

public class MarkdownArticleProvider : IArticleProvider
{
    private readonly string _articlesDir;

    public MarkdownArticleProvider(string articlesDir)
    {
        _articlesDir = articlesDir;
    }

    public async Task<List<ArticleModel>> GetArticlesAsync()
    {
        var articles = new List<ArticleModel>();
        var files = Directory.GetFiles(_articlesDir, "*.md",
            SearchOption.AllDirectories);

        foreach (var file in files)
        {
            var content = await File.ReadAllTextAsync(file);
            var (meta, body) = ParseFrontmatter(content);
            articles.Add(new ArticleModel
            {
                Id = GenerateStableId(file),
                Title = meta["title"],
                Content = body
            });
        }
        return articles;
    }
}

6.5 浏览历史

app.MapPost("/api/article/history", async (
    IArticleService service,
    HistoryDto dto) =>
{
    await service.RecordHistoryAsync(dto.ArticleId);
    return Results.Ok();
}).AddEndpointFilter<AuthorizeFilter>();

下一章将实现评论系统与敏感词过滤。

csharpmarkdowncrudarticle