Appearance
Are you an LLM? You can read better optimized documentation at /getting_started/introduction.md for this page in Markdown format
Introduction
This document provides a detailed technical introduction to TheExampleApp, a server-side rendered web application built with ASP.NET Core. It is designed to serve as a comprehensive guide for developers responsible for its maintenance and future development. The application's primary function is to demonstrate a robust integration with Contentful, a headless Content Management System (CMS).
Overview
TheExampleApp is a demonstration project that showcases best practices for consuming content from Contentful within a .NET ecosystem. It is not a Single-Page Application (SPA) but follows a traditional server-side rendering (SSR) model using ASP.NET Core and the Razor templating engine. The application fetches all its display content, such as courses, lessons, and categories, directly from a Contentful space.
By default, the application is connected to a Contentful space with read-only access. For the full end-to-end experience, including content editing capabilities, you can configure it with read and write access to your own Contentful space.
Note: As stated in the project's
README.md, this repository is no longer officially maintained as of January 2023. It remains a valuable learning resource, and developers are encouraged to fork and adapt it for their own purposes.
A hosted version of the application is available at https://the-example-app-csharp.herokuapp.com/.
For initial setup and prerequisites, please refer to the following guides:
What you'll learn
By reviewing this documentation, you will gain an understanding of:
- Application's Purpose: How the app serves as a practical example for integrating a headless CMS.
- Contentful Fundamentals: What Contentful is and the benefits of using a headless CMS to decouple content from presentation. As the
README.mdstates, "Contentful provides a content infrastructure for digital teams to power content in websites, apps, and devices." - Key Features: The application's core capabilities, including:
- Consumption of content from Contentful's Delivery and Preview APIs.
- Content modeling and how content structure affects application design.
- A sophisticated internationalization (i18n) strategy with dynamic locale fallbacks.
- Server-side rendering of Markdown content.
- Custom routing to create user-friendly URL structures.
- "Editorial features" that link content directly back to the Contentful web app for editing.
- One-click deployment options to Heroku and Azure for quick testing and demonstration.
- Target Audience: This application is primarily for .NET developers who want to learn how to build content-driven websites using ASP.NET Core and Contentful.
Application Architecture
The application employs a straightforward yet powerful architecture centered around the ASP.NET Core framework and Contentful's content delivery APIs.
High-Level Architecture Overview
The architecture is a classic server-side rendering model:
- A user's browser sends an HTTP request to the ASP.NET Core application (default port: 3000).
- The ASP.NET Core routing middleware maps the request URL to a specific Razor Page or MVC controller.
- The handler/action method communicates with the Contentful service layer, which uses the
contentful.aspnetcoreSDK to fetch the required content from the Contentful Delivery API (or Preview API if configured). - The fetched content, which may be in Markdown format, is processed (e.g., rendered to HTML using the
Markdiglibrary). - The processed data is passed to a Razor view.
- The Razor engine renders the final HTML page on the server.
- The complete HTML document is sent back to the user's browser.
This headless approach completely separates the content management (handled in Contentful) from the presentation layer (the ASP.NET Core application), allowing each to be developed and scaled independently. For a more detailed breakdown, see the Application Overview.
Technology Stack Summary
The application is built on the Microsoft .NET ecosystem with a focus on stability and proven technologies.
| Category | Technology | Version / Details |
|---|---|---|
| Backend | .NET Core | netcoreapp2.1 |
| ASP.NET Core | 2.1.5 (via Microsoft.AspNetCore.All metapackage) | |
| Language | C# | |
| Web Server | Kestrel | |
| Frontend | Templating | Razor (Server-Side Rendering) |
| Package Manager | Bower (Note: bower.json contains no dependencies) | |
| Data & CMS | Headless CMS | Contentful |
| SDK | contentful.aspnetcore | |
| Markdown Processor | Markdig (0.15.4) for rendering Markdown fields from Contentful into HTML. | |
| Tooling | Scaffolding | Microsoft.VisualStudio.Web.CodeGeneration.Design |
| Build/Project | .NET CLI, Visual Studio Solution (.sln) |
Key Implementation Details
The following sections dive into the core implementation patterns and technical decisions within the codebase, with a focus on Startup.cs, the application's configuration heart.
Contentful Integration
Contentful is integrated at the application's core. Configuration and service registration are handled within Startup.cs.
1. Configuration: API keys and the Space ID are managed in appsettings.json, allowing for easy configuration across different environments.
json
// TheExampleApp/appsettings.json
{
// ...
"ContentfulOptions": {
"DeliveryApiKey": "df2a18b8a5b4426741408fc95fa4331c7388d502318c44a5b22b167c3c1b1d03",
"PreviewApiKey": "10145c6d864960fdca694014ae5e7bdaa7de514a1b5d7fd8bd24027f90c49bbc",
"SpaceId": "qz0n5cdakyl9",
"UsePreviewApi": false
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
2. Service Registration: The basic Contentful services are registered with a single line in ConfigureServices.
csharp
// TheExampleApp/Startup.cs - in ConfigureServices method
services.AddContentful(Configuration);1
2
2
3. Custom IContentfulClient: The application provides a custom IContentfulClient implementation. The source code comments clarify the rationale: it allows for in-memory updates to ContentfulOptions and appends a custom user-agent string for tracking purposes. This is a crucial detail for debugging and API monitoring.
csharp
// TheExampleApp/Startup.cs - in ConfigureServices method
// This would normally not be needed, but since we want to load our ContentfulOptions from memory if they're changed within the application
// we provide our own implementation logic for the IContentfulClient
services.AddSingleton<IContentfulOptionsManager, ContentfulOptionsManager>();
services.AddTransient<IContentfulClient, ContentfulClient>((ip) => {
var client = ip.GetService<HttpClient>();
var options = ip.GetService<IContentfulOptionsManager>().Options;
var contentfulClient = new ContentfulClient(client,
options);
var version = typeof(Startup).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion;
// Append application info to the user-agent header for Contentful's APIs
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
4. Staging Environment Redirection: For testing against a non-production Contentful environment, the application includes a custom HttpClientHandler. If the StagingHost environment variable is set, this handler intercepts outgoing HTTP requests to the Contentful API and redirects them to the specified staging host.
csharp
// TheExampleApp/Startup.cs
public class StagingMessageHandler : HttpClientHandler
{
public string StagingHost { get; set; }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var regex = new Regex("contentful");
// Replaces the first occurrence of "contentful" in the URL with the staging host
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
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Internationalization (i18n)
The application features a sophisticated i18n implementation that synchronizes the application's locale with the locales available in Contentful.
1. Supported Cultures: The static cultures supported by the application's UI (e.g., for static text in .json files) are defined in a 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"),
};1
2
3
4
5
6
7
2
3
4
5
6
7
2. Dynamic Locale Resolution: The core of the i18n logic resides in a CustomRequestCultureProvider. This provider enables complex logic for determining the correct culture for a request.
The resolution order is as follows:
- Query String (
?locale=...): The provider first checks for alocaleparameter in the query string. - Contentful Validation: It then validates this locale against the list of available locales fetched directly from the Contentful space. This ensures the app doesn't try to request a language that doesn't exist in the CMS.
- Fallback Logic: If the requested locale is present in Contentful but not in the app's static
SupportedCultureslist, it traverses the fallback chain defined within Contentful (e.g.,de-ATmight fall back tode-DE). - Session Storage: The resolved locale (
locale) and any fallback used for static UI strings (fallback-locale) are stored in the user's session for subsequent requests.
This design ensures that users can view content in any language available in Contentful, while the application UI gracefully falls back to a supported language if a direct translation is not available.
csharp
// TheExampleApp/Startup.cs - in Configure method
app.UseRequestLocalization(new RequestLocalizationOptions
{
RequestCultureProviders = new List<IRequestCultureProvider>
{
// Custom provider to handle Contentful locales and fallbacks
new CustomRequestCultureProvider(async (s) => {
// ... complex logic as described above ...
}),
// Standard provider for query strings
new QueryStringRequestCultureProvider()
{
QueryStringKey = "locale"
},
// Custom provider to read the culture from the session
new CustomRequestCultureProvider(async (s) => {
// ... logic to pull locale from session ...
})
},
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = SupportedCultures,
SupportedUICultures = SupportedCultures
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Routing and URL Generation
The application uses a combination of Razor Pages conventions and MVC routing to create clean, user-friendly URLs.
1. Custom Razor Page Routes: Instead of relying on default file-based routing (e.g., /Courses/Lessons.cshtml), the application defines custom route conventions to map more intuitive URLs to the correct pages.
csharp
// TheExampleApp/Startup.cs - in ConfigureServices method
services.AddMvc().AddRazorPagesOptions(
options => {
// Maps /Courses/Categories/some-category to the /Courses.cshtml page
options.Conventions.AddPageRoute("/Courses", "Courses/Categories/{category?}");
// Maps /Courses/course-slug/lessons to the /Courses/Index.cshtml page
options.Conventions.AddPageRoute("/Courses/Index", "Courses/{slug}/lessons");
// Maps /Courses/course-slug/lessons/lesson-slug to the /Courses/Lessons.cshtml page
options.Conventions.AddPageRoute("/Courses/Lessons", "Courses/{slug}/lessons/{lessonSlug}");
});1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
2. URL Rewriting: A URL rewrite rule is configured to redirect requests from a trailing /lessons path to the main course page, preventing duplicate content and improving SEO.
csharp
// TheExampleApp/Startup.cs - in Configure method
var options = new RewriteOptions();
// ...
options.AddRedirect("courses/(.*)/lessons$", "/courses/$1");
app.UseRewriter(options);1
2
3
4
5
6
2
3
4
5
6
Session Management
Session state is used primarily for persisting the user's selected locale. A notable configuration is the extended session timeout.
csharp
// TheExampleApp/Startup.cs - in ConfigureServices method
services.AddSession(options => {
// IdleTimeout is set to a high value to confirm to requirements for this particular application.
// In your application you should use an IdleTimeout that suits your application needs or stick to the default of 20 minutes.
options.IdleTimeout = TimeSpan.FromDays(2);
});1
2
3
4
5
6
2
3
4
5
6
Gotcha: The two-day IdleTimeout is specific to this application's requirements. Developers reusing this code should be aware of this and adjust the timeout to a value appropriate for their own application's security and user experience needs.
Security: HTTPS Redirection
In production environments (non-development), the application implements automatic HTTPS redirection. When a request arrives with the X-Forwarded-Proto header set to http (typically from a reverse proxy or load balancer), the application issues a 301 Moved Permanently redirect to the HTTPS version of the URL.
csharp
// TheExampleApp/Startup.cs - in Configure method (production only)
options.Add((c) => {
var request = c.HttpContext.Request;
if(request.Headers.ContainsKey("X-Forwarded-Proto") && request.Headers["X-Forwarded-Proto"] == "http")
{
var response = c.HttpContext.Response;
response.StatusCode = StatusCodes.Status301MovedPermanently;
c.Result = RuleResult.EndResponse;
response.Headers[HeaderNames.Location] = "https://" + request.Host + request.Path + request.QueryString;
}
});1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
This ensures all production traffic uses encrypted connections, which is essential for security best practices.