This documentation is also published as Markdown for efficient machine reading: the whole site is indexed at /llms.txt, and every page has a clean Markdown copy under /_llms/. These are generated from the same source and cost far fewer tokens to read than this rendered HTML.

Skip to main content Skip to navigation
Guides

Use a YAML or JSON data file in pages

Register a YAML or JSON file as a typed value any Razor page or component can read through IDataFiles. The file hot-reloads when you edit it.

When a piece of site content is structured data — sponsors, navigation, schedule, feature flags, footer links — authoring it as Markdown front-matter or hand-rolling an IContentService is overkill. Register the file with AddDataFile<T> and read it through IDataFiles; the file hot-reloads on edit.

Register the file

AddDataFile<T>(name, path) deserializes path into T on first access and tracks the file for changes. Format is chosen from the extension: .yml and .yaml go through SharpYaml, .json through System.Text.Json. Both deserializers use camelCase property naming with case-insensitive matching, mirroring how front matter is parsed.

csharp
builder.Services.AddDataFile<List<Sponsor>>("sponsors", "data/sponsors.yml");
builder.Services.AddDataFile<Schedule>     ("schedule", "data/schedule.yml");
builder.Services.AddDataFile<List<NavLink>>("nav",      "data/nav.json");

The lookup key ("sponsors") is case-insensitive and must be unique across registrations. Paths are resolved against the current working directory if relative.

The target type needs a parameterless constructor — use a record with init-only properties so SharpYaml can populate it:

csharp
public record Sponsor
{
    public string Name { get; init; } = "";
    public string Tier { get; init; } = "";
    public string Url { get; init; } = "";
}

Read the data from a Razor page

Inject IDataFiles and call Get<T> with the registered name. The lookup is keyed by name and type — request the same T you registered with.

razor
@inject IDataFiles Data
  
<section>
    <h2>Sponsors</h2>
    <ul>
        @foreach (var sponsor in Data.Get<List<Sponsor>>("sponsors").OrderBy(s => s.Tier))
        {
            <li><a href="@sponsor.Url">@sponsor.Name</a> &mdash; @sponsor.Tier</li>
        }
    </ul>
</section>

For lookups that may not exist (optional configuration, feature-flag style toggles), TryGet<T> returns false when the name is missing or the type does not match:

csharp
if (Data.TryGet<FooterConfig>("footer", out var footer))
{
    // render footer
}

Load a directory of records

When each record is its own file — data/maintainers/agc93.yml, data/maintainers/devlead.yml — register the directory instead of every file by hand. AddDataDirectory<TItem>(name, path) deserializes every .yml, .yaml, and .json file in path and aggregates them into one list:

csharp
builder.Services.AddDataDirectory<Maintainer>("maintainers", "data/maintainers");

Each file contributes one TItem. A file whose root is an array contributes every element, so a directory can mix single-record and multi-record files. Files are ordered by name; anything that is not .yml, .yaml, or .json is ignored, and subdirectories are not scanned.

Read it back as an IReadOnlyList<TItem> — that is the type the directory is registered under:

razor
@inject IDataFiles Data
  
<ul>
    @foreach (var m in Data.Get<IReadOnlyList<Maintainer>>("maintainers").OrderBy(m => m.Name))
    {
        <li>@m.Name</li>
    }
</ul>

Adding, editing, or removing a file in the directory invalidates the cached list, the same way editing a single data file does.

Hot reload

When a data file changes on disk, the cached value is invalidated and the next Get<T> call reloads and re-deserializes it. Pages that read the data through IDataFiles see the fresh value on the next request — no app restart needed.

This is the same lifetime model that MarkdownContentService<T> uses for content files. Edits during dotnet run propagate immediately.

Verify

  • Run dotnet run and open a page that reads the data file — confirm the records render (the sponsors list, the nav links, whatever you registered).
  • With the server still running, edit a value in data/sponsors.yml and save. Refresh the page — the new value appears without a restart, confirming hot reload.
  • Request the data under a name you never registered (Data.Get<List<Sponsor>>("sponsorz")) and confirm the KeyNotFoundException message lists the registered names — proof the registration took.

Errors

AddDataFile itself never reads the file; the read happens on the first Get<T>. The three you will actually hit while authoring:

  • FileNotFoundException — the registered path does not exist (usually a typo). For AddDataDirectory, a missing directory raises DirectoryNotFoundException.
  • InvalidDataException — the file content failed to deserialize. The message includes the absolute path and the underlying serializer error, so a bad indent or stray comma points straight at the file.
  • KeyNotFoundExceptionGet<T> was called with a name that was never registered. The message lists every registered name.

The wrong-extension (NotSupportedException) and type-mismatch (InvalidCastException) cases are catalogued in Pennington.Data.IDataFiles.

What this is not

AddDataFile is for data that decorates pages (sponsors strip on the homepage, footer links, a feature flag). It does not produce routes — a data/speakers.yml registered this way will not give you /speakers/jane-doe/. For one-page-per-record needs, write a custom Source content from outside the markdown pipeline.