Using Strongly-Typed Markdown in ASP.NET Core

By FoxLearn 2/26/2025 2:56:51 AM   134
Using Markdown in an ASP.NET Core application is a great way to manage content, especially when working with static content or content management systems (CMS).

Markdown allows for easy formatting and can be parsed into HTML, which can then be displayed within your web application.

In this post, I’ll walk you through an experiment that processes Markdown files with embedded YAML and converts them into strongly-typed C# objects.

In this case, the frontmatter describes metadata about the book such as the title, author, genre, and publish date.

public class Book
{
    public string Title { get; set; } = "";
    public string Author { get; set; } = "";
    public string Genre { get; set; } = "";
    public DateTime PublishDate { get; set; }
}

We can now build a system that parses the YAML frontmatter, processes the Markdown content, and allows us to display the data in a structured format.

Parsing Markdown and YAML with Strong Typing

For this task, I created a MarkdownObject<T> class that parses the content of a Markdown document and converts the embedded YAML into a strongly-typed C# object. The T parameter allows the developer to define the object type to deserialize.

To start, install the necessary NuGet packages:

<ItemGroup>
  <PackageReference Include="Markdig" Version="0.40.0" />
  <PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

Here’s the MarkdownObject<T> class implementation:

using Markdig;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using Microsoft.AspNetCore.Html;
using YamlDotNet.Serialization;
using Md = Markdig.Markdown;

namespace Demo.Models;

public class MarkdownObject<T>
{
    private static readonly MarkdownPipeline MarkdownPipeline 
        = new MarkdownPipelineBuilder()
            .UseYamlFrontMatter()
            .UseAdvancedExtensions()
            .Build();
    private static readonly IDeserializer Deserializer 
        = new DeserializerBuilder()
            .WithYamlFormatter(new YamlFormatter())
            .WithCaseInsensitivePropertyMatching()
            .Build();

    public MarkdownObject(string content)
    {
        var doc = Md.Parse(content, MarkdownPipeline);
        FrontMatter = default;

        if (doc.Descendants<YamlFrontMatterBlock>().FirstOrDefault() is { } fm)
        {
            var yaml = fm.Lines.ToSlice();
            FrontMatter = Deserializer.Deserialize<T>(yaml.Text);
            doc.Remove(fm);
        }

        Html = new HtmlString(doc.ToHtml());
    }

    public T? FrontMatter { get; private set; }
    public IHtmlContent Html { get; private set; }
}

For our example, we would use MarkdownObject<Book> to parse and render the book data.

Using MarkdownObject in a Razor Page

In this demo, I store Markdown files in a Data directory. Each file corresponds to a unique book, and we’ll use the slug of the file name to dynamically load it in the Razor page.

@page "/book/{slug}"
@model SuperContent.Pages.BookDetail

<div class="row">
    <div class="col-12">
        <h1>@Model.Book.FrontMatter?.Title</h1>
        <h4>by @Model.Book.FrontMatter?.Author</h4>
    </div>
</div>

<div class="row">
    <div class="col-3">
        <dl>
            <dt>Genre</dt>
            <dd>@Model.Book.FrontMatter?.Genre</dd>
            <dt>Published on</dt>
            <dd>@Model.Book.FrontMatter?.PublishDate.ToString("MMMM dd, yyyy")</dd>
        </dl>
    </div>
    <div class="col-9">
        @Model.Book.Html
    </div>
</div>

The corresponding Razor Page model:

using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Demo.Models;

namespace Demo.Pages;

public partial class BookDetail : PageModel
{
    [BindProperty(SupportsGet = true)]
    public string Slug { get; set; } = "";

    public MarkdownObject<Book> Book { get; set; } = null!;

    public IActionResult OnGet()
    {
        var sanitizedSlug = SlugRegex.Replace(Slug, "");
        var path = Path.Combine("Data", $"{sanitizedSlug}.md");

        if (System.IO.File.Exists(path))
        {
            var content = System.IO.File.ReadAllText($"Data/{sanitizedSlug}.md");
            Book = new(content);
            return Page();
        }

        return NotFound();
    }

    [GeneratedRegex("[^a-zA-Z0-9_-]")]
    private static partial Regex SlugRegex { get; }
}

public class Book
{
    public string Title { get; set; } = "";
    public string Author { get; set; } = "";
    public string Genre { get; set; } = "";
    public DateTime PublishDate { get; set; }
}

In this example, we use the OnGet method to read the corresponding Markdown file from the Data directory. If the file exists, the content is parsed, and we create an instance of MarkdownObject<Book> to process the YAML and generate the content.

By processing Markdown and YAML into strongly-typed C# objects, you can easily manage content and metadata in your ASP.NET Core application.