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:
Connecting to the SDK
Using the Manifests Service
IManifestsService
to retrieve page contentUsing the Content Service
IContentService
to retrieve structured contentConfiguring 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:
Set up Visual Studio
Install the SDK
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:
The ‘manifests-global’ container
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:
"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:
the GUID of the Consumer Application
the Shared Access Signatures
the project-specific Service Bus host name
the id of the tenant that your resources have been deployed in
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.
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:
"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:
the GUID of the Consumer Application
the Shared Access Signatures
the Service Bus URL
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:
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:
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
:
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:
<!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
:
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
)
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:
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:
Don’t call
UseForritRoutingMiddleware
Implement your own instance of the
IRoutingService
andIStaticUrlService
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
.
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
:
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
:
public class TimeOfDayVariantProvider : IVariantProvider
2️⃣ In TimeOfDayVariantProvider
, Implement the single required method of IVariantProvider
:
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
:
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:
Create three Variants in Forrit, called "Morning", "Afternoon" and "Night"
Add a Component to a Page in Forrit One, ensuring that the Component is enabled for the three Variants completed above
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"
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.