Making a simple Consumer App
Purpose | This page describes how to build a Consumer App that uses version 3.0 of the Forrit CMS |
Overview
This page describes the process of creating a simple consumer app. This consists of connecting to a Manifest Service and implementing the single required methods of ILocaleProvider and IVariantProvider.
An ASP .NET Core application will be used as a server, overriding the default routing behaviour.
Prerequisites
Install Visual Studio 2022. Ensure all tools are installed for developing .Net Core 6.
From 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 edit the namespaces in the sample code below to match the name you choose
Select the ".Net Core 6" framework
Install the versions of the
Forrit.ConsumersandForrit.Consumers.TypesNuget packages in the solution that correspond to the CMS version 3.3 (this should be the nuget package version 3.2.1).Note: You need to have access to the private Cortex Nuget repository in order to install this package
Obtain Shared Access Signatures for the Manifests Container and the Releases table in Azure Blob Storage, you will need these later.
You will need the URL for the Service Bus for Forrit CMS. This will be used to automatically update the release when a change is made.
You will need the GUID for the consumer app that will be deployed to.
Configuring a Locale Provider
First we will create a Mock Locale Provider that always returns the same hard-coded value. This value must match a locale that you have defined in the Forrit CMS.
Add a new class to the project, called
MockLocaleProviderPaste the code below into the new class, replacing the entire class definition (note the namespace may be different).
Edit the placeholder in the declaration of
localewith a locale short code from your CMS (e.g. “en-gb”):CODEusing Forrit.Consumers.Providers; using System.Diagnostics; namespace mvc_sample.Providers { public class MockLocaleProvider : ILocaleProvider { public ValueTask<string> GetLocaleAsync(CancellationToken cancellationToken) { string locale = "{default-locale}"; Trace.WriteLine(string.Format("MockLocaleProvider: GetLocaleAsync: Returning locale '{0}'", locale)); return new ValueTask<string>(locale); } } }The
GetLocaleAsyncmethod will be called for every page request and in this implementation will always return the same mock value.Create a similar
MockVariantProviderclass that just returns a null Guid for the none variant:CODEusing Forrit.Consumers.Providers; using System.Diagnostics; namespace mvc_sample.Providers { public class MockVariantProvider : IVariantProvider { public ValueTask<Guid?> GetVariantAsync(CancellationToken cancellationToken) { Guid? variant = null; Trace.WriteLine(string.Format("MockVariantProvider: GetVariantAsync: Returning variant '{0}'", variant)); return new ValueTask<Guid?>(variant); } } }
Configuring a Manifest Service
Next we will configure a Manifest Service in the application and ensure that it connects successfully to your Manifests Container in Azure Blob Storage.
In the application
appSettings.jsonfile, add the following settings:CODE"ConsumerKey": "{consumer-app-uid}", "ConnectionStrings": { "ManifestsContainer": "{SAS-to-access-manifests}", "Releases": "{SAS-to-access-releases}", "ServiceBus": "{Endpoint-for-service-bus}" }Update the placeholders with the GUID for the Consumer App, the Shared Access Signatures and the Service Bus URL that you saved earlier.
Add the following code to
Program.cs.
The interface classes within here can be found in the Forrit.Consumers library.CODEbuilder.Services.AddControllersWithViews(); builder.Services.AddHostedService(provider => provider.GetRequiredService<IManifestsRefreshService>()); builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton<IManifestsRefreshService>(provider => new ManifestsRefreshSubscribingService( builder.Configuration.GetConnectionString("ServiceBus"), "manifests", Guid.NewGuid().ToString(), builder.Configuration.GetValue<Guid>("ConsumerKey"), provider.GetRequiredService<ILoggerFactory>().CreateLogger<ManifestsRefreshSubscribingService>())); builder.Services.AddSingleton<IManifestsRepository>(provider => new ManifestsRepository( builder.Configuration.GetValue<Guid>("ConsumerKey"), builder.Configuration.GetConnectionString("ManifestsContainer"), builder.Configuration.GetConnectionString("Releases"), provider.GetRequiredService<ILoggerFactory>().CreateLogger<ManifestsRepository>())); builder.Services.AddSingleton<IManifestsService, ManifestsService>(); builder.Services.AddSingleton<ILocaleProvider, MockLocaleProvider>(); builder.Services.AddSingleton<IVariantProvider, MockVariantProvider>();Create a folder called Controllers and add a new class called
HomeControllerwith this code:CODEusing Forrit.Consumers.Services; using Microsoft.AspNetCore.Mvc; using System.Threading; using System.Threading.Tasks; 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(); } } }
This implementation is the simplest possible configuration, but is adequate for our purposes here.
Change MVC Routing
Any page defined in Forrit should be displayed by providing the page route as a path parameter from the root of our ASP .NET website; so if there is a page reference called 'product1', it should be possible to display this by navigating to http://example.com/product1 (where 'example.com' is the name of your website - or localhost if running locally).
To do this, we will replace the default routing of our ASP .NET website with a route of our own design.
Add this code to Program.cs to use the HomeController:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default", "{*url}", new { controller = "Home", action = "View" });
});
This route passes all requests to the action Index in the Home controller, and passes any path parameters in the property named url.
The HomeController already contains a function to handle the requests which will come from our new route.
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>
This puts the web page data fetched from the Manifest Service onto an actual web page. The consumer app doesn’t have to do this, it depends on what mechanism it uses to display pages. In this case it uses ASP .NET MVC behaviour with Razor.
Build and run the application. As long are you have defined a page with a route that contains some content in the default locale returned by the MockLocaleProvider and the none variant, then that content should be visible when you navigate to that page. You should be able to enter the route of any page that has been published in the URL.
Set up Routing Manager
Since v3.0, the Routing manager provides routing as part of the CMS. The SDK includes its own routing manager that can handle URL redirects, as well as static URLs for media items. Alternatively the Consumer App can implement its own routing manager using the routing APIs in the SDK.
To configure the SDK routing manager, make the following changes to the code:
Add these lines to Program.cs:
CODEbuilder.Services.AddSingleton<IRoutingService, RoutingService>(); builder.Services.AddSingleton<IStaticUrlService, StaticUrlService>();Add this line to Program.cs:
CODEapp.UseForritRoutingMiddleware();
If the Consumer App is to use its own routing manager, then don’t call UseForritRoutingMiddleware and implement your own instance of the IRoutingService and IStaticUrlService APIs to fetch the data for the routes defined in the CMS. The Manifest Service provides APIs for fetching the published routing information.
Caching
The SDK automatically caches all pages and data feed items which are fetched. This uses the IDistributedCache interface, and it relies on the consumer app to specify what type of cache should be used. If the consumer app does not configure this, then there is no caching.
See this page for details about that interface.
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 multiple servers that Azure could be using to run the consumer app.
The cache object is also available for the consumer app to store any items it needs.
How it works
The Manifest Service maintains an up-to date local cache of all of the published pages of your site.
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. If no URL parameter is passed, it uses a hard coded route as a default page.
If the Routing Manager has been configured, then it intercepts the request before it reaches the View Action method and checks for any redirects or rewrites that have been defined in the CMS. Static URLs for media items are implemented as rewrites.
Once a page has been found, the GetPage.. functions are called on the Manifest Service, which returns strings containing the Head and Body elements of the requested page. If a URL parameter is passed which does not match a page route which has been published to the manifest, then the GetPage.. calls fail.
Finally, the Body element is passed into the Razor view as a ViewData object, and is rendered by the Razor View, using the @Html.Raw() helper function, because the returned value is raw HTML to be rendered.
Writing production Consumer Apps
This is a simple "Hello World" example that implements a minimal, non-production Consumer App. There are a few considerations worth emphasising when designing a production Consumer App.
Performance The call to your locale and variant providers -
mockLocaleProviderandmockVariantProviderin our 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, then ensure that you optimise this for performance (perhaps 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 sample app a Secret (the Blob Storage Connection String) is stored within web.config. For production, in the case of configuration values, it is recommended to use App Settings on the Azure App Service, which can be easily retrieved at runtime, and can be managed from the portal. For true production Secrets (keys, certificates) use Azure KeyVault.