Appearance
Are you an LLM? You can read better optimized documentation at /architecture/contentful_integration.md for this page in Markdown format
Contentful Integration
This document provides a detailed technical overview of how TheExampleApp integrates with the Contentful headless CMS. It covers the architecture, client configuration, data fetching patterns, and content modeling strategies used throughout the application.
Contentful SDK
The application leverages the official Contentful .NET SDK to communicate with the Contentful Delivery and Preview APIs.
- SDK Package:
contentful.aspnetcore - Version:
3.3.6
The core integration is established in Startup.cs by registering Contentful services with the ASP.NET Core dependency injection container. The services.AddContentful(Configuration) extension method is the primary entry point, which binds the ContentfulOptions section from appsettings.json to the SDK's configuration objects.
json:TheExampleApp/appsettings.json
{
// ...
"ContentfulOptions": {
"DeliveryApiKey": "...",
"PreviewApiKey": "...",
"SpaceId": "...",
"UsePreviewApi": false
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
While the application uses the standard registration method, it also provides a custom, session-aware configuration layer on top, which is detailed in the Client Configuration section.
Content Delivery Architecture
The application is designed to fetch content from Contentful using a server-side rendering (SSR) model. Content is retrieved within Razor Page models and then passed to the Razor view for rendering.
Delivery API vs. Preview API
The application can be configured to use either the Contentful Delivery API (for published content) or the Preview API (for draft/unpublished content). This is controlled by the UsePreviewApi boolean in the configuration.
- Delivery API: Used by default in production environments. It is optimized for speed and availability and only serves published entries. It uses the
DeliveryApiKey. - Preview API: Used for development or internal preview environments. It can access both draft and published entries, allowing content editors to review changes before they go live. It uses the
PreviewApiKey.
The application's custom ContentfulOptionsManager allows this setting to be changed at runtime on a per-session basis, which is a key part of the application's dynamic configuration capabilities. For more information on how this is exposed to the user, see the Preview Mode documentation.
Content Fetching Patterns
Content is fetched within the OnGet or OnPost handlers of Razor Page models. The IContentfulClient is injected via the constructor, a pattern established in the BasePageModel.
csharp:TheExampleApp/Models/BasePageModel.cs
// The base model for all Razor Pages ensures the Contentful client is available.
public class BasePageModel : PageModel
{
protected readonly IContentfulClient _client;
public BasePageModel(IContentfulClient client)
{
_client = client;
// A custom resolver is used to handle polymorphic content module lists.
_client.ContentTypeResolver = new ModulesResolver();
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
A typical data fetching operation involves building a query, executing it with _client.GetEntries(), and processing the results.
Query Builder Usage
The SDK's QueryBuilder provides a fluent, type-safe interface for constructing API requests. This is the standard method for fetching entries in the application. The following example from Index.cshtml.cs demonstrates a complex query.
csharp:TheExampleApp/Pages/Index.cshtml.cs
// Example of fetching a 'layout' entry for the home page.
public async Task OnGet()
{
var queryBuilder = QueryBuilder<Layout>.New
.ContentTypeIs("layout") // 1. Filter by Content Type ID
.FieldEquals(f => f.Slug, "home") // 2. Filter by a field value
.Include(4) // 3. Include linked entries up to 4 levels deep
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString()); // 4. Specify the locale
var indexPage = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
IndexPage = indexPage;
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Query Breakdown:
ContentTypeIs("layout"): Restricts the search to entries of the "layout" content type.FieldEquals(f => f.Slug, "home"): Finds the specific layout entry where theslugfield is equal to "home". The lambda expression provides compile-time safety.Include(4): A crucial parameter for performance and data integrity. It instructs the Contentful API to resolve and embed linked entries up to 4 levels deep in a single API call, preventing the N+1 query problem.LocaleIs(...): Specifies which localization of the content to fetch. The application retrieves the current locale from the user's session, demonstrating its robust internationalization support.
Client Configuration
The application features a sophisticated, non-standard client configuration that allows for runtime modification of Contentful credentials on a per-session basis. This is primarily intended for demonstration purposes and is not typically required for a standard Contentful application.
IContentfulClient Setup
Instead of relying on the default singleton IContentfulClient provided by AddContentful(), the application overrides the registration with a transient, factory-based approach in Startup.cs.
csharp:TheExampleApp/Startup.cs
// Custom registration of the IContentfulClient
services.AddTransient<IContentfulClient, ContentfulClient>((ip) => {
var client = ip.GetService<HttpClient>();
// The custom manager provides session-aware options.
var options = ip.GetService<IContentfulOptionsManager>().Options;
var contentfulClient = new ContentfulClient(client,
options);
// A custom user-agent is appended for analytics and debugging.
var version = typeof(Startup).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion;
contentfulClient.Application = $"app the-example-app.csharp/{version}; {contentfulClient.Application}";
return contentfulClient;
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
This custom factory ensures that for every request, the IContentfulClient is instantiated with the correct ContentfulOptions, which may come from the user's session rather than the static appsettings.json file.
Custom Client Implementation
The application does not use a custom implementation of IContentfulClient, but rather a custom instantiation logic for the SDK's default ContentfulClient.
A notable customization is the StagingMessageHandler, an HttpClientHandler that can rewrite Contentful API requests to a staging endpoint. This is activated when the StagingHost environment variable is set in the Staging environment, allowing developers to test against a different API endpoint without changing the core configuration.
csharp:TheExampleApp/Startup.cs
public class StagingMessageHandler : HttpClientHandler
{
public string StagingHost { get; set; }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Replaces the 'contentful' domain with the specified staging host.
var regex = new Regex("contentful");
var req = regex.Replace(request.RequestUri.ToString(), StagingHost, 1);
request.RequestUri = new Uri(req);
return await base.SendAsync(request, cancellationToken);
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Options Management
The core of the dynamic configuration is the ContentfulOptionsManager. This class is responsible for deciding whether to use the default application configuration or a custom configuration stored in the user's session.
See Contentful Setup for details on how these session options can be modified by the user.
csharp:TheExampleApp/Configuration/ContentfulOptionsManager.cs
/// <summary>
/// Class used to configure whether the current session should use the application configuration or options from session.
/// </summary>
public class ContentfulOptionsManager : IContentfulOptionsManager
{
private ContentfulOptions _options;
private readonly IHttpContextAccessor _accessor;
public ContentfulOptionsManager(IOptions<ContentfulOptions> options, IHttpContextAccessor accessor)
{
_options = options.Value; // Default options from appsettings.json
_accessor = accessor;
}
/// <summary>
/// Gets the currently configured ContentfulOptions either from session, if present, or from the application configuration.
/// </summary>
public ContentfulOptions Options {
get {
// Check if there are options stored in the current user's session.
var sessionString = _accessor.HttpContext.Session.GetString(nameof(ContentfulOptions));
if (!string.IsNullOrEmpty(sessionString))
{
// If yes, deserialize and return them.
return JsonConvert.DeserializeObject<ContentfulOptions>(sessionString);
}
// Otherwise, fall back to the default options.
return _options;
}
}
/// <summary>
/// Whether or not the application is using custom credentials.
/// </summary>
public bool IsUsingCustomCredentials
{
get
{
var sessionString = _accessor.HttpContext.Session.GetString(nameof(ContentfulOptions));
if (string.IsNullOrEmpty(sessionString))
{
return false;
}
var options = JsonConvert.DeserializeObject<ContentfulOptions>(sessionString);
return (options.SpaceId == _options.SpaceId &&
options.UsePreviewApi == _options.UsePreviewApi &&
options.DeliveryApiKey == _options.DeliveryApiKey &&
options.PreviewApiKey == _options.PreviewApiKey) == false;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Content Models
The application maps Contentful content types to strongly-typed C# Plain Old C# Objects (POCOs). This provides benefits like IntelliSense, compile-time checking, and cleaner code within the page models and views.
For a complete reference of all C# models and their mapping to Contentful content types, please see the Models documentation.
Mapping Contentful Entries to C# Models
Each C# model that represents a Contentful entry typically corresponds to a content type in the Contentful space. For example, a Layout class in C# maps to the "Layout" content type. The properties of the class map to the fields of the content type.
The SDK's ContentTypeResolver is used to handle deserialization of linked entries, especially in polymorphic fields (e.g., a "content modules" field that can contain links to different types of modules). The application provides a custom ModulesResolver in BasePageModel.cs to manage this mapping.
SystemProperties Handling
In addition to the content fields, every Contentful entry includes metadata accessible via the Sys property, which is of type SystemProperties. This metadata includes the entry's ID, version, creation date, and update date.
The application explicitly collects the SystemProperties of a fetched entry and its linked modules. This is used to power features like deep-linking directly to the Contentful web app, allowing editors to quickly jump from a page in TheExampleApp to the corresponding entry in the Contentful UI.
csharp:TheExampleApp/Pages/Index.cshtml.cs
// SystemProperties are collected from the main entry and its linked modules.
var systemProperties = new List<SystemProperties> { indexPage.Sys };
if (indexPage.ContentModules != null && indexPage.ContentModules.Any())
{
systemProperties.AddRange(indexPage.ContentModules?.Select(c => c.Sys));
}
SystemProperties = systemProperties;1
2
3
4
5
6
7
2
3
4
5
6
7
This collection of SystemProperties is then available to the view, where it can be used to generate "Edit in Contentful" links or for other metadata-driven functionality.