Appearance
Are you an LLM? You can read better optimized documentation at /advanced_topics/polymorphic_rendering.md for this page in Markdown format
Polymorphic content rendering
This document details the strategy for rendering polymorphic content fetched from the Contentful headless CMS. In TheExampleApp, pages are composed of various "modules" (e.g., text blocks, hero images, code snippets), each corresponding to a different Contentful content type. The application must dynamically resolve these content types into specific C# objects and render them using the appropriate server-side templates.
This mechanism is central to the application's content-driven architecture. For a higher-level overview of the Contentful integration, see the Contentful Integration documentation.
Dynamic module resolution
The core challenge in a component-based CMS architecture is handling collections of heterogeneous data. When a page layout is fetched from Contentful, its contentModules field is a list of links to entries of different types. The Contentful SDK, by default, would not know how to deserialize layoutCopy and lessonCodeSnippets into their corresponding C# classes (LayoutCopy, LessonCodeSnippets) within the same collection.
To solve this, the application employs a type discrimination pattern. This involves two key components:
- A common interface (
IModule): All content models that can be part of a page's content collection implement this interface. This allows them to be held in a singleList<IModule>. - A type resolver (
ModulesResolver): A custom class that instructs the Contentful SDK's deserializer which specific C# class to use based on thecontentTypeIdof an incoming Contentful entry.
This pattern allows the application to fetch a page and its associated modules in a single API request and receive a strongly-typed object graph, ready for rendering.
ModulesResolver
The ModulesResolver is the cornerstone of the polymorphic deserialization process. It implements the IContentTypeResolver interface from the Contentful.Core SDK. Its sole responsibility is to maintain a mapping between a Contentful content type ID (a string) and a local C# Type.
When the Contentful client deserializes a collection of linked entries, it calls the Resolve method for each entry, passing the entry's contentTypeId. The resolver returns the corresponding Type, which the client then uses to instantiate the correct object.
Implementation
The implementation is a straightforward dictionary lookup.
csharp
// TheExampleApp/Configuration/ModulesResolver.cs
using Contentful.Core.Configuration;
using System;
using System.Collections.Generic;
using TheExampleApp.Models;
namespace TheExampleApp.Configuration
{
/// <summary>
/// Resolves a strong type from a content type id. Instructing the serialization engine how to deserialize items in a collection.
/// </summary>
public class ModulesResolver : IContentTypeResolver
{
private Dictionary<string, Type> _types = new Dictionary<string, Type>()
{
{ "layoutCopy", typeof(LayoutCopy) },
{ "layoutHeroImage", typeof(LayoutHeroImage) },
{ "layoutHighlightedCourse", typeof(LayoutHighlightedCourse) },
{ "lessonCodeSnippets", typeof(LessonCodeSnippets) },
{ "lessonCopy", typeof(LessonCopy) },
{ "lessonImage", typeof(LessonImage) },
};
/// <summary>
/// Method to get a type based on the specified content type id.
/// </summary>
/// <param name="contentTypeId">The content type id to resolve to a type.</param>
/// <returns>The type for the content type id or null if none is found.</returns>
public Type Resolve(string contentTypeId)
{
return _types.TryGetValue(contentTypeId, out var type) ? type : null;
}
}
}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
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
The Module Interface Pattern
For the ModulesResolver to be effective, all potential module types must share a common base type or interface. This allows them to be stored in a single collection, such as List<IModule>. The IModule interface serves this purpose.
csharp
// TheExampleApp/Models/IModule.cs
using Contentful.Core.Models;
namespace TheExampleApp.Models
{
/// <summary>
/// Interface to mark which classes can be used as modules.
/// </summary>
public interface IModule
{
SystemProperties Sys { get; set; }
}
/// <summary>
/// Interface to mark lesson modules.
/// </summary>
public interface ILessonModule : IModule
{
}
/// <summary>
/// Interface to mark layout modules.
/// </summary>
public interface ILayoutModule : IModule
{
}
}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
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
IModule: The base interface for all modules. It requires aSystemPropertiesproperty, which is essential for accessing metadata like the Contentful Entry ID.ILessonModule/ILayoutModule: These derivative interfaces act as markers, allowing for further categorization of modules if needed, though they do not add new properties.
Rendering strategies
Once the ContentfulClient has used the ModulesResolver to create a strongly-typed collection of IModule objects, the final step is to render them to HTML. The application uses a server-side rendering approach with Razor partial views.
A parent view iterates through the collection of modules and uses a switch statement on the object's type to select and render the appropriate partial view for each module. This is a clean and type-safe way to handle the rendering logic.
Partial View Selection
A hypothetical rendering loop in a Razor view (e.g., Pages/Index.cshtml) would look like this:
csharp
// Example rendering loop in a Razor page
@* The Model would contain a property like:
public List<IModule> ContentModules { get; set; }
*@
@foreach (var module in Model.ContentModules)
{
// A switch statement on the concrete type of the module
switch (module)
{
case LayoutCopy copy:
// Render the partial view for LayoutCopy, passing the typed object
<partial name="Shared/LayoutCopy" model="copy" />
break;
case LessonCopy lessonCopy:
<partial name="Shared/LessonCopy" model="lessonCopy" />
break;
case LessonCodeSnippets code:
<partial name="Shared/LessonCodeSnippets" model="code" />
break;
// ... other cases for each module type
default:
// It's good practice to handle unknown or null types,
// perhaps by logging a warning.
break;
}
}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
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
Type-Specific Views
Each module type has a dedicated Razor partial view. These views are strongly typed to their corresponding model, providing compile-time safety and IntelliSense in Visual Studio.
Here is the partial view for the LayoutCopy module. Note the @model LayoutCopy directive and the direct access to model properties like Model.Headline and Model.Copy.
csharp
// TheExampleApp/Views/Shared/LayoutCopy.cshtml
@model LayoutCopy
@{ var visualStyle = Model.VisualStyle == "Emphasized" ? "--emphasized" : ""; }
<div class="module @("module-copy"+visualStyle)">
<div class="@("module-copy__wrapper"+visualStyle)">
<div class="@("module-copy__first" + visualStyle)">
@if (!string.IsNullOrEmpty(Model.Headline))
{
<h1 class="@("module-copy__headline" + visualStyle)">@Model.Headline </h1>
}
<div class="@("module-copy__copy" + visualStyle)"><markdown content="@Model.Copy" /></div>
</div>
<div class="@("module-copy__second" + visualStyle)">
@if (!string.IsNullOrEmpty(Model.CtaLink) && !string.IsNullOrEmpty(Model.CtaTitle))
{
<a class="cta @("module-copy__cta" + visualStyle)" href="@Model.CtaLink">@Model.CtaTitle</a>
}
</div>
</div>
</div>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
This view also utilizes the <markdown> Tag Helper, which processes the Copy field's Markdown content using the Markdig library. For more details on how content is displayed, see the Content Display documentation.
Extension to new module types
Adding support for a new module type from Contentful is a routine task that involves changes in the Model, Configuration, and View layers. Follow these steps to add a new module (e.g., a "Video Embed" module).
1. Define the Content Type in Contentful
First, create the new content type in the Contentful web app. For our example, let's assume its ID is videoEmbed and it has a field embedUrl (Text).
2. Create the C# Model
Create a new C# class in the TheExampleApp/Models directory. It must implement IModule (or a derivative).
csharp
// TheExampleApp/Models/VideoEmbed.cs
using Contentful.Core.Models;
namespace TheExampleApp.Models
{
public class VideoEmbed : ILayoutModule // or IModule, ILessonModule
{
public SystemProperties Sys { get; set; }
public string Title { get; set; }
public string EmbedUrl { get; set; }
}
}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
For more information on creating models, refer to the Models documentation.
3. Update the ModulesResolver
Register the new model in ModulesResolver.cs by adding an entry to the dictionary. The key must match the Contentful content type ID exactly.
csharp
// TheExampleApp/Configuration/ModulesResolver.cs
private Dictionary<string, Type> _types = new Dictionary<string, Type>()
{
{ "layoutCopy", typeof(LayoutCopy) },
{ "layoutHeroImage", typeof(LayoutHeroImage) },
// ... existing entries
{ "lessonImage", typeof(LessonImage) },
{ "videoEmbed", typeof(VideoEmbed) } // Add the new entry here
};1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Gotcha: If you forget this step, the Contentful SDK will not be able to deserialize the
videoEmbedtype. TheResolvemethod will returnnull, and the item will likely be missing from yourContentModulescollection, with no exception thrown.
4. Create the Razor Partial View
Create a new .cshtml file in Views/Shared/. The file should be strongly typed to your new model.
csharp
// TheExampleApp/Views/Shared/VideoEmbed.cshtml
@model VideoEmbed
<div class="module module-video">
<h2>@Model.Title</h2>
@if (!string.IsNullOrEmpty(Model.EmbedUrl))
{
<iframe src="@Model.EmbedUrl" frameborder="0" allowfullscreen></iframe>
}
</div>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
For more information on creating views, refer to the Views documentation.
5. Update the Rendering Logic
Finally, add a new case to the switch statement in the main rendering loop to handle the new type.
csharp
// In the relevant Razor page (e.g., Index.cshtml)
switch (module)
{
// ... existing cases
case VideoEmbed video:
<partial name="Shared/VideoEmbed" model="video" />
break;
// ...
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10