Appearance
Are you an LLM? You can read better optimized documentation at /advanced_topics/extending.md for this page in Markdown format
Extending the application
This document provides a technical guide for developers on how to extend and customize TheExampleApp. It covers common extension points, including adding new content types, pages, locales, and integrating custom services. A thorough understanding of the application's architecture is recommended. For more details, please refer to the Application Overview.
Adding new content types
The application is designed to render content dynamically from Contentful. Each piece of content that can be part of a page layout or a lesson is considered a "module." Adding a new type of content module involves a three-step process: defining a C# model, registering it with the type resolver, and creating a corresponding view for rendering.
This system relies on a polymorphic rendering pattern, where the Contentful SDK deserializes a collection of mixed content types into their specific C# models. For a deeper dive into this concept, see Polymorphic Rendering.
1. Creating new models
First, create a C# class in the TheExampleApp/Models directory to represent your new Contentful content type. This class must implement one of the marker interfaces defined in IModule.cs to be recognized by the system.
IModule: The base interface for all modules.ILessonModule: For modules that appear within a lesson.ILayoutModule: For modules that are part of a general page layout.
Example: Suppose you create a "Video" content type in Contentful with the ID video and fields for a title and a video URL. The corresponding C# model would look like this:
csharp
// TheExampleApp/Models/VideoModule.cs
using TheExampleApp.Models;
using Contentful.Core.Models;
namespace TheExampleApp.Models
{
/// <summary>
/// Represents a video module from Contentful.
/// </summary>
public class VideoModule : ILayoutModule // Or ILessonModule, depending on usage
{
/// <summary>
/// The system properties from Contentful.
/// This is required by the IModule interface.
/// </summary>
public SystemProperties Sys { get; set; }
/// <summary>
/// The title of the video.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The URL of the video (e.g., a YouTube embed URL).
/// </summary>
public string VideoUrl { 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
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
2. Updating ModulesResolver
The ModulesResolver class is responsible for mapping Contentful content type IDs to their corresponding C# types during deserialization. You must register your new model here.
Open TheExampleApp/Configuration/ModulesResolver.cs and add an entry to the _types dictionary. The key is the Contentful content type ID (e.g., video), and the value is the C# type (typeof(VideoModule)).
csharp
// TheExampleApp/Configuration/ModulesResolver.cs
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) },
// Add your new module here
{ "video", typeof(VideoModule) }
};
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
3. Creating views
Finally, create a Razor partial view to render the HTML for your new module. By convention, module views are placed in Pages/Shared/Modules/ and named after the model (e.g., _VideoModule.cshtml). The view will receive the model instance.
Example: Create Pages/Shared/Modules/_VideoModule.cshtml:
csharp
@* Pages/Shared/Modules/_VideoModule.cshtml *@
@model TheExampleApp.Models.VideoModule
<div class="video-module-container">
<h2>@Model.Title</h2>
<iframe
width="560"
height="315"
src="@Model.VideoUrl"
frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>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
The application's rendering logic will now automatically use this partial view whenever it encounters a VideoModule object in a list of modules to be rendered.
Adding new pages
The application uses ASP.NET Core Razor Pages. Adding a new page involves creating the page files, configuring routing if necessary, and fetching the required data from Contentful in the page model.
1. Creating Razor Pages
A Razor Page consists of two files: a .cshtml file for the view markup and a .cshtml.cs file for the page model logic.
- Create a new folder under
Pages/(e.g.,Pages/About/). - Add an
Index.cshtmlandIndex.cshtml.csfile to this folder.
The page model should inherit from BasePageModel to get access to the IContentfulClient.
csharp
// Pages/About/Index.cshtml.cs
using System.Threading.Tasks;
using Contentful.Core;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TheExampleApp.Models;
namespace TheExampleApp.Pages.About
{
public class AboutModel : BasePageModel
{
public AboutModel(IContentfulClient client) : base(client)
{
}
public Layout AboutPage { get; set; }
public async Task OnGet()
{
// Fetch the 'about' page layout from Contentful
var queryBuilder = QueryBuilder<Layout>.New
.ContentTypeIs("layout")
.FieldEquals(f => f.Slug, "about-us") // Assuming a slug field
.Include(4); // Include linked entries up to 4 levels deep
AboutPage = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
}
}
}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
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
2. Routing configuration
For simple routes like /about, no special configuration is needed. ASP.NET Core will automatically map the URL to Pages/About/Index.cshtml.
For more complex, parameterized routes, you must add a convention in Startup.cs. The existing course routing provides a clear example.
csharp
// TheExampleApp/Startup.cs in ConfigureServices
services.AddMvc().AddRazorPagesOptions(
options => {
// Existing routes
options.Conventions.AddPageRoute("/Courses", "Courses/Categories/{category?}");
options.Conventions.AddPageRoute("/Courses", "Courses/{slug}/lessons");
options.Conventions.AddPageRoute("/Courses/Lessons", "Courses/{slug}/lessons/{lessonSlug}");
// Example: Add a new route for a team member profile page
// This maps the URL /team/{memberSlug} to the page /Pages/Team/Profile.cshtml
options.Conventions.AddPageRoute("/Team/Profile", "team/{memberSlug}");
});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
3. Contentful integration
The page model is responsible for fetching its own data from Contentful. As seen in Pages/Index.cshtml.cs, this is typically done in the OnGet() or OnGetAsync() method using the injected IContentfulClient.
Key practices include:
- Use
QueryBuilder: Construct type-safe queries to fetch entries. - Filter by a unique field: Use a
slugor another unique identifier to fetch the specific entry for the page. - Set the locale: Ensure you are fetching content for the correct language by using
LocaleIs(). The current locale is available from theHttpContext. - Use
Include(): Specify an include level to resolve linked entries (like modules) in a single API call, preventing N+1 query problems.
csharp
// From TheExampleApp/Pages/Index.cshtml.cs
public async Task OnGet()
{
var queryBuilder = QueryBuilder<Layout>.New
.ContentTypeIs("layout")
.FieldEquals(f => f.Slug, "home")
.Include(4)
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString());
var indexPage = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
IndexPage = indexPage;
// ...
}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
Adding new locales
The application supports internationalization (i18n) for both dynamic content from Contentful and static UI strings. For a high-level overview, see the Localization documentation.
1. Updating SupportedCultures
The primary list of application-supported locales is defined in Startup.cs. To add a new language, add its CultureInfo to this static list.
csharp
// TheExampleApp/Startup.cs
public static List<CultureInfo> SupportedCultures = new List<CultureInfo>
{
//When adding supported locales make sure to also add a static translation files for the locale under /wwwroot/locales
new CultureInfo("en-US"),
new CultureInfo("de-DE"),
// Add your new locale here, e.g., French (France)
new CultureInfo("fr-FR"),
};1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Important: This list controls the UI culture. The application also has logic in CustomRequestCultureProvider to handle fallbacks if a locale is available in Contentful but not in this list.
2. Creating translation files
Static UI strings (labels, button text, etc.) are stored in JSON files located in wwwroot/locales/.
- Copy an existing file, such as
en-US.json. - Rename it to match the new locale code (e.g.,
fr-FR.json). - Translate all the string values within the new file.
json
// wwwroot/locales/en-US.json (snippet)
{
"homeLabel": "Home",
"coursesLabel": "Courses",
"settingsLabel": "Settings"
}
// wwwroot/locales/fr-FR.json (snippet)
{
"homeLabel": "Accueil",
"coursesLabel": "Cours",
"settingsLabel": "Paramètres"
}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
The JsonViewLocalizer service will automatically pick up and use these files based on the current UI culture.
3. Testing new locales
The application determines the active locale primarily from the locale query string parameter. To test your new locale, append it to the URL:
https://<your-app-url>/?locale=fr-FR
This will set the locale in the user's session, and subsequent requests will continue to use it. The CustomRequestCultureProvider in Startup.cs contains the full logic for how the locale is resolved from the query string and session.
Integrating new services
The application uses the built-in ASP.NET Core dependency injection (DI) container to manage services. For a detailed explanation of DI in this project, refer to Dependency Injection.
1. Dependency injection
To use a service within a Razor Page model, controller, or another service, request it via constructor injection. The DI container will provide an instance of the registered implementation.
csharp
// Example of injecting a custom INotificationService into a page model
public class ContactModel : PageModel
{
private readonly INotificationService _notificationService;
// The service is injected here
public ContactModel(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task<IActionResult> OnPostAsync()
{
// Use the service
await _notificationService.SendEmailAsync("admin@example.com", "New Contact Form Submission");
return Page();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2. Service registration
All services must be registered with the DI container in the ConfigureServices method of Startup.cs. Choose the appropriate service lifetime:
AddTransient: A new instance is created every time the service is requested.AddScoped: A single instance is created per HTTP request.AddSingleton: A single instance is created for the entire application lifetime.
csharp
// TheExampleApp/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// ... existing registrations
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddContentful(Configuration);
services.AddTransient<IVisitedLessonsManager, VisitedLessonsManager>();
// Register your new service here
// Example: Registering a notification service with a transient lifetime
services.AddTransient<INotificationService, EmailNotificationService>();
services.AddMvc().AddRazorPagesOptions(
// ...
);
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3. Configuration
If your service requires configuration values (e.g., API keys, connection strings), they should be managed through the IConfiguration system.
- Add your settings to
appsettings.json. - Create a strongly-typed options class to hold the settings.
- Bind the configuration section to your options class in
ConfigureServices. - Inject
IOptions<YourOptionsClass>into your service.
This approach decouples your service from static configuration files and allows for environment-specific overrides. The registration of the Contentful client in Startup.cs is a complex but illustrative example of service and options configuration.