Skip to main content
Skip table of contents

4.0 Creating a Page-based app

Introduction

The SDK is how you retrieve your Forrit One content.

It is a private Nuget package called Forrit.Consumers, and any application using this package to retrieve content is said to be a Consumer Application.

Consumer Applications must be written in .NET to use the SDK.

Quickstart

In this quickstart you’ll learn how to use the SDK by:

  1. Connecting to the SDK

  2. Using the Manifests Service IManifestsService to retrieve page content

  3. Using the Content Service IContentService to retrieve structured content

  4. Configuring application routing to display the content

Prerequisites

For more up-to-date and in-depth setup information, see Creating a simple React app | setup.

Before we begin, you must:

  1. Set up Visual Studio

  2. Install the SDK

  3. Obtain information from Azure portal

Set up Visual Studio

Install Visual Studio 2022 – ensure all tools required for developing .Net Core 8 are installed.

From within Visual Studio, create a new Solution using the ASP .NET Core Web App template.

Name the solution mvc-sample.

If you use a different name, ensure you also edit the namespaces in the sample code later.

Select the .Net Core 8 framework.

Install the SDK

Install version 4.0.x of the Forrit.Consumers and Forrit.Consumers.Types Nuget packages.

SDK version 4.0.x corresponds to CMS version 4.0. The latest version should be installed.

You require access to the private Forrit Nuget repository to install this package.

Obtain Information from Azure Portal

In Azure Blob Storage, obtain Shared Access Signatures as URLs for:

  1. The ‘manifests-global’ container

  2. The ManifestReleases table

Note the host name of the project-specific Service Bus for Forrit CMS.

This is used to automatically update the Release when a new one is published to the Consumer App.

You will also require the GUID of the Consumer Application being published to.

If you are using managed identities to connect to your resources you will also require the id of the tenant that your resources have been deployed to.

1. Connect to the SDK

Before using the SDK to retrieve content we need to set up the application settings to connect to the deployed resources.

Here we configure the SDK with the details from the previous section.

Steps

1️⃣ In the application appSettings.json file, add the following settings:

JSON
  "FCMS": {
    "ConsumerKey": "{consumer-app-uid}",
    "ManifestsContainerSas": "{SAS-to-access-manifests}",
    "ReleasesSas": "{SAS-to-access-releases}",
    "ServiceBusNamespace": "{hostname-for-service-bus}",
    "TenantId": "{tenant-id}",
    "DefaultLocale": "{default-locale-short-code}"
  }

2️⃣ Replace the placeholders with the information you saved earlier:

  1. the GUID of the Consumer Application

  2. the Shared Access Signatures

  3. the project-specific Service Bus host name

  4. the id of the tenant that your resources have been deployed in

  5. the default locale short code you wish to use (e.g. en-GB)

3️⃣ Add the following code to Program.cs:

The interface classes within can be found in the Forrit.Consumers library.

C#
builder.Services.AddControllersWithViews();
builder.Services.AddHttpContextAccessor();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddForritCmsServices(builder.Configuration);

If not using Managed Identities

If you want to connect to the Service Bus using connection strings then update your appsettings.json file like below.

1️⃣ In the application appSettings.json file, add the following settings:

JSON
  "FCMS": {
    "ConsumerKey": "{consumer-app-uid}",
    "ManifestsContainerSas": "{SAS-to-access-manifests}",
    "ReleasesSas": "{SAS-to-access-releases}",
    "ServiceBusConnectionString": "{Url-for-service-bus}",
    "DefaultLocale": "{default-locale-short-code}"
  }

2️⃣ Replace the placeholders with the information you saved earlier:

  1. the GUID of the Consumer Application

  2. the Shared Access Signatures

  3. the Service Bus URL

  4. the default locale short code you wish to use (e.g. en-GB)

2. Connect to the Manifest Service

The Manifests Service is responsible for retrieving your Page content.

1️⃣ Create a folder called Controllers and add a new class called HomeController with this code:

C#
using Forrit.Consumers.Services;
using Microsoft.AspNetCore.Mvc;

namespace mvc_sample.Controllers
{
    public class HomeController : Controller
    {
        private readonly IManifestsService _manifestsService;

        public HomeController(IManifestsService manifestsService)
        {
            _manifestsService = manifestsService;
        }

        public async Task<IActionResult> ViewAsync([FromRoute] string url, CancellationToken cancellationToken)
        {
            ViewData["Body"] = await _manifestsService.GetPageBodyHtmlAsync(url, cancellationToken: cancellationToken);
            ViewData["Head"] = await _manifestsService.GetPageHeadHtmlAsync(url, cancellationToken: cancellationToken);

            return View();
        }
    }
}

3. Use the Content Service

The content service is responsible for retrieving content entries and the content manifest.

1️⃣ Create a new class called ContentController inside your controllers folder with this code:

C#
using Forrit.Consumers.Services;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;

namespace mvc_sample.Controllers
{
    public class ContentController : Controller
    {
        private readonly IContentService _contentService;
        
        public ContentController(
            IContentService contentService)
        {
            _contentService = contentService;
        }
        
        // This method gets the published content manifest that contains all the
        // names and ids of published types and entries.
        public async Task<IActionResult> GetPublishedContentManifest()
        {
            var manifest = await _contentService.GetContentManifestAsync();
            
            // From here you can get a content type by name which allows you to
            // get the id of the type and see the fields that the type has.
            var blogType = manifest.ContentTypes.FirstOrDefault(
                ct => ct.Name.Equals("Blog", StringComparison.InvariantCulture));
                
            return Json(manifest);
        }
        
        // This method gets a single blog entry for the passed id
        public async Task<IActionResult> GetBlogEntryForId([FromRoute] Guid blogId)
        {
            // This gets the content entry for the type with the name of Blog and the id
            // that was passed.
            var contentString = await _contentService.GetContentEntryForTypeJsonAsync(
              "Blog",
              blogId);
              
            var contentObject = JToken.Parse(contentString);
              
            return Json(contentObject);
        }
    }
}

4. Configure Routing

In this step we replace the default routing of our ASP.NET application with routing that will display the retrieved Pages at the route that was defined for them in the CMS UI.

For example, a Page with CMS-defined route ‘product’ will have its content displayed at the route /product in our application.

1️⃣ Add the following code to Program.cs to use the HomeController:

C#
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute("default", "{*url}", new { controller = "Home", action = "View" });
        });

This routing passes all requests to the View action in the Home controller, and passes any path parameters in a property named url. The HomeController contains a function to handle the requests that come from our new routing – see 4.0 Creating a Page-based app | How-it-Works for more information.

2️⃣ Add a new Razor page called View.cshtml to the Pages/Shared folder and put this code in it:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    @Html.Raw(@ViewData["Head"])
</head>
<body>
    @Html.Raw(@ViewData["Body"])
</body>
</html>

The Consumer Application can choose other methods for displaying Page content. However, in this example, we use ASP.NET Core MVC behaviour with Razor.

Quickstart Outcome

The SDK is now set up to retrieve Forrit One Page content.

As long as you have published a Page that contains some content in the default Locale you set in the appsettings.json and the None variant, then that content should be visible when you route to it. You should be able to route to any Page that has been published.

This implementation is the simplest possible configuration but is adequate for the purpose of this tutorial.

Additional Steps (Optional)

Configure the Routing Service

The SDK includes a Routing Service IRoutingService that can handle the HTTP redirects and rewrites defined in the CMS Routing Manager. This includes the static URLs of Media Items, which are implemented as HTTP rewrites.

To configure the SDK’s Routing Service:

1️⃣ Update the AddForritCmsServices call in Program.cs:

C#
builder.Services.AddForritCmsServices(
  builder.Configuration,
  new ForritServicesSetupSettings {
    UseInbuiltRouting = true,
  });

2️⃣ Add the following line to Program.cs (this must come before the call to app.UseRouting)

C#
app.UseForritRoutingMiddleware();

Customising the Routing Service

To provide custom routing behaviour that is not supported by the CMS, you can add your own routing middleware – before UseForritRoutingMiddleware, for example:

CODE
app.UseMiddleware<CustomRoutingMiddleware>();
app.UseForritRoutingMiddleware();

With this method, handle your specific cases in the custom middleware, then pass the other routes to the SDK Routing Service.

This approach is preferable to entirely replacing the built-in SDK routing with a routing service implementation of your own – which we’ll look at in the next section.

Implementing your own Routing Service

You can implement your own routing service and not use the SDK’s built-in one at all. To do so:

  1. Don’t call UseForritRoutingMiddleware

  2. Implement your own instance of the IRoutingService and IStaticUrlService APIs to fetch the routes defined in the CMS Routing Manager – the Manifest Service provides APIs for fetching this routing information

Configure Caching

The SDK automatically caches all Pages it fetches. It uses the IDistributedCache interface, and it relies on the Consumer Application to specify the type of cache that should be used.

If the Consumer Application does not specify the cache type then no caching is used.

For development purposes and in production with a single server, a distributed memory cache will work. However, to provide the optimal caching behaviour a Redis cache should be used. This ensures the caching will work across the multiple servers that Azure could be using to run the Consumer Application.

The cache object is also available for the Consumer Application to store any items it needs.

Set up a custom Locale Provider

Locale Providers conform to the ILocaleProvider interface.

A Locale Provider implements the logic that determines which Locale is returned for each Page request.

In the following steps we create a simple ‘mock’ Locale Provider that always returns the same Locale short code value, e.g. en-GB – note that this must exist as a Locale in the published Release.

If the consumer app does not provide an ILocaleProvider implementation then the SDK default implementation is used. This takes the locale from the query parameter of the URL.

Steps

1️⃣ Add a new class to the project, called CustomLocaleProvider.

2️⃣ Paste the code below into the new class, replacing the entire class definition:

Remember to change the namespace if you chose a different Solution name earlier.

Replace {default-locale} with a locale short code of your choice that exists in the Release being published e.g en-GB.

C#
using Forrit.Consumers.Providers;
using System.Diagnostics;

namespace mvc_sample.Providers
{
    public class CustomLocaleProvider : ILocaleProvider
    {
        public ValueTask<string> GetLocaleAsync(CancellationToken cancellationToken)
        {
            string locale = "{default-locale}";
            Trace.WriteLine(string.Format("CustomLocaleProvider: GetLocaleAsync: Returning locale '{0}'", locale));
            return new ValueTask<string>(locale);
        }
    }
}

3️⃣ Update your AddForritCmsServices call in Program.cs, remember to keep any existing settings that you have added to ForritServicesSetupSettings:

C#
builder.Services.AddSingleton<ILocaleProvider, CustomLocaleProvider>();
builder.Services.AddForritCmsServices(
  builder.Configuration,
  new ForritServicesSetupSettings {
    CustomLocaleProvider = provider => provider.GetRequiredService<ILocaleProvider>(),
  });

Add a Variant Provider With Multiple Variants

The quickstart tutorial shows the minimum required to use the SDK using only the default Variant None.

This example, however, shows how to add a more realistic Variant Provider implementation that may return one of three different Variants: Morning, Afternoon, and Night, depending on the time of day.

Locale Providers can also be written in a similar manner.

Steps

1️⃣ In Visual Studio, create a new class called TimeOfDayVariantProvider and ensure that it implements IVariantProvider:

C#
public class TimeOfDayVariantProvider : IVariantProvider

2️⃣ In TimeOfDayVariantProvider, Implement the single required method of IVariantProvider:

C#
public ValueTask<Guid?> GetVariantAsync(CancellationToken cancellationToken = default)
{
    string variant;
    DateTime systemDateTime = DateTime.Now;

    if (systemDateTime.Hour >= 0 && systemDateTime.Hour < 12)
    {
        variant = "{UID-for-morning-variant}";
    }
    else if (systemDateTime.Hour >= 12 && systemDateTime.Hour < 18)
    {
        variant = "{UID-for-afternoon-variant}";
    }
    else
    {
        variant = "{UID-for-night-variant}";
    }

    return new ValueTask<Guid?>(new Guid(variant));
}

3️⃣ Update your AddForritCmsServices call in Program.cs, remember to keep any existing settings that you have added to ForritServicesSetupSettings:

C#
builder.Services.AddSingleton<IVariantProvider, TimeOfDayVariantProvider>();
builder.Services.AddForritCmsServices(
  builder.Configuration,
  new ForritServicesSetupSettings {
    CustomVariantProvider = provider => provider.GetRequiredService<IVariantProvider>(),
  });

Outcome

You’ve learned how to create a realistic Variant Provider that returns different Variants depending on the time of day. Now, requesting Pages at different times of day will result in different Page Variants being returned.

These Page Variants may include different Content Items and Components for the Page, assuming they exist in the Release and have been configured for the Pages being requested.

Try it Yourself

To see this in action yourself, you could, for example:

  1. Create three Variants in Forrit, called "Morning", "Afternoon" and "Night"

  2. Add a Component to a Page in Forrit One, ensuring that the Component is enabled for the three Variants completed above

  3. Edit the Page’s Content Items for each Variant so that it displays different content for all Variants, for example "Good Morning", "Good Afternoon" and "Good Evening"

  4. Run the application and check that the correct Variant is selected and that the correct content is retrieved

Troubleshooting

If you’ve added new Components and Pages in this tutorial but the SDK requests are not returning that content, then:

  • Make sure you have created a new Release containing the updated Pages, and that the Release has been published to the correct Consumer Application

  • Check that the UIDs you used for your Variants in the Forrit Marketing Portal are the same as the UIDs your Variant Provider returns

Further Reading

How it Works

The Manifest Service maintains an up-to date local cache of all of your published Pages.

The View Action method is called each time a new route is matched. It expects the url parameter to contain a valid Page route from Forrit One. If no URL parameter is passed, a hardcoded default route is used.

If the Routing Service has been configured it intercepts requests before they reach the View Action method and checks for any redirects or rewrites that have been defined in the CMS, including the static URLs of Media Items which are implemented as rewrites.

Once a Page has been found, the GetPage.. functions are called on the Manifest Service, which return strings containing the Head and Body sections of the requested Page. If a URL parameter is passed that does not match a published Page route then the GetPage.. calls fail.

Finally, the Page Head and Body sections are passed into the Razor view as a ViewData object, and are rendered by the Razor View using the @Html.Raw() helper function.

Writing production Consumer Applications

This above quickstart tutorial is a simple example that implements a minimal, non-production Consumer Application. There are a few considerations worth emphasising when designing a production Consumer Application, detailed below.

Performance

The call to your Locale Provider and Variant Provider – MockLocaleProvider  and MockVariantProvider in the quickstart example – is called on every Page request. You should ensure an optimal implementation of your Locale/Variant selection logic.

If you call an external REST service in order to determine the Locale and Variant ensure you optimise the calls for performance – perhaps by caching the results in order to reduce load on the server for future requests. Otherwise, even though this is an asynchronous call, the overall performance and scalability of the server could be impacted.

Secrets

In the quickstart example a Secret (the Blob Storage Connection String) is stored within appSettings.json.

However, for production, it is recommended to use App Settings on the Azure App Service for configuration values. These can be easily retrieved at runtime, and can be managed from the Azure portal.

For true production Secrets (keys, certificates), use Azure KeyVault.

Glossary

See the Glossary for more information.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.