ASP.NET Core Razor Pages and SaaS Headless Content Management Systems

This blog post attempts to present information about using ASP.NET Core Razor Pages with SaaS headless content management systems using Contentstack as an example. The information in this post is intentionally limited and oversimplified.

ASP.NET Core Razor Pages are based on ASP.NET MVC (model-view-controller), but with a simplified implementation based on common patterns. Both ASP.NET MVC and ASP.NET Core Razor Pages use razor views (.cshtml files) that contain a mix of C# and HTML that the framework compiles into a class. Each view can specify the type of a view model class from which it can retrieve data.

In ASP.NET MVC, an HTTP request matches a route, which triggers an action method in a controller. The action method of the controller can assist in the assembly of a model object that the infrastructure will passes to a method of an ActionResult class that the controller determines, which in turn generates an HTTP response, such as by passing the view model to view rendering logic.

With ASP.NET Core Razor Pages, there is no need to define routes or implement controllers. All Razor Pages view model classes must be or inherit from the PageModel class, which contains what in MVC would be action methods and the method of the ActionResult class. The infrastructure registers routes that match the paths to all .cshtml files that contain the @page directive. So, by default, ASP.NET Core Razor Pages maps URLs to files.

By convention, the name and location of the .cshtml file indicate the URLs that it handles. Views can contain C# to run and HTML that the infrastructure will embed within the default layout (the output of the view appears where /Pages/Shared/_Layout.chtml calls @RenderBody()). The default PageModel class for index.cshtml contains an empty OnGet() method that returns void, which informs the infrastructure to render index.cshtml.

Content delivery solutions for headless CMS typically map URLs either to files generated from data in the CMS or to entries in the CMS, using custom logic to determine what data to render and how. It would seem unwieldly to generate ASP.NET Razor Pages (both PageModel classes and views) from data in the CMS.

A more appropriate solution would map URLs to entries in the CMS and use logic to determine a shared view to render. The view can be hard-coded, determined by logic, or specified by the user. The page model can expose the entry model associated with the requested URL as a property. This view and its page model model can be generic for all pages, such as to pass metadata to the layout for rendering.  Each entry can specify a partial view to invoke, which can access components of the entry model and rendering logic specific to the content type of the entry.

To render entries from the headless CMS with ASP.NET Razor Pages, we need:

  • A content type and an entry with a value.
  • A PageModel class to back the view to apply to all pages.
  • The layout used by that view.
  • Logic to construct entry models for specific content types.
  • Request routing logic.
  • The Contentstack .NET SDK or an HttpClient for connecting to Contentstack.
  • Entry model classes to represent entries, with properties matching the fields defined by content type (when not used in views, I call these entry models). These are actually optional; you could instead work with JObject or anything else that can parse JSON.

This sounds like a lot, but each piece is relatively small, and once this infrastructure is in place, it is highly reusable and easy to extend.

Visual Studio Solution Explorer

The HttpClient that connects to Contentstack is the biggest piece, and you could use the Contentstack .NET SDK to avoid this. I am reusing an HttpClient that I created for Blazor with an appsettings.json file to support it.

Assuming that all entries have a field named title, here is the simplest possible entry model class to represent any entry from the CMS:

public class EntryModelBase
{
    public string Title { get; set; }
}

Request routing logic depends on URL structure. Headless CMS support various approaches for specifying the URLs. Probably the simplest example is a URL such as:

/get/contentType/entryId

While the location of the .cshtml file defines its default URL, the @page directive allows override of the route pattern that activates the view. Here is an index.cshtml that uses the EntryModel property of the IndexModel Pagemodel class, which is an instance of the EntryModelBase class that has the Title property representing an entry from the CMS.

@page "/get/{contentType}/{entryId}"
@model IndexModel

<div class="text-center">
    <h1 class="display-4">@Model.EntryModel.Title</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

This may belong in a different base class that inherits from PageModel and from which IndexModel inherits, but for now I just specified that IndexModel as the model for the layout (/Shared/_Layout.cshtml) to avoid using ViewData to get the title.

@model IndexModel
...
<title>@Model.EntryModel.Title</title>

Here is the IndexModel PageModel class (index.cshtml.cs) that sets its EntryModel property to the CMS entry specified by the URL. The infrastructure will pass the values from the URL to the parameters of the OnGet() method of IndexModel, which retrieves the corresponding EntryModelBase from the CMS and store it to its own EntryModel property for the view to access.

using ContentstackRazorPages.Core.DeliveryClient;
using ContentstackRazorPages.Core.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;

    // connection to CMS
    private readonly IContentDeliveryClient _contentClient;

    // represents entry specified in URL
    public EntryModelBase EntryModel { get; private set; }

    // dependency injection infrastructure passes CMS connection to constructor
    public IndexModel(ILogger<IndexModel> logger, IContentDeliveryClient contentClient)
    {
        _logger = logger;
        _contentClient = contentClient;
    }

    // infrastructure passes values from URL to page handler
    public void OnGet(string contentType, string entryId)
    {
        EntryModel = _contentClient.AsObjectAsync<EntryModelBase>(
            new EntryIdentifier(contentType, entryId)).Result;
    }
}

I think that the last piece is to configure dependency injection in the ConfigureServices() method of Startup.cs.

JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
{
    //TODO: configure serializer settings including JsonConverters for modular blocks
};

ContentstackDeliveryClientOptions options = new ContentstackDeliveryClientOptions();
Configuration.GetSection("ContentstackOptions").Bind(options);
services.AddSingleton<ContentstackDeliveryClientOptions>(options);
services.AddHttpClient<IContentDeliveryClient, ContentstackDeliveryClient>();
Browser showing results

My next steps would be to add fields to the content type and the model to specify a view to apply (to let the CMS user select alternate views) and a partial view to invoke. Then I would work on determining the types of classes to use for those content types, probably by attributing the entry models to identify the content types for which they apply. I would also make the layout base class change and implement a better URL strategy.

Update 30.April.20201: The following blog post takes this concept just a little bit further.

3 thoughts on “ASP.NET Core Razor Pages and SaaS Headless Content Management Systems

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: