Appearance
Are you an LLM? You can read better optimized documentation at /configuration/service_configuration.md for this page in Markdown format
Service configuration
This document provides a detailed technical overview of the service and application configuration within the TheExampleApp project, focusing on the Startup.cs class. It is intended for developers responsible for maintaining and extending the application's core functionalities.
The Startup.cs class is the heart of an ASP.NET Core application's configuration. It defines the services the application will use (dependency injection) and how the application will respond to HTTP requests (the middleware pipeline).
Startup.cs configuration
In TheExampleApp, the Startup class is invoked by the web host builder in Program.cs. It contains two primary methods that are called by the ASP.NET Core runtime:
ConfigureServices(IServiceCollection services): Used to register services with the built-in dependency injection (DI) container.Configure(IApplicationBuilder app, IHostingEnvironment env): Used to define the HTTP request processing pipeline by configuring middleware.
The order of registrations in Configure is critical as it defines the sequence in which middleware components process an incoming request.
csharp
// TheExampleApp/Program.cs
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
// The runtime looks for a class named Startup and calls its methods.
.UseStartup<Startup>()
.Build();
}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
ConfigureServices method
This method is responsible for setting up the application's services for dependency injection.
Registering Contentful services
The application's primary data source is the Contentful headless CMS. The services are configured to allow for dynamic switching of Contentful spaces and API keys (e.g., for previewing content).
- Standard Registration:
services.AddContentful(Configuration)registers the basic Contentful services using settings fromappsettings.json. - Custom
IContentfulClient: The application overrides the defaultIContentfulClientregistration to provide more dynamic behavior. This is a key architectural decision. A transientIContentfulClientis registered, which is created per-request. This allows the client to useContentfulOptionsthat might be stored in the user's session, enabling features like previewing different Contentful spaces via query parameters. - Options Management:
IContentfulOptionsManageris registered as a singleton to manage the application'sContentfulOptions, allowing them to be modified at runtime. - Staging Environment: For staging deployments, a custom
HttpClientis injected using aStagingMessageHandler. This handler intercepts requests to the Contentful API and redirects them to a specified staging host, allowing testing against a non-production Contentful environment.
csharp
// TheExampleApp/Startup.cs
// 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>();
// The manager retrieves options, potentially from the user's session.
var options = ip.GetService<IContentfulOptionsManager>().Options;
var contentfulClient = new ContentfulClient(client,
options);
var version = typeof(Startup).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion;
// Adds application info to the User-Agent header for Contentful API tracking.
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Custom service implementations
Several custom services are registered to handle application-specific logic. For more information on the overall DI strategy, see the Dependency Injection architecture documentation.
| Service | Implementation | Lifetime | Description |
|---|---|---|---|
IHttpContextAccessor | HttpContextAccessor | Singleton | Provides access to the current HttpContext from within services. Essential for services that need to inspect request details or manage session state. |
IViewLocalizer | JsonViewLocalizer | Singleton | A custom implementation for localization that reads UI string translations from JSON files located in wwwroot/locales. See the Localization documentation for details. |
IVisitedLessonsManager | VisitedLessonsManager | Transient | Manages tracking of which course lessons a user has visited, likely using session state. |
IBreadcrumbsManager | BreadcrumbsManager | Transient | Allows controllers and Razor Pages to modify the breadcrumbs generated by the Breadcrumbs middleware. See the Breadcrumbs documentation. |
IFileProvider | EmbeddedFileProvider | Singleton | Provides access to files embedded as resources within the application's compiled assembly. |
MVC and Razor Pages setup
The application uses a combination of MVC and Razor Pages.
services.AddMvc(): Registers all the necessary services for MVC and Razor Pages.AddRazorPagesOptions: Configures custom URL routing conventions for Razor Pages, making URLs more user-friendly. This is used to map dynamic routes for courses and lessons.
csharp
// TheExampleApp/Startup.cs
services.AddMvc().AddRazorPagesOptions(
options => {
// Maps /Courses/Categories/{category?} to the /Courses.cshtml Razor Page.
options.Conventions.AddPageRoute("/Courses", "Courses/Categories/{category?}");
// Maps /Courses/{slug}/lessons to the /Courses/Index.cshtml Razor Page.
options.Conventions.AddPageRoute("/Courses/Index", "Courses/{slug}/lessons");
// Maps /Courses/{slug}/lessons/{lessonSlug} to the /Courses/Lessons.cshtml Razor 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
Session configuration
Session state is enabled and configured with a non-standard timeout.
services.AddSession(): Registers the services required for session state.IdleTimeout: The session timeout is set to 2 days.
Warning: The default session
IdleTimeoutin ASP.NET Core is 20 minutes. The 2-day timeout in this application is a specific project requirement. For most applications, such a long timeout is not recommended due to security and resource management considerations.
csharp
// TheExampleApp/Startup.cs
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
Configure method
This method builds the HTTP request pipeline by chaining together middleware components. The order of middleware is crucial.
Middleware pipeline
The request pipeline is configured as follows:
- Exception Handling: Catches exceptions and displays either a developer-friendly error page or a generic error page (configured first but applies to all subsequent middleware).
- URL Rewriting: Redirects certain URL patterns and enforces HTTPS (in non-development environments).
- Static Files: Serves static assets like CSS, JavaScript, and images from the
wwwrootdirectory. - Session: Enables session state for the request.
- Request Localization: Determines the correct culture for the request based on query strings and session values.
- Status Code Pages: Re-executes the pipeline with a different path for non-successful status codes (e.g., 404 Not Found).
- Custom Middleware:
UseBreadcrumbs()andUseDeeplinks()are called. - MVC: Adds the MVC routing middleware to the pipeline to handle requests for controllers and Razor Pages.
Request localization
The application uses a sophisticated localization strategy to handle multiple languages and fallbacks. This is managed by app.UseRequestLocalization().
The culture for a request is determined by a chain of IRequestCultureProvider instances:
CustomRequestCultureProvider(Query String Processor):- Checks for a
localekey in the query string (e.g.,?locale=de-DE). - Validates the requested locale against the list of available locales fetched from the Contentful space.
- If the locale is supported by Contentful but not by the application's static translation files (
SupportedCultures), it traverses Contentful's fallback chain to find a supported locale. - The original requested locale and the determined fallback are stored in the session for subsequent requests.
- Checks for a
QueryStringRequestCultureProvider: A standard provider that also looks for thelocalequery string key.CustomRequestCultureProvider(Session Reader):- Reads the culture and fallback culture from the session.
- The fallback culture takes precedence if it exists. This ensures that even if the user navigates away, the UI remains in a consistent, supported language.
If no provider determines a culture, it defaults to en-US. For a complete guide, see the Localization documentation.
Static files
app.UseStaticFiles() enables the serving of files directly from the web root directory (wwwroot). This includes CSS, JavaScript, images, and the JSON files used by JsonViewLocalizer.
Error handling
Error handling behavior differs between development and production environments.
- Development:
app.UseDeveloperExceptionPage()provides a detailed error page with stack traces.app.UseBrowserLink()enables live browser updates. - Production:
app.UseExceptionHandler("/Error")redirects the user to a generic/Errorpage upon an unhandled exception. - Status Code Pages:
app.UseStatusCodePagesWithReExecute("/Error")handles non-500 error codes (like 404 Not Found) by re-executing the request pipeline with the path/Error, allowing for a consistent error page design.
URL rewriting
The UseRewriter middleware is configured with two rules:
- HTTPS Redirect (non-development environments only): A custom rule inspects the
X-Forwarded-Protoheader. If a request was forwarded from a load balancer over HTTP, it issues a 301 permanent redirect to the equivalent HTTPS URL. This is essential for ensuring security in production environments behind a reverse proxy. - Path Redirect: A regex-based rule redirects requests from
courses/{slug}/lessonsto/courses/{slug}. This helps enforce canonical URLs.
csharp
// TheExampleApp/Startup.cs
var options = new RewriteOptions();
if (env.IsDevelopment())
{
// ...
}
else
{
// Rule 1: Enforce HTTPS behind a reverse proxy
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;
}
} );
app.UseExceptionHandler("/Error");
}
// Rule 2: Clean up course URLs
options.AddRedirect("courses/(.*)/lessons$", "/courses/$1");
app.UseRewriter(options);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
Custom middleware
The application defines two custom middleware components, registered via extension methods.
Breadcrumbs middleware
Registered with app.UseBreadcrumbs(), this middleware is responsible for generating a breadcrumb trail based on the request URL.
- Functionality: The middleware automatically generates breadcrumbs by:
- Adding a "Home" breadcrumb first (using the localized
homeLabeltranslation) - Splitting the URL path into segments (using "/" as delimiter, removing empty entries)
- Creating a
Breadcrumbobject for each segment with:- Path constructed incrementally from the URL segments
- Label derived from the segment text (hyphens replaced with spaces)
- Attempting to translate labels using
IViewLocalizerby looking up{lowerCaseFirstChar(label)}Labelkeys
- Adding a "Home" breadcrumb first (using the localized
- Data Storage: The resulting list of
Breadcrumbobjects is stored inHttpContext.Items["breadcrumbs"], making it available to downstream components like Razor views for rendering. - Modification: The
IBreadcrumbsManagerservice can be injected into controllers or pages to modify breadcrumbs after generation. TheReplaceCrumbForSlug(string slug, string label)method replaces the label of a breadcrumb whose current label matches theslugparameter.
For more details, refer to the Breadcrumbs feature documentation.
Deeplinks middleware
Registered with app.UseDeeplinks(), the Deeplinker middleware provides a powerful integration point with the Contentful web app. It allows content editors to "deeplink" from the CMS into the application to preview content using different settings.
It inspects the query string for the following parameters:
space_id,preview_token,delivery_token: If all three are present, it creates a newContentfulOptionsobject with these credentials. The credentials are validated using data annotations, and if validation fails, the errors and attempted options are serialized to the session, and the user is redirected to/settingswith cache control headers set to prevent caching. If validation succeeds, the options are stored in the user's session. This allows an editor to preview content from a different Contentful space or with different API keys without restarting the application or changing configuration files.api: Can be set tocpa(Contentful Preview API) to enable preview mode. When this parameter is present without the full credential set, it updates only theUsePreviewApiflag while preserving existing credentials from the configuration.editorial_features: Can be set toenabledordisabledto toggle UI elements related to in-context editing, with the state stored in the session.
This middleware is essential for a smooth content editing and previewing workflow. For configuration details, see the Application Settings documentation.
csharp
// TheExampleApp/Configuration/Deeplinker.cs
public async Task Invoke(HttpContext context)
{
var query = context.Request.Query;
// Logic to handle switching Contentful space/keys via query string
if (query.ContainsKey("space_id") && query.ContainsKey("preview_token") && query.ContainsKey("delivery_token"))
{
// Creates new ContentfulOptions with provided credentials
var currentOptions = new ContentfulOptions();
currentOptions.DeliveryApiKey = query["delivery_token"];
currentOptions.SpaceId = query["space_id"];
currentOptions.PreviewApiKey = query["preview_token"];
currentOptions.UsePreviewApi = query.ContainsKey("api") && query["api"] == "cpa";
// Validates the options
var validationResult = /* validation logic */;
if (validationResult.Any())
{
// On validation failure: serialize errors to session and redirect to /settings
context.Session.SetString("SettingsErrors", JsonConvert.SerializeObject(modelStateErrors));
context.Session.SetString("SettingsErrorsOptions", JsonConvert.SerializeObject(validateableOptions));
context.Response.Redirect("/settings");
context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
context.Response.Headers.Add("Pragma", "no-cache");
return;
}
else
{
// On success: store in session
context.Session.SetString(nameof(ContentfulOptions), JsonConvert.SerializeObject(currentOptions));
}
}
// Logic to handle switching between Preview and Delivery APIs
else if (query.ContainsKey("api"))
{
// Updates UsePreviewApi flag while preserving existing credentials
var currentOptions = new ContentfulOptions
{
UsePreviewApi = query["api"] == "cpa",
// ... copies existing options from manager
};
context.Session.SetString(nameof(ContentfulOptions), JsonConvert.SerializeObject(currentOptions));
}
// Logic to enable/disable editorial features
if (query.ContainsKey("editorial_features") && string.Equals(query["editorial_features"], "enabled", StringComparison.InvariantCultureIgnoreCase))
{
context.Session.SetString("EditorialFeatures", "Enabled");
}
else if (query.ContainsKey("editorial_features"))
{
context.Session.SetString("EditorialFeatures", "Disabled");
}
await _next(context);
}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
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