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 the application's integration with the Contentful headless CMS. It covers the SDK, configuration, data modeling, and querying patterns used to fetch and display content.
See also:
- /architecture/overview.md
- /configuration/contentful_setup.md
- /models/content_models.md
- /features/internationalization.md
Contentful SDK
The application uses the official contentful.aspnetcore SDK (v3.3.6) to communicate with the Contentful Delivery and Preview APIs. This package provides a strongly-typed client, model mapping, and integration with the ASP.NET Core dependency injection framework.
The initial registration of Contentful services is done in Startup.cs via the AddContentful extension method, which reads base configuration from appsettings.json.
csharp
// File: TheExampleApp/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddContentful(Configuration);
// ...
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
However, to support dynamic, per-session credentials, the application overrides the default IContentfulClient registration with a custom factory. This advanced setup is detailed in the following sections.
Client Configuration
While AddContentful() provides the basic configuration, the application requires the ability to change Contentful settings (like API keys or space ID) at runtime for a user's session. To achieve this, we replace the default transient IContentfulClient with our own factory method.
This is configured in Startup.cs:
csharp
// File: TheExampleApp/Startup.cs
// ...
// Register the manager that can retrieve options from the session or config.
services.AddSingleton<IContentfulOptionsManager, ContentfulOptionsManager>();
// Register a custom factory for IContentfulClient.
services.AddTransient<IContentfulClient, ContentfulClient>((ip) => {
// Get the HttpClient, which may be a custom one for staging environments.
var client = ip.GetService<HttpClient>();
// Use our custom manager to get the correct options for the current request.
var options = ip.GetService<IContentfulOptionsManager>().Options;
// Instantiate the client with the resolved options.
var contentfulClient = new ContentfulClient(client, options);
// Add a custom user-agent string for analytics and tracking.
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
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Key aspects of this configuration:
- Dependency on
IContentfulOptionsManager: The factory doesn't useIOptions<ContentfulOptions>directly. Instead, it resolvesIContentfulOptionsManager, which is responsible for deciding whether to use credentials fromappsettings.jsonor from the current user's session. - Custom
HttpClient: The factory resolvesHttpClientfrom the DI container. In staging environments (whenIsStaging()returns true and theStagingHostenvironment variable is set), a customHttpClientwith aStagingMessageHandleris registered to redirect Contentful API calls to an alternative staging endpoint. - Custom Application Header: A custom string is appended to the client's
Applicationproperty. This modifies theX-Contentful-User-Agentheader, which is useful for identifying API traffic from this specific application in Contentful logs.
Dynamic Options Management
The ContentfulOptionsManager is the core component enabling runtime changes to Contentful credentials. It acts as a proxy between the IContentfulClient and the configuration sources.
Its primary logic is in the Options property getter:
csharp
// File: TheExampleApp/Configuration/ContentfulOptionsManager.cs
public ContentfulOptions Options {
get {
// Check if ContentfulOptions have been 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, return the default options loaded from appsettings.json.
return _options;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
How it works:
- The manager is injected with the default
IOptions<ContentfulOptions>(from configuration) and anIHttpContextAccessorto access the current request's session. - On every request for the
Options, it first checks theSessionfor a JSON-serializedContentfulOptionsobject. - If session-based options are found, they are used. This allows a single user's requests to be directed to a different Contentful space or API (e.g., Preview vs. Delivery) without affecting other users.
- If no options are in the session, it falls back to the application-wide configuration from
appsettings.json.
This pattern is particularly useful for features like a "Settings" page where a user can input their own Contentful credentials to preview content from their own space.
The manager also provides an IsUsingCustomCredentials property to determine whether the current session is using custom credentials or the default application configuration:
csharp
// File: TheExampleApp/Configuration/ContentfulOptionsManager.cs
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
This property compares the session-stored credentials against the default configuration and returns true if they differ. This is useful for displaying UI indicators or controlling access to certain features based on whether custom credentials are active.
Delivery vs. Preview API
The ability to switch between Contentful's Delivery API (for published content) and Preview API (for draft/pending content) is managed through the ContentfulOptions object.
ContentfulOptions.UsePreviewApi(boolean): Whentrue, theContentfulClientautomatically targets the Preview API endpoint (preview.contentful.com).ContentfulOptions.PreviewApiKey: This key must be provided whenUsePreviewApiistrue.
Because of the ContentfulOptionsManager, switching to the Preview API for a user is as simple as:
- Creating a new
ContentfulOptionsobject. - Setting
UsePreviewApi = trueand providing the correctPreviewApiKey. - Serializing this object to JSON and storing it in the user's session under the
nameof(ContentfulOptions)key.
On subsequent requests, the ContentfulOptionsManager will pick up these session-based options, and the IContentfulClient will be instantiated to use the Preview API for that user only. This is the mechanism that powers live preview functionality.
Content Models
Content from Contentful is mapped to strongly-typed C# Plain Old CLR Objects (POCOs). These models reside in the TheExampleApp/Models directory. The mapping between Contentful fields and C# properties is handled automatically by the SDK, where the C# property name (PascalCase) is expected to match the Contentful field ID (camelCase).
Below is an example of the Course model, which corresponds to the "course" content type in Contentful.
csharp
// File: TheExampleApp/Models/Course.cs
using Contentful.Core.Models;
using System.Collections.Generic;
namespace TheExampleApp.Models
{
public class Course
{
/// <summary>
/// The system defined meta data properties.
/// Contains ID, timestamps, etc.
/// </summary>
public SystemProperties Sys { get; set; }
/// <summary>
/// Maps to a "Title" text field.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Maps to a "Slug" text field.
/// </summary>
public string Slug { get; set; }
/// <summary>
/// Maps to a single "Image" media asset link.
/// </summary>
public Asset Image { get; set; }
/// <summary>
/// Maps to a "ShortDescription" text field.
/// </summary>
public string ShortDescription { get; set; }
/// <summary>
/// Maps to a "Description" text field.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Maps to a "Duration" number field (in minutes).
/// </summary>
public int Duration { get; set; }
/// <summary>
/// Maps to a "SkillLevel" text field.
/// </summary>
public string SkillLevel { get; set; }
/// <summary>
/// Maps to a list of linked entries of the "Lesson" content type.
/// </summary>
public List<Lesson> Lessons { get; set; }
/// <summary>
/// Maps to a list of linked entries of the "Category" content type.
/// </summary>
public List<Category> Categories { get; set; }
}
}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
56
57
58
59
60
61
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
56
57
58
59
60
61
The Lesson model demonstrates how content can be composed of modular components:
csharp
// File: TheExampleApp/Models/Lesson.cs
using Contentful.Core.Models;
using System.Collections.Generic;
namespace TheExampleApp.Models
{
public class Lesson
{
/// <summary>
/// The system defined meta data properties.
/// </summary>
public SystemProperties Sys { get; set; }
/// <summary>
/// Maps to a "Title" text field.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Maps to a "Slug" text field.
/// </summary>
public string Slug { get; set; }
/// <summary>
/// Maps to a list of linked entries implementing ILessonModule.
/// Modules represent the individual content blocks that make up a lesson.
/// </summary>
public List<ILessonModule> Modules { get; set; }
}
}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
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
Key Mapping Conventions:
| Contentful Field Type | C# Property Type | Description |
|---|---|---|
| Text, Symbol | string | For simple text content. |
| Number (Integer) | int | For numeric values. |
| Date | DateTime? | For date and time values. |
| Asset (one) | Asset | For a single linked image, video, or file. |
| Asset (many) | List<Asset> | For multiple linked assets. |
| Entry Link (one) | YourModel | For a single linked entry (e.g., Lesson). |
| Entry Link (many) | List<YourModel> | For multiple linked entries (e.g., List<Lesson>). |
| System Properties | SystemProperties | Provides access to metadata like Id, CreatedAt, etc. |
Querying Content
Content is fetched from Contentful using the injected IContentfulClient and a fluent QueryBuilder<T>. This provides a strongly-typed, readable, and maintainable way to construct API requests.
The Courses/Index.cshtml.cs page model demonstrates a typical query to fetch a single course by its slug, including its linked lessons.
csharp
// File: TheExampleApp/Pages/Courses/Index.cshtml.cs
public async Task<IActionResult> OnGet(string slug)
{
// 1. Construct the query using the fluent QueryBuilder.
var queryBuilder = QueryBuilder<Course>.New
.ContentTypeIs("course") // Target the 'course' content type.
.FieldEquals(f => f.Slug, slug?.ToLower()) // Filter where the 'slug' field matches the route parameter.
.Include(5) // Resolve linked entries up to 5 levels deep.
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString()); // Request a specific locale.
// 2. Execute the query against the Contentful API and get the first result.
Course = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
if(Course == null)
{
// Set error message and return 404 if no entry was found.
TempData["NotFound"] = _localizer["errorMessage404Course"].Value;
return NotFound();
}
// Track this course as visited for the current user.
_visitedLessonsManager.AddVisitedLesson(Course.Sys.Id);
// Update the breadcrumb trail with the course title.
_breadcrumbsManager.ReplaceCrumbForSlug(Course.Slug.ToLower().Replace('-', ' '), Course.Title);
// Pass visited lessons to the view for display.
ViewData["VisitedLessons"] = _visitedLessonsManager.VisitedLessons;
return Page();
}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
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
Breakdown of the Query:
QueryBuilder<Course>.New: Initializes a new query that will returnCourseobjects..ContentTypeIs("course"): This is a mandatory filter that specifies which content type to search for. The string must match the Contentful Content Type ID..FieldEquals(f => f.Slug, ...): A strongly-typed filter that translates tofields.slug=valuein the API request. This is the preferred way to filter, as it provides compile-time checking..Include(5): This is a critical parameter for resolving linked entries. Anincludelevel of 5 tells Contentful to embed linked entries (and their linked entries, and so on) up to 5 levels deep in the JSON response. Without this, theCourse.Lessonsproperty would benull..LocaleIs(...): Specifies the desired locale for the content. This is essential for internationalization.
Localization in Contentful
The application supports multiple locales for both UI strings and Contentful content. The strategy for handling content localization is sophisticated, aiming to provide the best possible experience even when a direct translation is not available.
Contentful Locale Fallbacks: Contentful spaces can be configured with a locale fallback chain (e.g., de-DE -> en-US). If a field is requested for de-DE but is empty, the Contentful API can automatically return the value from en-US.
Application-Level Fallbacks: The application adds another layer of intelligence on top of Contentful's behavior. The custom RequestCultureProvider in Startup.cs implements a specific logic:
- A user requests a page with a locale, e.g.,
?locale=de-AT(Austrian German). - The application checks if
de-ATis a locale supported by its own static UI resources (en-US,de-DE). In this case, it is not. - Instead of defaulting to
en-US, the application queries the Contentful API to get the defined locale fallback chain forde-AT. Let's assume the chain isde-AT->de-DE->en-US. - The application traverses this chain until it finds a locale that is supported by its UI resources (
de-DE). - It then configures the request as follows:
- Content Locale:
de-ATis stored in the session (LOCALE_KEY). TheIContentfulClientwill use this to request content, allowing the user to seede-ATcontent if it exists. - UI Locale:
de-DEis stored in the session (FALLBACK_LOCALE_KEY) and set as the request'sUICulture. This ensures all static text (buttons, labels, etc.) renders in German instead of defaulting to English.
- Content Locale:
This ensures that the user gets the most specific content available from Contentful while the surrounding application UI remains in a consistent, supported language. The LocaleIs parameter in the query builder automatically picks up the correct content locale from the session.