webhouse.appwebhouse.appdocs

Read @webhouse/cms content from ASP.NET Core — Razor Pages, MVC, or minimal APIs.

Setup

my-project/
  cms.config.ts
  content/
  wwwroot/uploads/
  Pages/
  Services/

Service class

Create Services/Webhouse.cs:

csharp
using System.Text.Json;
using System.Text.Json.Serialization;

namespace MyApp.Services;

public class WebhouseDocument
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("slug")]
    public string Slug { get; set; } = string.Empty;

    [JsonPropertyName("status")]
    public string Status { get; set; } = string.Empty;

    [JsonPropertyName("locale")]
    public string? Locale { get; set; }

    [JsonPropertyName("translationGroup")]
    public string? TranslationGroup { get; set; }

    [JsonPropertyName("data")]
    public Dictionary<string, JsonElement> Data { get; set; } = new();
}

public class Webhouse
{
    private readonly string _contentDir;
    private static readonly JsonSerializerOptions _options = new()
    {
        PropertyNameCaseInsensitive = true,
    };

    public Webhouse(IWebHostEnvironment env)
    {
        _contentDir = Path.Combine(env.ContentRootPath, "content");
    }

    public List<WebhouseDocument> Collection(string name, string? locale = null)
    {
        var dir = Path.Combine(_contentDir, name);
        if (!Directory.Exists(dir)) return new();

        var docs = new List<WebhouseDocument>();
        foreach (var file in Directory.GetFiles(dir, "*.json"))
        {
            try
            {
                var doc = JsonSerializer.Deserialize<WebhouseDocument>(File.ReadAllText(file), _options);
                if (doc == null || doc.Status != "published") continue;
                if (locale != null && doc.Locale != locale) continue;
                docs.Add(doc);
            }
            catch (JsonException) { /* skip malformed */ }
        }

        return docs
            .OrderByDescending(d => GetString(d, "date") ?? "")
            .ToList();
    }

    public WebhouseDocument? Document(string collection, string slug)
    {
        var path = Path.Combine(_contentDir, collection, $"{slug}.json");
        if (!File.Exists(path)) return null;
        var doc = JsonSerializer.Deserialize<WebhouseDocument>(File.ReadAllText(path), _options);
        return doc?.Status == "published" ? doc : null;
    }

    public WebhouseDocument? FindTranslation(WebhouseDocument doc, string collection)
    {
        if (string.IsNullOrEmpty(doc.TranslationGroup)) return null;
        return Collection(collection).FirstOrDefault(other =>
            other.TranslationGroup == doc.TranslationGroup && other.Locale != doc.Locale);
    }

    public static string? GetString(WebhouseDocument doc, string key)
    {
        return doc.Data.TryGetValue(key, out var v) && v.ValueKind == JsonValueKind.String
            ? v.GetString()
            : null;
    }
}

Register in Program.cs

csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton<Webhouse>();

var app = builder.Build();
app.UseStaticFiles();
app.MapRazorPages();
app.Run();

Razor Page

csharp
// Pages/Blog/Post.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyApp.Services;

public class PostModel : PageModel
{
    private readonly Webhouse _webhouse;
    public WebhouseDocument? Post { get; private set; }

    public PostModel(Webhouse webhouse) => _webhouse = webhouse;

    public IActionResult OnGet(string slug)
    {
        Post = _webhouse.Document("posts", slug);
        return Post == null ? NotFound() : Page();
    }
}
razor
@* Pages/Blog/Post.cshtml *@
@page "{slug}"
@model PostModel
@using MyApp.Services

<article>
  <h1>@Webhouse.GetString(Model.Post!, "title")</h1>
  <time>@Webhouse.GetString(Model.Post!, "date")</time>
  <div class="prose">
    @Html.Raw(Markdig.Markdown.ToHtml(Webhouse.GetString(Model.Post!, "content") ?? ""))
  </div>
</article>

Add Markdig for markdown rendering: dotnet add package Markdig

Minimal API alternative

csharp
var app = WebApplication.Create(args);
var wh = new Webhouse(app.Environment);

app.MapGet("/", () => wh.Collection("posts", "en"));
app.MapGet("/blog/{slug}", (string slug) =>
{
    var post = wh.Document("posts", slug);
    return post == null ? Results.NotFound() : Results.Ok(post);
});

app.Run();

Serving media

ASP.NET Core's UseStaticFiles() serves from wwwroot/ by default. Either:

  1. Symlink content/uploads to wwwroot/uploads, or
  2. Add an additional static files middleware:
csharp
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "public/uploads")),
    RequestPath = "/uploads",
});

Caching

csharp
builder.Services.AddMemoryCache();

// In Webhouse class:
public List<WebhouseDocument> CollectionCached(string name, string? locale, IMemoryCache cache)
{
    var key = $"webhouse:{name}:{locale}";
    return cache.GetOrCreate(key, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
        return Collection(name, locale);
    }) ?? new();
}

Next steps

Tags:FrameworksFilesystem Adapter
Previous
Consume from Go
Next
Testing the Consumer Examples
JSON API →Edit on GitHub →