Appearance
Are you an LLM? You can read better optimized documentation at /features/runtime_settings.md for this page in Markdown format
Runtime settings
This document provides a technical overview of the dynamic runtime configuration feature in TheExampleApp. This functionality allows developers and testers to change key application settings, specifically Contentful API credentials, for the duration of a user session without requiring an application restart or code changes.
Dynamic configuration
The application is designed to override its default configuration on a per-session basis. The default settings for connecting to Contentful are loaded from appsettings.json at startup. However, the runtime settings feature allows these to be temporarily replaced by values provided through the UI.
This is achieved through a custom implementation that leverages ASP.NET Core's session management and dependency injection system. The core components enabling this are:
ISession: User-provided settings are serialized and stored in the current user's session state. This ensures the overrides are isolated to that user and are not persisted permanently. See Session Management for more details.ContentfulOptionsManager: A custom service that acts as the single source of truth for Contentful options during a request. It intelligently decides whether to provide the default options fromappsettings.jsonor the overridden options from the user's session.- Dependency Injection Factory: The
IContentfulClientis registered in the DI container using a transient lifetime and a factory function. For each request, this factory consults theContentfulOptionsManagerto get the correct settings before creating the client instance. This ensures that every Contentful API call within that request uses the appropriate credentials (either default or session-based).
This architecture allows the application to remain stateless at the server level while providing stateful configuration on a per-user basis.
Settings page
The user interface for managing these runtime settings is located at the /Settings route. This page provides the following functionalities:
- Update Contentful Credentials: Users can provide a new Space ID, Content Delivery API (CDA) Access Token, and Content Preview API (CPA) Access Token.
- Enable Editorial Features: A checkbox to enable or disable editorial features, which is also managed via the session.
- Reset to Default: A "Reset Credentials" button allows the user to clear the session-specific settings and revert to using the default configuration from
appsettings.json. - Share Configuration: When using session credentials, a shareable link is generated. This link contains the current runtime settings as query parameters, allowing another user to instantly load the same configuration.
The view (Settings.cshtml) displays the current connection status, indicating whether the application is using the default server credentials or session-based overrides.
html
<!-- TheExampleApp/Pages/Settings.cshtml -->
@if (Model.IsUsingCustomCredentials == false)
{
<div class="status-block__content">
<div class="status-block__message">
<p><em>@Localizer["usingServerCredentialsLabel"]</em></p>
<p>
<strong>@Localizer["connectedToSpaceLabel"]:</strong><br />
@Model.SpaceName (ID:@Model.AppOptions.SpaceId)
</p>
<p>
<strong>@Localizer["credentialSourceLabel"]:</strong><br />
@Localizer["loadedFromLocalFileLabel"] <a href="..." target="_blank" rel="noopener">appsettings.json</a>
</p>
</div>
</div>
}
else
{
<div class="status-block__content">
<div class="status-block__message">
<p><em>@Localizer["usingSessionCredentialsLabel"]</em></p>
<!-- ... -->
<form method="post" action="/Settings?handler=ResetCredentials" asp-antiforgery="true">
<p>
<strong>@Localizer["applicationCredentialsLabel"]:</strong><br />
<button type="submit">@Localizer["resetCredentialsLabel"]</button><br />
<a class="status-block__sharelink" href="@($"...")">@Localizer["copyLinkLabel"]</a>
</p>
</form>
</div>
</div>
}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
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
Implementation
The implementation is centered around the SettingsModel Razor Page, a custom ContentfulOptionsManager, and the DI configuration in Startup.cs.
Session-based Configuration Override
When a user submits the settings form, the OnPost handler in Settings.cshtml.cs is invoked. If the model is valid, it serializes the new ContentfulOptions object to a JSON string and stores it in the session.
csharp
// TheExampleApp/Pages/Settings.cshtml.cs
public IActionResult OnPost(SelectedOptions appOptions)
{
if (!ModelState.IsValid)
{
TempData["Invalid"] = true;
return Page();
}
// ... handle editorial features ...
var currentOptions = new ContentfulOptions
{
DeliveryApiKey = appOptions.AccessToken,
SpaceId = appOptions.SpaceId,
UsePreviewApi = _manager.Options.UsePreviewApi,
PreviewApiKey = appOptions.PreviewToken,
// ... other options are preserved
};
// Serialize the options object and store it in the session.
HttpContext.Session.SetString(nameof(ContentfulOptions), JsonConvert.SerializeObject(currentOptions));
TempData["Success"] = true;
return RedirectToPage("Settings");
}
public IActionResult OnPostResetCredentials()
{
// Clear the session key to revert to default settings.
HttpContext.Session.SetString(nameof(ContentfulOptions), "");
return RedirectToPage("Settings");
}
public IActionResult OnPostSwitchApi(string api, string prevPage)
{
// Retain all options except whether to use the preview API or not.
var options = new ContentfulOptions
{
UsePreviewApi = api == "cpa",
DeliveryApiKey = _manager.Options.DeliveryApiKey,
SpaceId = _manager.Options.SpaceId,
PreviewApiKey = _manager.Options.PreviewApiKey,
// ... other options are preserved
};
HttpContext.Session.SetString(nameof(ContentfulOptions), JsonConvert.SerializeObject(options));
return Redirect($"{prevPage}?api={api}");
}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
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
The ContentfulOptionsManager Pattern
This manager is the key to the dynamic configuration system. It's registered as a singleton and has access to both the default application options (via IOptions<ContentfulOptions>) and the current HttpContext (via IHttpContextAccessor).
Its Options property contains the core logic: it checks the session for a serialized ContentfulOptions string. If found, it deserializes it and returns the object. If not, it returns the default options.
csharp
// TheExampleApp/Configuration/ContentfulOptionsManager.cs
public class ContentfulOptionsManager : IContentfulOptionsManager
{
private ContentfulOptions _options;
private readonly IHttpContextAccessor _accessor;
public ContentfulOptionsManager(IOptions<ContentfulOptions> options, IHttpContextAccessor accessor)
{
_options = options.Value; // Default options from appsettings.json
_accessor = accessor;
}
/// <summary>
/// Gets the currently configured ContentfulOptions either from session, if present,
/// or from the application configuration.
/// </summary>
public ContentfulOptions Options {
get {
var sessionString = _accessor.HttpContext.Session.GetString(nameof(ContentfulOptions));
if (!string.IsNullOrEmpty(sessionString))
{
// If session contains settings, deserialize and use them.
return JsonConvert.DeserializeObject<ContentfulOptions>(sessionString);
}
// Otherwise, fall back to the default settings.
return _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
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
This manager is then used in Startup.cs to configure the IContentfulClient for each request.
csharp
// TheExampleApp/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// ...
// Register the custom options manager as a singleton.
services.AddSingleton<IContentfulOptionsManager, ContentfulOptionsManager>();
// Register the Contentful client with a transient lifetime using a factory.
services.AddTransient<IContentfulClient, ContentfulClient>((ip) => {
var client = ip.GetService<HttpClient>();
// The factory resolves the manager and gets the correct options for the current request.
var options = ip.GetService<IContentfulOptionsManager>().Options;
var contentfulClient = new ContentfulClient(client, options);
// ...
return contentfulClient;
});
// ...
}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
Form Handling and Validation
The SettingsModel uses a nested class, SelectedOptions, to represent the form data. This class implements IValidatableObject to perform complex, multi-field validation.
The validation logic is particularly robust:
- It checks that all required fields (
SpaceId,AccessToken,PreviewToken) are non-empty. - If all fields are present, it performs live API calls to the Contentful Delivery and Preview APIs to verify that the credentials are correct.
This prevents users from saving invalid credentials that would break the application. The MakeTestCalls method attempts to fetch the space details using the provided tokens and handles specific ContentfulExceptions to return user-friendly error messages. For more on the Contentful APIs, see the API Configuration and Preview Mode documentation.
csharp
// TheExampleApp/Pages/Settings.cshtml.cs - SelectedOptions class
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var localizer = validationContext.GetService(typeof(IViewLocalizer)) as IViewLocalizer;
// Check that all required fields are non-empty
if (string.IsNullOrEmpty(SpaceId))
{
yield return new ValidationResult(localizer["fieldIsRequiredLabel"].Value, new[] { nameof(SpaceId) });
}
if (string.IsNullOrEmpty(AccessToken))
{
yield return new ValidationResult(localizer["fieldIsRequiredLabel"].Value, new[] { nameof(AccessToken) });
}
if (string.IsNullOrEmpty(PreviewToken))
{
yield return new ValidationResult(localizer["fieldIsRequiredLabel"].Value, new[] { nameof(PreviewToken) });
}
if(!string.IsNullOrEmpty(SpaceId) && !string.IsNullOrEmpty(AccessToken) && !string.IsNullOrEmpty(PreviewToken))
{
// We got all required fields. Make test calls to both APIs to verify credentials.
var httpClient = validationContext.GetService(typeof(HttpClient)) as HttpClient;
// Test both Delivery API and Preview API
foreach(var validationResult in MakeTestCalls(httpClient, localizer))
{
yield return validationResult;
}
}
}
private IEnumerable<ValidationResult> MakeTestCalls(HttpClient httpClient, IViewLocalizer localizer)
{
// Test Delivery API (CDA) credentials
try
{
var cdaClient = new ContentfulClient(httpClient, new ContentfulOptions
{
DeliveryApiKey = AccessToken,
SpaceId = SpaceId,
UsePreviewApi = false
});
var space = cdaClient.GetSpace().Result;
}
catch (AggregateException ae)
{
ValidationResult validationResult = null;
ae.Handle((ce) => {
if (ce is ContentfulException)
{
var contentfulException = ce as ContentfulException;
// Handle 401 Unauthorized (invalid access token)
if (contentfulException.StatusCode == 401) {
validationResult = new ValidationResult(localizer["deliveryKeyInvalidLabel"].Value, new[] { nameof(AccessToken) });
}
// Handle 404 Not Found (invalid space ID)
else if (contentfulException.StatusCode == 404) {
validationResult = new ValidationResult(localizer["spaceOrTokenInvalid"].Value, new[] { nameof(SpaceId) });
}
else {
validationResult = new ValidationResult(localizer["somethingWentWrongLabel"].Value, new[] { nameof(AccessToken) });
}
return true;
}
return false;
});
if (validationResult != null)
{
yield return validationResult;
}
}
// Test Preview API (CPA) credentials
try
{
var cpaClient = new ContentfulClient(httpClient, new ContentfulOptions
{
PreviewApiKey = PreviewToken,
SpaceId = SpaceId,
UsePreviewApi = true
});
var space = cpaClient.GetSpace().Result;
}
catch (AggregateException ae)
{
ValidationResult validationResult = null;
ae.Handle((ce) => {
if (ce is ContentfulException)
{
var contentfulException = ce as ContentfulException;
// Handle 401 Unauthorized (invalid preview token)
if (contentfulException.StatusCode == 401) {
validationResult = new ValidationResult(localizer["previewKeyInvalidLabel"].Value, new[] { nameof(PreviewToken) });
}
// Handle 404 Not Found (invalid space ID)
else if (contentfulException.StatusCode == 404) {
validationResult = new ValidationResult(localizer["spaceOrTokenInvalid"].Value, new[] { nameof(SpaceId) });
}
else {
validationResult = new ValidationResult(localizer["somethingWentWrongLabel"].Value, new[] { nameof(PreviewToken) });
}
return true;
}
return false;
});
if (validationResult != null)
{
yield return validationResult;
}
}
}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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
Use cases
This dynamic configuration feature is primarily intended for technical users and supports several key workflows:
- Testing Different Spaces: Developers and QA can easily point the application to different Contentful spaces (e.g.,
development,staging,feature-branch-space) to test new content models or features without altering the baseappsettings.jsonfile or requiring a new deployment. For more on the base setup, see Contentful Setup. - Application Demonstration: Useful for demonstrating the application with different data sets. A presenter can switch between various Contentful spaces live to showcase different content scenarios.
- Simplified Development: Streamlines the development process when working with multiple content sources, allowing for rapid switching and testing.
- Troubleshooting: Allows for quick validation of credentials if there is a suspected issue with the Contentful connection.